Zum Inhalt springen

Interner Dienst mit gültigem Zertifikat über rootless Podman-Quadlets und Caddy


Ein gültiges TLS-Zertifikat setzt grundsätzlich einen aus dem Internet erreichbaren Dienst voraus. Rein internen Netzwerken fehlt dieser per Definition. Rootless Podman in Verbindung mit Quadlets und Caddy als Reverseproxy umgeht diese Einschränkung durch eine DNS-Challenge, die keinen offenen Port benötigt.

Ein Quadlet bindet einen rootless Podman-Container als regulären systemd-Dienst ein. Der Dienst startet mit dem System und wird nach einem Absturz automatisch neu gestartet.

Wiki.js dient im Folgenden als Beispiel für einen Dienst hinter diesem Setup. Die erstellten Quadlet- und Caddy-Dateien lassen sich auf jeden weiteren Dienst übertragen, der einen HTTP-Port bereitstellt.

Voraussetzungen

Das Setup erfordert Podman, eine eigene Domain bei einem DNS-Provider mit API und einen API-Key des Providers für diese Domain.

Die Einrichtung von rootless Podman ist im Beitrag zum Shiny Server beschrieben.

In Frage kommt jeder DNS-Provider mit eigener API, etwa Cloudflare, Route 53, DigitalOcean oder Hetzner.

Ein A-Record für wiki.example.com zeigt auf die interne IP des Hosts, etwa 192.168.1.10. Der Name löst damit öffentlich auf, während die Adresse dahinter nur im eigenen Netz erreichbar ist.

Der API-Key erhält die minimal notwendigen Rechte. Für die DNS-Challenge genügt ein Schlüssel, der TXT-Einträge für die eigene Domain anlegen kann. Darüber hinaus benötigt der Schlüssel keine Berechtigungen.

Caddy wird später die Ports 80 und 443 belegen. Der Kernel behandelt die Ports 1 bis 1023 als privilegiert und lässt sie nur von root binden. Für rootless Podman lässt sich diese Grenze verschieben. Der folgende Befehl erledigt das:

Terminal window
echo "net.ipv4.ip_unprivileged_port_start=80" \
| sudo tee /etc/sysctl.d/99-podman-ports.conf
sudo sysctl --system

Danach laufen Podman und der Container weiterhin unter dem angemeldeten Benutzer. Die Verschiebung gilt systemweit.

Bei einer Firewall auf dem Host benötigen die Ports 80 und 443 eine Freigabe.

Zuletzt erhält der Stack einen eigenen Benutzer am Host.

Terminal window
sudo useradd -m podman-svc
sudo loginctl enable-linger podman-svc
sudo -u podman-svc -i

enable-linger startet für den Benutzer eine dauerhafte systemd-Instanz, auch ohne Anmeldung. Alle folgenden Befehle laufen in dieser Shell und erfordern keine root-Rechte.

Zwei Variablen müssen in der Shell gesetzt werden, sie zeigen auf das Laufzeitverzeichnis des Benutzers.

Terminal window
export XDG_RUNTIME_DIR=/run/user/$(id -u)
export DBUS_SESSION_BUS_ADDRESS=unix:path=$XDG_RUNTIME_DIR/bus

Ohne sie erreichen weder podman noch systemctl --user die Benutzerinstanz. Der Caddy-Build bricht dann ab.

DNS-Challenge

Die DNS-Challenge legt einen TXT-Eintrag in der Zone an, den die Zertifizierungsstelle beim Nameserver abfragt. Der Host muss dafür keinen Port öffnen und ist während der Ausstellung nicht aus dem Internet erreichbar.

Caddy mit DNS-Modul

Der offizielle Caddy-Container enthält keine DNS-Provider-Module. Für eine DNS-Challenge wird mit xcaddy ein eigener Caddy gebaut, der das Modul des jeweiligen Providers einschließt. Eine Liste der Module findet sich unter github.com/caddy-dns. PROVIDER ist der Repo-Name in Kleinbuchstaben und erscheint weiter unten in der Caddyfile.

Die Containerfile liegt in einem eigenen Verzeichnis, etwa ~/caddy-dns.

Terminal window
mkdir -p ~/caddy-dns
Containerfile
FROM mirror.gcr.io/library/caddy:2-builder AS builder
RUN xcaddy build --with github.com/caddy-dns/PROVIDER
FROM mirror.gcr.io/library/caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Der Build läuft in diesem Verzeichnis:

