Let’s Encrypt für lokale Dienste

Ich habe in meinem lokalen Netzwerk diverse Dienste laufen, auf die ich mit der IP-Adresse als URL zugreife. Das ist im Alltag ziemlich umständlich, wenn ich z.B. gerade das Lesezeichen für einen Dienst (z.B. meine Octoprint-Installation) nicht zur Hand habe aber darauf z.B. vom Handy zugreifen möchte.
Weiterhin nutze ich auch in meinem lokalen Netzwerk eine verschlüsselte Verbindung soweit möglich. Dies hat jedoch zusätzlich zur Folge, dass ich entweder mit einer Zertifikatswarnung leben muss oder ein Self-Signed-Zertifikat erstellen muss und dieses auf allen Endgeräten installieren muss. So habe ich bislang das Thema behandelt – bis mein root-Zertifikat letztens ausgelaufen ist und ich es verlängern musste. Was für eine Arbeit…

Da kam es mir ganz Recht, dass ich kürzlich bei Wolfgang in seinem YouTube-Kanal über eine coole Anleitung gestolpert bin, die sich dieses Problems annimmt.

Ich bin dieser Anleitung gefolgt und nachdem ich dank der Hilfe im reddit-Forum vom NgnixProxyManager einen Denkfehler ausbügeln konnte, läuft das ganze jetzt mit minimalem Aufwand mit sprechender URL, Let’s Encrypt-Zertifikat und aus dem Internet nicht erreichbar!

Dazu habe ich mir bei Porkbun eine DOT-party bestellt. Anschließend habe ich lokal in meinem Netzwerk via Docker einen NginxProxyManager Stack eingerichtet. In den DNS-Einstellungen von Porkbun habe ich für meine DOT-party Domain die folgenden Einträge konfiguriert:

TYPEHOSTANSWER
Astolzfuss.party192.168.9.200
Aproxy.stolzfuss.party192.168.9.200
CNAME*.stolzfuss.partyproxy.stolzfuss.party
DNS Records für stolzfuss.party

Damit habe ich für meine Domain stolzfuss.party alles für den Zugriff aus meinem lokalen Netzwerk heraus fertig eingerichtet.

Im NPM habe ich mir nun ein Wildcard Zertifikat für *.stolzfuss.party via Let’s Encrypt einrichtet. Mit diesem Zertifikat kann ich nun den ersten Proxy im NPM erstellen: proxy.stolzfuss.party mit dem neu erstellten Zertifikat und den nachfolgenden Daten:

SchemeForward Hostname / IPForward Port
http192.168.9.20081
NPM Proxy Host Konfiguration

Wenn ich nun die NPM-Instanz via https://proxy.stolzfuss.party aufrufe, lädt bei mir im lokalen Netzwerk die NPM-Admin-Webseite. Alle anderen Zugriffe, die von außerhalb meines lokalen Netzwerkes auf diese Seite zugreifen wollen, bekommen einen Fehler, da die geroutete IP-Adresse in meinem lokalen Netz liegt.

Analog zum NPM kann ich nun alle meine lokalen Webseiten mit eigenen Subdomains in der Admin-Oberfläche vom NPM hinterlegen.
Und ich brauche mir in Zukunft keine IP-Adresse mehr merken und habe auch kein Problem mehr mit ablaufenden Zertifikaten.

Vielen Dank Wolfgang für diese coole Idee und das Tutorial!

Astronomy Picture of the Day

Bei der Auflistung meiner Services habe ich meinen Astronomy Picture of the Day – Service bereits erwähnt. Hier folgt nun das, was hinter diesem Mini-Service steht.

Ich nutze schon seit Jahren den Wallpaper Cycler, um meine Bildschirme mit individuellen Hintergrundbildern zu versorgen. Mit diesem Tool kann ich Bilder überlagern, Effekte zu Objekten zufügen und vieles mehr.

Da mich das Thema Weltraum schon lange fasziniert (für mich war die Mondlandung 1969 eines der beeindruckendsten technischen Ereignisse des 20. Jahrhunderts), habe ich als Grundlage für meinen Wallpaper Cycler schon lange ein Grundset an astronomischen Bildern genutzt. Vor einiger Zeit bin ich dann aber über die „Astronomy Picture of the Day“ – Seite der NASA gestolpert. Leider werden die Bilder dort auf der Seite nicht auch als tägliches Bild unter wiederkehrendem Namen hinterlegt. Jedes Bild enthält dort noch zusätzliche Informationen. Diese sind zwar super spannend zu lesen, aber für meinen Zweck, dieses Bild als Hintergrundbild für meinen Desktop zu nutzen ungeeignet.

