feat: initial WireGuard#100
Conversation
Grafana is no longer publicly accessible. It's now only reachable at https://grafana.internal for clients connected to the WireGuard VPN. - Add stacks/traefik/conf.d/internal.yml with VPN-only routes for grafana.internal and glowroot.internal (served with mkcert wildcard cert) - Mount overlays/wireguard/certs/ into stacks/traefik so certs are shared between the wireguard and traefik compose projects - Fix wireguard overlay incompatibilities with multi-instance approach: use bind-mount for certs instead of named volume, change network from `frontend` to `proxy`, remove orphaned traefik service override that referenced the now-deleted single-instance dynamic.yml - Add service definitions to overlays/wireguard/traefik/internal.yml (grafana-internal → monitoring-grafana:3000, glowroot-internal → app:4000) - Remove stacks/traefik/conf.d/monitoring.yml.template and the start-monitoring Makefile step that generated it (public Grafana route) - Remove GRAFANA_HOSTNAME from monitoring .env.template and generate script; hardcode GF_SERVER_ROOT_URL=https://grafana.internal in docker-compose.yml - Gitignore overlays/wireguard/certs/ and config/ (contain private keys) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds wireguard-proxy service that listens on 10.8.0.1:443 (wg0 IP) inside the WireGuard container's network namespace and forwards to traefik:443. Docker DNS resolves 'traefik' per connection, so it handles Traefik container restarts without needing hardcoded IPs or iptables DNAT rules. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Unreferenced near-duplicate of stacks/traefik/conf.d/internal.yml, which is the file Traefik actually loads.
`make get-vpn-ca` writes the root CA to the working tree so clients can install it; it should not land in commits.
The mkcert container that writes the key and the Traefik container that reads it both run as root, so the key does not need to be world-readable.
The `domain: "*.internal"` env var was never consumed: the compose file overrides the image entrypoint with `command: sh /mkcert.sh`, and the script hardcodes the two host names. The generated cert is already a 2-SAN cert, not a wildcard.
Sweep em dashes out of files added or modified on this branch. Pre-existing em dashes in master (README intro and footer) are left alone as out of scope.
The vishnunair/docker-mkcert image only publishes a `latest` tag, so pinning by digest is the only stable reference available.
Previously mkcert.sh produced a single 2-SAN cert (grafana.internal + glowroot.internal) shared by both routes. Switch to one cert per host and list both in Traefik's tls.certificates so SNI selects the right one. Existing deployments must clear the wireguard-certs volume to drop the old internal.crt/internal.key on first run after this change.
Replaces the pinned vishnunair/docker-mkcert digest with a minimal custom Dockerfile that fetches mkcert directly from the upstream release URL, removing the dependency on a third-party image.
radnov
left a comment
There was a problem hiding this comment.
It was a PITA to test this on an M1 Mac, but I think it works as expected, with a few caveats that I've added as comments.
| FROM alpine:3.23 | ||
|
|
||
| ARG MKCERT_VERSION=v1.4.4 | ||
| ARG TARGETARCH=amd64 |
There was a problem hiding this comment.
The explicit default overrides Buildkit's auto-populated TARGETARCH, so the build always downloads the amd64 mkcert binary.
On an M* Mac it then runs under Rosetta and crashes with the following error during cert generation:
runtime: lfstack.push invalid packing
| ARG TARGETARCH=amd64 | |
| ARG TARGETARCH |
| wireguard-certs: | ||
| name: wireguard-certs |
There was a problem hiding this comment.
wireguard-certs volume is declared in both stacks/traefik and overlays/wireguard, producing
volume "wireguard-certs" already exists but was created for project "traefik" (expected "wireguard")
It's just a warning, but worth declaring it with external: true here.
| mkcert "${host}" | ||
| mv "${host}.pem" "/certs/${host}.crt" | ||
| mv "${host}-key.pem" "/certs/${host}.key" | ||
| chmod 600 "/certs/${host}.key" |
There was a problem hiding this comment.
I asked if this can be 600 instead of 644 but seems like it's the following error:
ERR error="open /etc/traefik/certs/grafana.internal.key: permission denied" filename=internal.yml providerName=file
and causes Traefik to silently fall back to its built-in default cert.
From a VPN client:
$ curl -kv --resolve grafana.internal:443:10.8.0.1 https://grafana.internal/ 2>&1 | grep subject
- subject: CN=TRAEFIK DEFAULT CERT
Suggested fix (verified end-to-end on a fresh deploy - Traefik then serves the mkcert cert and https://grafana.internal verifies cleanly):
| chmod 600 "/certs/${host}.key" | |
| chown nobody:nobody "/certs/${host}.key" | |
| chmod 640 "/certs/${host}.key" |
There was a problem hiding this comment.
On MacOs with Docker Desktop and the "Use kernel networking for UDP" Networking option enabled (the default), inbound UDP packets to a published container port have their source IP rewritten before delivery.
We should add a note somewhere in this doc that this setting should be disabled for the VPN to work on MacOs.
| volumes: | ||
| - ./config:/config | ||
| - ./coredns/Corefile:/config/coredns/Corefile:ro |
There was a problem hiding this comment.
FYI
Nested "file-on-dir" bind mounts fail on MacOs with VirtioFS (default) - reported at docker/desktop-feedback#420
"dir-on-dir" nesting was fixed in docker/desktop-feedback#264
I changed the default VirtioFS to gRPC FUSE as a workaround. Maybe it's worth adding a note about this in the docs until this is fixed (or not), too.
Co-authored-by: radnov <radoslav@dhis2.org>
feat: align server-tools with VPN deployment
No description provided.