fleet-manager: front the admin UI with Traefik (443 + Route 53 ACME), retire acme side-car
Management-plane ingress: a host-networked Traefik terminates the LE cert on :443
(Route 53 DNS-01, public-resolver propagation check) and forwards plaintext to
the manager running FLEET_MGR_ADMIN_BEHIND_PROXY=true on loopback :9500. Only the
admin plane rides Traefik — the agent gRPC/mTLS plane (:9600) stays direct (Traefik
can't terminate fleet-CA mTLS; hosts reach it via OPNsense L3 routing). This is the
k8s ingress-controller + cert-manager role in miniature.
- traefik/dynamic/fleet-manager.yml: file-provider router (Host rule -> manager),
passHostHeader so the WebAuthn RP origin matches.
- compose: traefik service (static config as CLI flags so ${ACME_EMAIL} interpolates)
+ traefik-acme volume; manager switched to behind-proxy defaults, RP_ORIGIN now
the port-less https origin.
- Retire the acme side-car (Traefik owns ACME now): remove the service + acme-state
volume; renew-loop.sh kept + relabelled as the self-TLS-mode cert helper.
- :443 only (admin UI is https-only) so it can't collide with a boot-LAN nginx :80.
This commit is contained in:
@@ -71,13 +71,17 @@ services:
|
||||
influxdb:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
# admin plane (HTTPS + passkey). Bind loopback; the mgmt proxy (holding the
|
||||
# LE cert) fronts it, or set the cert paths below + expose directly.
|
||||
# admin plane — passkey auth behind the Traefik mgmt ingress: Traefik
|
||||
# terminates the LE cert on :443 and forwards plaintext here on loopback,
|
||||
# so the listener MUST stay private (127.0.0.1) — only Traefik reaches it.
|
||||
FLEET_MGR_ADMIN_ADDR: "${FLEET_MGR_ADMIN_ADDR:-127.0.0.1:9500}"
|
||||
FLEET_MGR_ADMIN_BEHIND_PROXY: "${FLEET_MGR_ADMIN_BEHIND_PROXY:-true}"
|
||||
FLEET_MGR_ADMIN_INSECURE: "${FLEET_MGR_ADMIN_INSECURE:-false}"
|
||||
# Only used for self-TLS mode (BEHIND_PROXY=false + a cert, no Traefik):
|
||||
FLEET_MGR_ADMIN_CERT: "${FLEET_MGR_ADMIN_CERT:-/pki/admin/fullchain.pem}"
|
||||
FLEET_MGR_ADMIN_KEY: "${FLEET_MGR_ADMIN_KEY:-/pki/admin/privkey.pem}"
|
||||
FLEET_MGR_ADMIN_INSECURE: "${FLEET_MGR_ADMIN_INSECURE:-false}"
|
||||
# WebAuthn RP — MUST be the UI's real origin (origin-bound passkeys).
|
||||
# WebAuthn RP — MUST be the UI's real origin (origin-bound passkeys). Behind
|
||||
# Traefik on :443 this is the PORT-LESS https origin.
|
||||
FLEET_MGR_RP_ID: "${FLEET_MGR_RP_ID:?set FLEET_MGR_RP_ID in .env}"
|
||||
FLEET_MGR_RP_ORIGIN: "${FLEET_MGR_RP_ORIGIN:?set FLEET_MGR_RP_ORIGIN in .env}"
|
||||
FLEET_MGR_RP_NAME: "${FLEET_MGR_RP_NAME:-Fleet Command}"
|
||||
@@ -114,43 +118,55 @@ services:
|
||||
# The admin LE cert under ./pki/admin/ is produced by the `acme` side-car.
|
||||
- ./pki:/pki:ro
|
||||
|
||||
# --- admin-UI cert renewal side-car (Let's Encrypt DNS-01 via Route 53) -----
|
||||
# Long-running "compose cron": issues the UI cert on first start, then renews
|
||||
# within 30 days of expiry. Writes into ./pki/admin/ — the SAME dir the manager
|
||||
# reads (./pki:/pki:ro) — so the manager hot-reloads it with no restart. Needs
|
||||
# outbound reach to Let's Encrypt + the AWS Route 53 API (bridge NAT).
|
||||
# Bring this up BEFORE the manager (or the manager crash-loops until the cert
|
||||
# exists, then self-heals). See DEPLOY.md §2c.
|
||||
acme:
|
||||
image: goacme/lego:v4.17.4 # mirror via harbor if no dockerhub egress
|
||||
# --- management-plane ingress (Traefik) — admin UI on :443 ------------------
|
||||
# Terminates the Let's Encrypt cert (Route 53 DNS-01) on :443 and forwards
|
||||
# plaintext to the manager (behind-proxy mode, loopback :9500). Host networking
|
||||
# so it binds :443 on the mgmt interface and reaches the host-networked manager
|
||||
# on 127.0.0.1:9500. Needs outbound reach to Let's Encrypt + the AWS Route 53
|
||||
# API. ONLY the admin plane rides Traefik — the agent gRPC/mTLS plane (:9600) is
|
||||
# direct (Traefik can't terminate fleet-CA client-cert mTLS; hosts reach it via
|
||||
# OPNsense L3 routing). This is the k8s ingress-controller + cert-manager role
|
||||
# in miniature — the router/ACME model maps to Traefik Ingress + cert-manager.
|
||||
#
|
||||
# NOTE: only :443 (the admin UI is https-only; passkeys require it) so it can't
|
||||
# collide with a boot-LAN nginx :80 on a single-host lab. Route 53 DNS-01 needs
|
||||
# no :80. Set the Host() rule in traefik/dynamic/fleet-manager.yml to your UI host.
|
||||
traefik:
|
||||
image: traefik:v3.1 # mirror via harbor if no dockerhub egress
|
||||
restart: unless-stopped
|
||||
entrypoint: ["/bin/sh", "/renew-loop.sh"]
|
||||
network_mode: host
|
||||
command:
|
||||
- "--global.checknewversion=false"
|
||||
- "--global.sendanonymoususage=false"
|
||||
- "--entrypoints.websecure.address=:443"
|
||||
- "--providers.file.directory=/etc/traefik/dynamic"
|
||||
- "--providers.file.watch=true"
|
||||
- "--certificatesresolvers.le.acme.email=${ACME_EMAIL:?set ACME_EMAIL in .env}"
|
||||
- "--certificatesresolvers.le.acme.storage=/acme/acme.json"
|
||||
- "--certificatesresolvers.le.acme.dnschallenge.provider=route53"
|
||||
# Public resolvers for the propagation check (split-horizon DNS won't serve
|
||||
# the public _acme-challenge). First dry-run vs LE staging: also add
|
||||
# --certificatesresolvers.le.acme.caserver=https://acme-staging-v02.api.letsencrypt.org/directory
|
||||
- "--certificatesresolvers.le.acme.dnschallenge.resolvers=8.8.8.8:53,1.1.1.1:53"
|
||||
- "--log.level=INFO"
|
||||
environment:
|
||||
ACME_DOMAIN: "${FLEET_MGR_RP_ID:?set FLEET_MGR_RP_ID in .env}" # = the UI hostname
|
||||
ACME_EMAIL: "${ACME_EMAIL:?set ACME_EMAIL in .env}"
|
||||
ACME_STAGING: "${ACME_STAGING:-}" # set 1 for a dry-run
|
||||
# Public resolver(s) for the propagation check — the internal/split-horizon
|
||||
# DNS doesn't serve the public _acme-challenge record. Comma-separated host:port.
|
||||
ACME_RESOLVERS: "${ACME_RESOLVERS:-8.8.8.8:53}"
|
||||
# AWS creds for the Route 53 DNS-01 challenge (or omit + use an instance role).
|
||||
# Route 53 DNS-01 creds (or attach an instance role + omit). IAM policy in
|
||||
# DEPLOY.md §2c; AWS_HOSTED_ZONE_ID optional (pins the zone).
|
||||
AWS_ACCESS_KEY_ID: "${AWS_ACCESS_KEY_ID:-}"
|
||||
AWS_SECRET_ACCESS_KEY: "${AWS_SECRET_ACCESS_KEY:-}"
|
||||
AWS_REGION: "${AWS_REGION:-us-east-1}"
|
||||
# Optional: pin the zone (skips the ListHostedZonesByName lookup — needed if
|
||||
# a public + private zone share the domain; lets you tighten the IAM policy).
|
||||
AWS_HOSTED_ZONE_ID: "${AWS_HOSTED_ZONE_ID:-}"
|
||||
volumes:
|
||||
- ./acme/renew-loop.sh:/renew-loop.sh:ro
|
||||
- acme-state:/le # lego account + cert store — PERSIST
|
||||
- ./pki/admin:/out # shared with the manager's ./pki/admin
|
||||
networks:
|
||||
- fleet-mgmt
|
||||
- ./traefik/dynamic:/etc/traefik/dynamic:ro
|
||||
- traefik-acme:/acme # acme.json (ACME account + issued certs) — PERSIST
|
||||
depends_on:
|
||||
- fleet-manager
|
||||
|
||||
volumes:
|
||||
influx-data:
|
||||
influx-config:
|
||||
fleet-data:
|
||||
acme-state:
|
||||
traefik-acme:
|
||||
|
||||
networks:
|
||||
fleet-mgmt:
|
||||
|
||||
Reference in New Issue
Block a user