Auf der Suche nach einer Lösung für mein Problem habe ich mir lange Zeit mit einem simplen Shell-Skript beholfen, welches cron gesteuert das Bild herunterlädt. Leider war mein Shell-Skript alles andere als robust. So kann es zum Beispiel vorkommen, dass auf der APOD-Seite statt eines Bildes ein Video präsentiert wird. Das Shell-Skript hat hier trotzdem stur das aktuelle Bild ins Archiv verschoben, bevor es versucht hat, das neue Bild zu laden. Damit stand ich dann manchmal ohne Hintergrundbild da.

Da ich kürzlich angefangen habe, für meine privaten Sachen auch python zu lernen, war das der Startschuss, mein Shell-Skript durch ein python Skript zu ersetzen.
Ziel war es, ein Skript zu haben, welches ohne Installation periodisch auf dem Server ausgeführt werden kann. Hierbei kommt zum einen python selbst als auch ein spezielles Docker image zum Einsatz. Das python Skript bietet bestimmt noch Optimierungspotenzial. Für den Moment reicht für mich jedoch völlig aus.

import requests
from bs4 import BeautifulSoup
import urllib.parse
import os
import shutil
from datetime import date


def download_image(url, save_path):
    response = requests.get(url, stream=True)
    response.raise_for_status()
    with open(save_path, 'wb') as file:
        for chunk in response.iter_content(chunk_size=8192):
            file.write(chunk)


def get_first_image_url(html, url):
    soup = BeautifulSoup(html, 'html.parser')
    img_tag = soup.find('img')
    if img_tag:
        img_url = img_tag.get('src')
        if img_url and not img_url.startswith('data:'):
            return urllib.parse.urljoin(url, img_url)
    return None


def move_to_archive(src, target_folder):
    current_date = date.today().strftime("%Y%m%d")
    new_img_name = f"{current_date}-img.jpg"
    new_path = os.path.join(target_folder, new_img_name)
    if os.path.exists(src):
        shutil.move(src, new_path)


def create_folders():
    if not os.path.exists("data"):
        os.makedirs("data")
    if not os.path.exists("data/archive"):
        os.makedirs("data/archive")


def save_image_from_apod(url, save_path, archive_path):
    response = requests.get(url)
    response.raise_for_status()
    image_url = get_first_image_url(response.text, url)
    if image_url:
        create_folders()
        move_to_archive(save_path, archive_path)
        download_image(image_url, save_path)
        print(f"Das Bild wurde erfolgreich gespeichert: {save_path}")
    else:
        print("Es wurde kein Bild gefunden.")


apod_url = 'https://apod.nasa.gov/apod/'  # URL der HTML-Seite hier angeben
img_name = 'data/img.jpg'  # Speicherpfad für das Bild hier angeben
archive = 'data/archive'

save_image_from_apod(apod_url, img_name, archive)

Dieses Skript läuft lokal in meiner IDE wunderbar. Doch es sollte ja ohne größere Installationen auf dem Server laufen. Hier hilft mir docker weiter. Ich habe mir ein Dockerfile gebaut, welches mir ein spezielles python docker Image erstellt. In diesem Image sind genau die Abhängigkeiten enthalten, die dieses Skript benötigt.

FROM python:slim

ARG uid=1000
ARG gid=1000

RUN pip install --upgrade pip
RUN python -m pip install requests
RUN python -m pip install beautifulsoup4
RUN python -m pip install Pillow

USER ${uid}:${gid}

WORKDIR /python

CMD [ "python", "./main.py" ]

Mit diesem docker Image habe ich einmalig einen Container erstellt. Nachdem das Skript abgearbeitet ist, beendet sich dieser Container automatisch. Diesen Container rufe ich nun via cron täglich mit docker start apod_python auf. Zusätztlich ist dieser Aufruf noch via healthchecks absichert, damit ich informiert werde, wenn der cronjob mal nicht gelaufen ist.

Das gespeicherte Bild liefere ich über einen einfachen nginx-Container aus. Damit habe ich mir eine einfache Lösung geschaffen, die genau mein Problem löst. Zusätzlich ist das ganze so flexibel, dass ich mit einem ähnlichen python Skript mir auch tagesaktuell das jeweils neue doodle von Google für meine Speed Dial 2 Erweiterung laden kann.

Authentik benutzen

Nachdem ich die Installation von authentik in Kombination mit meiner traefik Installation zum laufen bekommen habe (siehe hier), ging es nun ans Werk, das System authentik zu verstehen. Sprich, wie kann ich zum Beispiel einzelne Anwendungen für Benutzer sperren.

Die Lösung hierzu lag schlicht im Lesen der sehr guten Dokumentation von authentik. Im folgenden werde ich das Prinzip aber am Beispiel des traefik Dashboards und einem WhoAmI-Container, einem authentik-Admin-User und einem normalen authentik-User beschreiben.

