Dienste hinter dem heimischen Anschluss öffentlich erreichbar machen: WireGuard-Tunnel zu einem VPS, Caddy terminiert TLS.
Find a file
2026-07-26 11:36:00 +00:00
README.md README.md aktualisiert 2026-07-26 11:36:00 +00:00

Edge-Proxy: WireGuard + Caddy in einem Podman-Pod

Öffentlich erreichbarer Reverse-Proxy für Dienste hinter NAT. Ein VPS mit öffentlicher IP nimmt den Traffic an und schiebt ihn durch einen WireGuard-Tunnel zu diesem Pod. Caddy terminiert TLS (Let's Encrypt) und proxyt an die Backends im LAN.


Funktionsprinzip

Internet-Nutzer
    │  :80 / :443
    ▼
VPS  (öffentliche IP, WireGuard-Server, DNAT + MASQUERADE)
    │  durch den WG-Tunnel
    ▼
┌─────────────── Podman-Pod (ein Netzwerk-Namespace) ───────────────┐
│                                                                   │
│  wireguard-Container ──baut wg0──►  10.0.100.2/24                  │
│                                        ▲                          │
│  caddy-Container ──lauscht :80/:443────┘  (sieht wg0 direkt)       │
│         │                                                         │
└─────────┼─────────────────────────────────────────────────────────┘
          ▼
     Backends im LAN (selbes Subnetz)

Kernidee: Beide Container teilen sich denselben Netzwerk-Namespace (ein Pod). Caddy lauscht dadurch direkt auf wg0. Der Rückweg der Antworten ist damit automatisch symmetrisch — auf diesem Host sind keine Forwarding-, DNAT- oder Routing-Regeln nötig. Genau das macht das Setup portabel: drei Dateien kopieren, starten, fertig.

Öffentlicher Traffic kommt nur durch den Tunnel — der Pod hat bewusst kein Host-Portmapping (-p 80:80).


Dateien

Datei Rolle
edge-proxy.yaml Was läuft: Pod-Definition (2 Container, Volumes, Caps).
edge-proxy.kube Wie systemd startet: Quadlet-Unit → edge-proxy.service.
wg0.conf WireGuard-Client-Config (PrivateKey! → chmod 600).
Caddyfile Reverse-Proxy-Regeln + Domains für automatisches HTTPS.

Die .kube ist der moderne Standard-Weg, podman kube play unter systemd zu fahren (ersetzt das deprecatete podman generate systemd). Der Service ruft intern beim Start kube play <yaml> und beim Stop kube down auf — der Pod wird also aus der YAML neu erzeugt, nicht nur ge-startet.


Verzeichnis-Layout auf dem Host (rootful)

/etc/containers/systemd/
├── edge-proxy.kube          # Quadlet-Unit
└── edge-proxy.yaml          # Pod-Definition

/opt/edge-proxy/
├── wg_confs/
│   └── wg0.conf             # chmod 600, root:root
├── Caddyfile
├── caddy-data/              # PERSISTENT: Certs + ACME-Account  ← nicht löschen
└── caddy-config/            # Caddy autosave

Warum /data persistent sein muss: Ohne persistentes caddy-data/ holt Caddy nach jedem Neustart neue Zertifikate und läuft in die Let's-Encrypt- Rate-Limits (5 identische Certs/Woche). Das Volume ist Pflicht.


Erstinstallation

# 1) Host: WireGuard-Kernelmodul dauerhaft laden
sudo modprobe wireguard
echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf
#    -> danach kann SYS_MODULE in der YAML entfallen (ein Privileg weniger)

# 2) Verzeichnisse + Dateien
sudo mkdir -p /etc/containers/systemd /opt/edge-proxy/wg_confs
sudo cp edge-proxy.yaml edge-proxy.kube /etc/containers/systemd/
sudo cp wg0.conf  /opt/edge-proxy/wg_confs/
sudo cp Caddyfile /opt/edge-proxy/Caddyfile

# 3) Rechte auf den privaten Schlüssel (WICHTIG)
sudo chmod 600 /opt/edge-proxy/wg_confs/wg0.conf
sudo chown root:root /opt/edge-proxy/wg_confs/wg0.conf
sudo chmod 700 /opt/edge-proxy/wg_confs

# 4) (Optional) Trockenlauf mit sichtbarer Ausgabe, bevor systemd übernimmt
sudo podman kube play /etc/containers/systemd/edge-proxy.yaml
sudo podman exec edge-proxy-wireguard wg show     # Handshake da?
sudo podman logs edge-proxy-caddy                 # Certs / Fehler?
sudo podman kube down /etc/containers/systemd/edge-proxy.yaml  # wieder abräumen!

# 5) systemd übernimmt
sudo systemctl daemon-reload
sudo systemctl start edge-proxy.service
sudo systemctl status edge-proxy.service

Kein systemctl enable nötig — Quadlet-Units sind "generated". Der Boot-Start kommt aus der [Install]-Sektion (WantedBy=multi-user.target) und wird beim daemon-reload verdrahtet. systemctl enable würde sogar fehlschlagen.


Betrieb / Alltag

Rootful → System-Scope, immer mit sudo systemctl (nicht --user).

sudo systemctl start   edge-proxy.service
sudo systemctl stop    edge-proxy.service
sudo systemctl restart edge-proxy.service
sudo systemctl status  edge-proxy.service
journalctl -u edge-proxy.service -f          # Live-Log

sudo systemctl is-enabled edge-proxy.service # "generated"/"enabled" = Boot-Start ok

Nach Änderung an edge-proxy.yaml, edge-proxy.kube, Caddyfile oder wg0.conf:

sudo systemctl daemon-reload      # nur bei .kube/.yaml-Struktur-Änderungen nötig
sudo systemctl restart edge-proxy.service

Container-Namen folgen dem Muster <pod>-<container>: edge-proxy-wireguard, edge-proxy-caddy.


Health-Checks

# WireGuard: läuft der Tunnel?  -> "latest handshake" + "transfer: … received"
sudo podman exec edge-proxy-wireguard wg show

# Caddy: liegen echte Production-Certs im persistenten Volume?
sudo podman exec edge-proxy-caddy ls -R /data/caddy/certificates
#   Pfad "acme-v02.api.letsencrypt.org-directory/<domain>/" mit .crt/.key = ok

# End-to-End durch den kompletten Weg (DNS -> VPS -> Tunnel -> Caddy)
curl -vI https://<deine-domain>
#   erwartet: issuer "Let's Encrypt" + HTTP-Statuscode vom Backend

Ein ruhiger Caddy-Log ist der beste Log. Fehlen obtaining certificate- Zeilen, heißt das meist: Certs sind schon im /data und werden nur geladen.


Troubleshooting

Symptom Wahrscheinliche Ursache / Fix
wg show zeigt keinen Handshake AllowedIPs oder PersistentKeepalive = 25 in wg0.conf prüfen (Peer-Block).
Tunnel steht, aber HTTPS bricht ab Rückweg-Routing auf dem VPS (MASQUERADE/SNAT). Muss dort stimmen — hier nichts zu tun.
Certs kommen nicht Öffentliches DNS muss auf die VPS-IP zeigen; VPS muss 80 UND 443 forwarden.
Cert-Fehler / Rate-Limit beim Testen Erst mit LE-Staging testen (acme_ca https://acme-staging-v02.api.letsencrypt.org/directory als globale Caddyfile-Option), dann auf Prod.
world accessible-Warnung (WG) sudo chmod 600 /opt/edge-proxy/wg_confs/wg0.conf.
Service startet nicht via systemd sudo /usr/libexec/podman/quadlet -dryrun zeigt, was Quadlet aus der .kube generiert.
Name-Kollision beim Start Alten Pod abräumen: sudo podman kube down …/edge-proxy.yaml.

Nur kosmetisch (ignorierbar)

  • Caddyfile input is not formatted → caddy fmt --overwrite.
  • failed to sufficiently increase receive buffer size → HTTP/3-UDP-Puffer; optional net.core.rmem_max / wmem_max per sysctl erhöhen.

Wichtige Voraussetzungen (Merkzettel)

  1. DNS der Domains → öffentliche VPS-IP.
  2. VPS forwardet :80 und :443 in den Tunnel und macht den Rückweg (MASQUERADE/SNAT). Dieser Teil lebt auf dem VPS, nicht hier.
  3. wg0.conf: PersistentKeepalive = 25 im [Peer] (hält das NAT-Mapping offen).
  4. Rootful — WireGuard braucht NET_ADMIN + Kernelmodul.
  5. caddy-data/ niemals löschen (enthält die Zertifikate).

Umzug auf einen anderen Host

Portabilität war das Designziel:

# auf dem neuen Host:
sudo modprobe wireguard && echo wireguard | sudo tee /etc/modules-load.d/wireguard.conf
# edge-proxy.yaml + edge-proxy.kube  -> /etc/containers/systemd/
# wg0.conf + Caddyfile               -> /opt/edge-proxy/...   (chmod 600 wg0.conf!)
sudo systemctl daemon-reload && sudo systemctl start edge-proxy.service

Die Zertifikate holt Caddy auf dem neuen Host automatisch neu — sofern DNS/VPS weiter passen. Optional caddy-data/ mitkopieren, dann sind die alten Certs sofort da.