Terminal window
cd ~/caddy-dns
podman build -t caddy-dns .

Caddyfile

Die Caddyfile liegt in ~/.config/caddy/Caddyfile.

Terminal window
mkdir -p ~/.config/caddy
~/.config/caddy/Caddyfile
wiki.example.com {
tls {
dns PROVIDER {env.DNS_API_TOKEN}
}
reverse_proxy wikijs:3000
}

Secrets

API-Key und Datenbankpasswort werden als Podman-Secrets angelegt, damit sie nicht in den Unit-Dateien stehen:

Terminal window
printf '%s' 'API-TOKEN' | podman secret create dns_api_token -
printf '%s' 'DB-PASSWORT' | podman secret create wiki_db_password -

Quadlet

Alle folgenden Dateien liegen in ~/.config/containers/systemd/, wo Podman nach Quadlet-Units sucht.

Terminal window
mkdir -p ~/.config/containers/systemd

wiki.network gibt den Containern des Stacks ein eigenes Netz, in dem sie sich über ihren Namen finden.

wiki.network
[Unit]
Description=Netz für Wiki.js
[Network]
NetworkName=wiki

Erforderlich sind vier Volume-Units, die sich nur im VolumeName unterscheiden.

wiki-db.volume
[Volume]
VolumeName=wiki-db
wiki-data.volume
[Volume]
VolumeName=wiki-data
caddy-data.volume
[Volume]
VolumeName=caddy-data
caddy-config.volume
[Volume]
VolumeName=caddy-config

caddy-data speichert die Zertifikate. Ohne dieses Volume holt Caddy sie bei jedem Neustart erneut.

wiki-db.container ist die Datenbank. Sie veröffentlicht keinen Port. Erreichbar ist sie nur im Netz wiki.

wiki-db.container
[Unit]
Description=PostgreSQL für Wiki.js
[Container]
ContainerName=wiki-db
Image=docker.io/library/postgres:17-alpine
Network=wiki.network
Volume=wiki-db.volume:/var/lib/postgresql/data
Secret=wiki_db_password,type=mount,target=/run/secrets/db_password
Environment=POSTGRES_DB=wiki
Environment=POSTGRES_USER=wiki
Environment=POSTGRES_PASSWORD_FILE=/run/secrets/db_password
[Service]
Restart=always
[Install]
WantedBy=default.target

wikijs.container liest das Passwort über DB_PASS_FILE aus demselben Secret.

wikijs.container
[Unit]
Description=Wiki.js
Requires=wiki-db.service
After=wiki-db.service
[Container]
ContainerName=wikijs
Image=ghcr.io/requarks/wiki:2
Network=wiki.network
Volume=wiki-data.volume:/wiki/data
Secret=wiki_db_password,type=mount,target=/run/secrets/db_password
Environment=DB_TYPE=postgres
Environment=DB_HOST=wiki-db
Environment=DB_PORT=5432
Environment=DB_NAME=wiki
Environment=DB_USER=wiki
Environment=DB_PASS_FILE=/run/secrets/db_password
[Service]
Restart=always
[Install]
WantedBy=default.target

caddy.container ist der einzige Container mit PublishPort. Der API-Key wird als Umgebungsvariable in den Container übergeben. Das Caddyfile liest ihn als {env.DNS_API_TOKEN}.

caddy.container
[Unit]
Description=Caddy
Requires=wikijs.service
After=wikijs.service
[Container]
ContainerName=caddy
Image=localhost/caddy-dns:latest
Network=wiki.network
PublishPort=80:80
PublishPort=443:443
Volume=%h/.config/caddy/Caddyfile:/etc/caddy/Caddyfile:ro,Z
Volume=caddy-data.volume:/data
Volume=caddy-config.volume:/config
Secret=dns_api_token,type=env,target=DNS_API_TOKEN
[Service]
Restart=always
[Install]
WantedBy=default.target

Ohne ContainerName benennt Podman die Container mit dem Präfix systemd-. Dann findet Caddy den Host wikijs nicht.

Starten

Terminal window
systemctl --user daemon-reload
systemctl --user start caddy

caddy startet wikijs und wiki-db über die Abhängigkeiten.

Wiki.js ist danach unter https://wiki.example.com erreichbar und legt beim ersten Aufruf das Administratorkonto an.

Wiki.js Setup-Bildschirm mit Feldern für Administratorkonto und Site URL