Als erstes muss der vorhandene Embedded Outpost noch bearbeitet werden: Applications => Outposts. In der Konfiguration steht per default docker_network: null. Dieses muss durch docker_network: proxy ersetzt werden. Diesen Teil hatte ich zunächst vergessen und ich wurde immer auf mein traefik-Dashboard geleitet, egal welche URL ich aufgerufen habe.

Weiterhin gilt (aktuell): Zwischen Application und Provider besteht eine 1-zu-1 Beziehung. In der Version 2023.5 wurden zwar auch Backchannel Provider eingeführt, aber diesen Punkt in der Dokumentation habe ich mir noch nicht angeschaut. Ich arbeite aktuell mit der 1-zu-1 Beziehung.

Damit erstelle ich mir dann für meine WhoAmI-Seite einen passende Proxy Provider analog zum bestehenden ForwardAuth Provider aus der Installationsanleitung. Es ist wieder eine single Application, diesmal mit dem External Host https://whoami.deine-domain.de.

Genau so folgt als nächstes eine neue Application, die den neu Erstellen Who-am-i-Provider nutzt. Als Policy engine mode nutze ich aktuell weiterhin any.

Nun müssen wir noch im embedded Outpost die neue Anwendung mit markieren, so dass dieser Outpost beiden Providern zugewiesen ist.

Nun erstelle ich mir unter Directory => Users zwei neue Test-User (traefik und whoami). Für den Moment zum Testen habe ich hier einfach nur den Usernamen vergeben. Anschließend klickt man auf die beiden neu erstellten Benutzer und kann dann mit Set Password noch ein Passwort für den Benutzer festlegen.

Gleiches mache ich mit Gruppen. Ich erstelle zwei Testgruppen (traefik und whoami) und weise die beiden Gruppen den jeweiligen Benutzern zu.

Bevor es jetzt mit der Zuweisung der Benutzer/Gruppen auf die Anwendungen los geht, teste ich in einem Inkognito-Browser-Fenster, ob aktuell beide Benutzer Zugriff auf die beiden Seiten https://traefik.deine-domain.de sowie https://whoami.deine-domain.de haben. Wenn beide Benutzer beide Seiten nach dem Login öffnen können, ist bis hier alles korrekt.

Als nächstes Testen wir die Einschränkung auf Benutzerebene. Dazu öffne ich zunächst unter Applications => Applications das Traefik Dashboard und wechsle auf den Reiter Policy/Group/User Bindings.
Nach einem Klick auf Create Binding wähle ich als Typ User aus. Für das Traefik Dashboard wähle ich den Traefik-Benutzer aus und klicke auf Create.
Analog dazu verfahre ich mit der Who-am-i-Application. Nur wähle ich hier den whoami Benutzer aus.

Jetzt wird der Test mit dem Inkognito-Browser-Fenster wiederholt. Wenn alles klappt, hat der Benutzer traefik Zugriff auf das Traefik-Dashboard und der Benutzer whoami Zugriff auf die Who-am-i Seite. Anders herum muss jetzt die Meldung Request has been denied erscheinen.

Nun ändere ich für beide Anwendungen das Binding von User auf Group. Dabei bekommt die Traefik-Anwendung das Group Binding traefik zugewiesen und die Who-am-i-Anwendung das Group Binding whoami zugewiesen. Da bislang jede dieser beiden Gruppen nur genau einen Benutzer hat, darf sich am Verhalten auf des Logins (noch) nichts ändern. Der Benutzer traefik darf weiterhin das Traefik-Dashboard sehen und der Benutzer whoami darf die Who-am-I-Seite sehen.

Im letzten Schritt weisen wir dem Benutzer traefik zusätzlich noch den die Gruppe whoami zu. Damit hat nun durch das Group Binding auch der Benutzer traefik Zugriff auf die Who-am-i-Seite. Der Benutzer whoami darf das Traefik-Dashboard weiterhin nicht sehen.

Zum Abschluss noch eine wichtige Bemerkung! Wenn Ihr hier mit User- oder Group-Bindings arbeitet, dann könnt Ihr auch Euren Admin-User vom Aufruf aussperren. Sprich, mein Admin Benutzer hat mit der obigen Konfiguration weder Zugriff auf das Traefik-Dashboard noch auf die Who-am-i-Seite. Am besten definiert Ihr Euch also für Eure Anwendungen jeweils Gruppen und weist Eurem Admin Benutzer die jeweiligen Gruppen zu und nutzt nur das Group-Binding. Damit sperrt Ihr Euch dann nicht von den einzelnen Anwendungen aus.

Man kann hier bestimmt noch viel tiefer in die Policies und den ganzen Kram einsteigen. Für mich reicht hier aktuell das Wissen um die Benutzer/Gruppen-Bindings aus, um im Freundeskreis einzelen Personen gezielt Zugriff auf einzelne Seiten zu gewähren.