[Unit] Description=Caddy reverse proxy (custom build with googleclouddns + Coraza) Wants=network-online.target vaultwarden.service After=network-online.target vaultwarden.service # Wants= (not Requires=) on vaultwarden.service: a Vaultwarden restart # returns a brief 502 from reverse_proxy rather than tearing down Caddy # (which would drop TLS termination at 443). Client cache covers the gap. OnFailure=status-email-root@%n.service [Container] # Custom build pushed by cloudbuild.yaml. :latest is what `podman # auto-update` watches; bump deliberately by re-tagging a known-good # tag as :latest in Artifact Registry. The repo grants # roles/artifactregistry.reader to allUsers (declared in Pulumi) so the # host pulls without auth. # # The image path is templated from `caddy_image` which Ansible receives via # --extra-vars at deploy time, sourced from the Pulumi output `imageRepo`. Image={{ caddy_image }} ContainerName=caddy AutoUpdate=registry Network=vaultwarden.network # 443 on the host. The sysctl ip_unprivileged_port_start=443 (set by the # podman role) lets rootless caddy bind without CAP_NET_BIND_SERVICE. PublishPort=443:443 # The distroless image runs Caddy as nonroot (65532). keep-id maps the host # vault_user onto that uid, so the bind-mounted data/log dirs (owned by # vault_user) are writable inside; without it they appear as root:root 0750 # and Caddy can't open its logs or store certs. UserNS=keep-id:uid=65532,gid=65532 # The host sysctl above doesn't carry into the container's own netns, where # nonroot would otherwise be refused :443 (and :80, which Caddy binds for # redirects but isn't published). Same as Docker's per-container default. Sysctl=net.ipv4.ip_unprivileged_port_start=0 # Caddy's default storage is $HOME/.local/share/caddy — the container layer, # wiped on every auto-update recreate. Keep certs on the /data mount instead. Environment=XDG_DATA_HOME=/data # Caddyfile bind-mount, plus Caddy's data dir (certs, OCSP staples) and # log dir (access log + Coraza audit). All :Z under SELinux. Volume=%h/caddy/Caddyfile:/etc/caddy/Caddyfile:Z,ro Volume=%h/caddy/data:/data:Z Volume=%h/caddy/logs:/var/log/caddy:Z # VAULT_FQDN, WG_SUBNET + GCP_PROJECT come from the Ansible-rendered env file. EnvironmentFile=%h/caddy/caddy.env # GCP SA JSON for DNS-01. Podman mounts the secret at /run/secrets/gcp-sa-dns # by default for type=mount. Environment=GOOGLE_APPLICATION_CREDENTIALS=/run/secrets/gcp-sa-dns Secret=gcp-sa-dns,type=mount [Service] Restart=always RestartSec=10s TimeoutStartSec=300 [Install] WantedBy=default.target