From 6fbff1e19dc9161307ac3eeb036f99d4ceb5e8f7 Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 10 Oct 2026 17:57:21 +0200 Subject: [PATCH 1/3] feat(traefik): add Selectel v2 DNS-01 challenge support lego's selectelv2 provider authenticates via Keystone/OpenStack and needs four credentials at once, so it can't fit the single-token x-dns-challenge-tokens fan-out pattern. Add SELECTELV2_USERNAME, SELECTELV2_PASSWORD, SELECTELV2_ACCOUNT_ID, and SELECTELV2_PROJECT_ID as explicit, optional x-vault-env entries instead. Fixes #220 Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 2 +- apps/traefik/docker-compose.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/AGENTS.md b/AGENTS.md index 6581b14..77ef867 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -202,7 +202,7 @@ Apps are accessible at `{app-name}.{DOMAIN}` with automatic SSL. `acmeresolver` `apps/traefik/docker-compose.yml` has no config template at all - Traefik's entire static configuration (entryPoints, the docker provider, logging, and the ACME resolver) is expressed directly as `TRAEFIK_*` environment variables, split across `x-vault-env`/`x-internal-env` like any other app. This isn't a style choice: Traefik's static config sources (file, env vars, CLI flags) are mutually exclusive, not merged - if a config file is present, environment variables for static config are silently ignored entirely, verified empirically while building this. Keeping traefik file-free sidesteps that trap. -`acmeresolver` only has `dnsChallenge` configured - a target's vault must provide `DNS_CHALLENGE_PROVIDER` (a lego provider name, e.g. `cloudflare`) and `DNS_CHALLENGE_TOKEN` (a single credential value). `httpChallenge` used to be configured unconditionally alongside it as a zero-config default, but Traefik picks http-01 whenever both challenge types are present on the same resolver, silently ignoring the configured DNS provider - that broke renewal outright on any target where the http-01 path isn't actually reachable (traffic to `/.well-known/acme-challenge/...` routed to an app container instead of Traefik's own ACME handler). See #202. `DNS_CHALLENGE_TOKEN` is mapped into every supported provider's own expected variable name at once, in its own `x-dns-challenge-tokens` anchor (separate from `x-vault-env` - it's not a direct 1:1 vault declaration, it's one vault value fanned out into several provider-specific slots): Cloudflare's `CF_DNS_API_TOKEN`, DigitalOcean's `DO_AUTH_TOKEN`, Hetzner's `HETZNER_API_TOKEN`, Linode's `LINODE_TOKEN`. lego only ever reads the one belonging to whichever provider was actually selected, so the others sit unused and harmless. This only works for providers needing a single token; add one by adding one more `x-dns-challenge-tokens` line mapping `${DNS_CHALLENGE_TOKEN:-}` into that provider's lego variable name (see [go-acme.github.io/lego/dns](https://go-acme.github.io/lego/dns/) for the full ~200-provider list and each one's required variable(s)). Providers needing more than one credential (Route53, GoDaddy, Namecheap, Selectel's current v2 API, and most others actually dominant by real market share - checked deliberately, single-token providers are the minority) don't fit this pattern and would need their own explicit, non-fanned-out vault variables instead - not yet added since nothing in the catalog needs one today. +`acmeresolver` only has `dnsChallenge` configured - a target's vault must provide `DNS_CHALLENGE_PROVIDER` (a lego provider name, e.g. `cloudflare`) and `DNS_CHALLENGE_TOKEN` (a single credential value). `httpChallenge` used to be configured unconditionally alongside it as a zero-config default, but Traefik picks http-01 whenever both challenge types are present on the same resolver, silently ignoring the configured DNS provider - that broke renewal outright on any target where the http-01 path isn't actually reachable (traffic to `/.well-known/acme-challenge/...` routed to an app container instead of Traefik's own ACME handler). See #202. `DNS_CHALLENGE_TOKEN` is mapped into every supported provider's own expected variable name at once, in its own `x-dns-challenge-tokens` anchor (separate from `x-vault-env` - it's not a direct 1:1 vault declaration, it's one vault value fanned out into several provider-specific slots): Cloudflare's `CF_DNS_API_TOKEN`, DigitalOcean's `DO_AUTH_TOKEN`, Hetzner's `HETZNER_API_TOKEN`, Linode's `LINODE_TOKEN`. lego only ever reads the one belonging to whichever provider was actually selected, so the others sit unused and harmless. This only works for providers needing a single token; add one by adding one more `x-dns-challenge-tokens` line mapping `${DNS_CHALLENGE_TOKEN:-}` into that provider's lego variable name (see [go-acme.github.io/lego/dns](https://go-acme.github.io/lego/dns/) for the full ~200-provider list and each one's required variable(s)). Providers needing more than one credential (Route53, GoDaddy, Namecheap, and most others actually dominant by real market share - checked deliberately, single-token providers are the minority) don't fit this pattern and would need their own explicit, non-fanned-out vault variables instead. Selectel's current v2 API is the first of these actually added: lego's `selectelv2` provider authenticates via Keystone/OpenStack, needing four credentials at once, so `apps/traefik/docker-compose.yml`'s `x-vault-env` declares `SELECTELV2_USERNAME`, `SELECTELV2_PASSWORD`, `SELECTELV2_ACCOUNT_ID`, `SELECTELV2_PROJECT_ID` directly (each optional, defaulting to empty like the fan-out entries do) rather than going through `x-dns-challenge-tokens`. lego's older `selectel` provider is a different, legacy Selectel DNS API that takes a single `SELECTEL_API_TOKEN` and *would* fit the fan-out pattern - it's not what `selectelv2` needs. `apps/cloudflared/` is a separate, optional catalog app - not part of `apps/traefik/` itself - for targets that want to front Traefik with a Cloudflare Tunnel instead of (or alongside) exposing `HTTP_PORT`/`HTTPS_PORT` directly. It's a single `cloudflared` service joined to the external `traefik` network only (no `internal` network, no traefik labels - it isn't routed *to*, it originates the connection), running `cloudflared tunnel --no-autoupdate run` with the token-based (dashboard-managed) tunnel mode: `TUNNEL_TOKEN` in the environment is all `cloudflared` needs, no local tunnel config file. Connection direction is `cloudflared` → `traefik:80` over the docker network (the tunnel's ingress rule, set in the Cloudflare Zero Trust dashboard, points at `http://traefik:80`), and Traefik then routes onward to backend services exactly as it already does for direct traffic - no change to how apps register with Traefik. Because `cloudflared` reaches Traefik over the `traefik` docker network, its connecting IP already falls inside the existing `TRAEFIK_ENTRYPOINTS_HTTPS_FORWARDEDHEADERS_TRUSTEDIPS` range (`172.16.0.0/12,10.0.0.0/8`) - no trustedIPs change needed for this topology. Deploying `cloudflared` doesn't remove Traefik's own published ports; the two paths coexist unless a target deliberately stops publishing `HTTP_PORT`/`HTTPS_PORT`. diff --git a/apps/traefik/docker-compose.yml b/apps/traefik/docker-compose.yml index 9c6a40f..8800b12 100644 --- a/apps/traefik/docker-compose.yml +++ b/apps/traefik/docker-compose.yml @@ -1,6 +1,10 @@ x-vault-env: &vault-env TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_EMAIL: ${ADMIN_MAIL} TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_DNSCHALLENGE_PROVIDER: ${DNS_CHALLENGE_PROVIDER:-} + SELECTELV2_USERNAME: ${SELECTELV2_USERNAME:-} + SELECTELV2_PASSWORD: ${SELECTELV2_PASSWORD:-} + SELECTELV2_ACCOUNT_ID: ${SELECTELV2_ACCOUNT_ID:-} + SELECTELV2_PROJECT_ID: ${SELECTELV2_PROJECT_ID:-} x-dns-challenge-tokens: &dns-challenge-tokens CF_DNS_API_TOKEN: ${DNS_CHALLENGE_TOKEN:-} DO_AUTH_TOKEN: ${DNS_CHALLENGE_TOKEN:-} From 8940b4177216a6ecb26ccda23051c2b0a1a805e9 Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 10 Oct 2026 18:09:17 +0200 Subject: [PATCH 2/3] refactor(traefik): name every DNS challenge provider's credentials explicitly Drop the shared DNS_CHALLENGE_TOKEN fan-out for single-token providers (CF_DNS_API_TOKEN, DO_AUTH_TOKEN, HETZNER_API_TOKEN, LINODE_TOKEN) so every provider follows the same explicit, non-fanned-out pattern as Selectel v2 - one vault variable per lego credential, regardless of how many a provider needs. Update vaults/traefik.yml's live mapping to match. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 6 ++++-- apps/traefik/docker-compose.yml | 11 +++++------ vaults/traefik.yml | 2 +- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 77ef867..480d8cb 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -154,7 +154,7 @@ Name a variable by what it *is*, never by its format (`SESSION_KEY`, not `KEY_HE Common shared variables a target's vaults map into one or more apps' `.env`: - `DOMAIN` - Base domain for all services -- `DNS_CHALLENGE_PROVIDER`, `DNS_CHALLENGE_TOKEN` - Optional DNS-01 ACME challenge, traefik-only (see "Traefik Integration" below) +- `DNS_CHALLENGE_PROVIDER` - Optional DNS-01 ACME challenge provider selector, traefik-only; the selected provider's own credential variable(s) are documented in "Traefik Integration" below - `DATABASE_PASSWORD` - Shared database password - `SESSION_KEY` - Shared session-signing key (safe to share - see above) - `TIMEZONE` - System timezone @@ -202,7 +202,9 @@ Apps are accessible at `{app-name}.{DOMAIN}` with automatic SSL. `acmeresolver` `apps/traefik/docker-compose.yml` has no config template at all - Traefik's entire static configuration (entryPoints, the docker provider, logging, and the ACME resolver) is expressed directly as `TRAEFIK_*` environment variables, split across `x-vault-env`/`x-internal-env` like any other app. This isn't a style choice: Traefik's static config sources (file, env vars, CLI flags) are mutually exclusive, not merged - if a config file is present, environment variables for static config are silently ignored entirely, verified empirically while building this. Keeping traefik file-free sidesteps that trap. -`acmeresolver` only has `dnsChallenge` configured - a target's vault must provide `DNS_CHALLENGE_PROVIDER` (a lego provider name, e.g. `cloudflare`) and `DNS_CHALLENGE_TOKEN` (a single credential value). `httpChallenge` used to be configured unconditionally alongside it as a zero-config default, but Traefik picks http-01 whenever both challenge types are present on the same resolver, silently ignoring the configured DNS provider - that broke renewal outright on any target where the http-01 path isn't actually reachable (traffic to `/.well-known/acme-challenge/...` routed to an app container instead of Traefik's own ACME handler). See #202. `DNS_CHALLENGE_TOKEN` is mapped into every supported provider's own expected variable name at once, in its own `x-dns-challenge-tokens` anchor (separate from `x-vault-env` - it's not a direct 1:1 vault declaration, it's one vault value fanned out into several provider-specific slots): Cloudflare's `CF_DNS_API_TOKEN`, DigitalOcean's `DO_AUTH_TOKEN`, Hetzner's `HETZNER_API_TOKEN`, Linode's `LINODE_TOKEN`. lego only ever reads the one belonging to whichever provider was actually selected, so the others sit unused and harmless. This only works for providers needing a single token; add one by adding one more `x-dns-challenge-tokens` line mapping `${DNS_CHALLENGE_TOKEN:-}` into that provider's lego variable name (see [go-acme.github.io/lego/dns](https://go-acme.github.io/lego/dns/) for the full ~200-provider list and each one's required variable(s)). Providers needing more than one credential (Route53, GoDaddy, Namecheap, and most others actually dominant by real market share - checked deliberately, single-token providers are the minority) don't fit this pattern and would need their own explicit, non-fanned-out vault variables instead. Selectel's current v2 API is the first of these actually added: lego's `selectelv2` provider authenticates via Keystone/OpenStack, needing four credentials at once, so `apps/traefik/docker-compose.yml`'s `x-vault-env` declares `SELECTELV2_USERNAME`, `SELECTELV2_PASSWORD`, `SELECTELV2_ACCOUNT_ID`, `SELECTELV2_PROJECT_ID` directly (each optional, defaulting to empty like the fan-out entries do) rather than going through `x-dns-challenge-tokens`. lego's older `selectel` provider is a different, legacy Selectel DNS API that takes a single `SELECTEL_API_TOKEN` and *would* fit the fan-out pattern - it's not what `selectelv2` needs. +`acmeresolver` only has `dnsChallenge` configured - a target's vault must provide `DNS_CHALLENGE_PROVIDER` (a lego provider name, e.g. `cloudflare`) and that provider's own credential variable(s). `httpChallenge` used to be configured unconditionally alongside it as a zero-config default, but Traefik picks http-01 whenever both challenge types are present on the same resolver, silently ignoring the configured DNS provider - that broke renewal outright on any target where the http-01 path isn't actually reachable (traffic to `/.well-known/acme-challenge/...` routed to an app container instead of Traefik's own ACME handler). See #202. + +Every provider's credential variable(s) are declared directly in `apps/traefik/docker-compose.yml`'s `x-vault-env`, named exactly after lego's own expected env var name (see [go-acme.github.io/lego/dns](https://go-acme.github.io/lego/dns/) for the full ~200-provider list and each one's required variable(s)), each optional and defaulting to empty (`${VAR:-}`): Cloudflare's `CF_DNS_API_TOKEN`, DigitalOcean's `DO_AUTH_TOKEN`, Hetzner's `HETZNER_API_TOKEN`, Linode's `LINODE_TOKEN`, and Selectel v2's `SELECTELV2_USERNAME`/`SELECTELV2_PASSWORD`/`SELECTELV2_ACCOUNT_ID`/`SELECTELV2_PROJECT_ID` (lego's `selectelv2` provider authenticates via Keystone/OpenStack, needing all four at once; lego's older `selectel` provider is a different, legacy Selectel DNS API taking a single `SELECTEL_API_TOKEN` instead). lego only ever reads the credential(s) belonging to whichever provider was actually selected, so the rest sit unused and harmless. A single-token provider used to share one generic `DNS_CHALLENGE_TOKEN` vault variable fanned out into each provider's slot, but that made single-token and multi-credential providers (like Selectel v2) inconsistent with each other; naming every provider's variable(s) explicitly, with no fan-out, keeps the pattern uniform regardless of how many credentials a provider needs. Add a new provider by adding its lego variable name(s) as their own `${VAR:-}` entries in `x-vault-env`; a target's own vault then maps whichever of those it actually uses to its real GitHub Secret (see `vaults/traefik.yml`'s `CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN}` for an example). `apps/cloudflared/` is a separate, optional catalog app - not part of `apps/traefik/` itself - for targets that want to front Traefik with a Cloudflare Tunnel instead of (or alongside) exposing `HTTP_PORT`/`HTTPS_PORT` directly. It's a single `cloudflared` service joined to the external `traefik` network only (no `internal` network, no traefik labels - it isn't routed *to*, it originates the connection), running `cloudflared tunnel --no-autoupdate run` with the token-based (dashboard-managed) tunnel mode: `TUNNEL_TOKEN` in the environment is all `cloudflared` needs, no local tunnel config file. Connection direction is `cloudflared` → `traefik:80` over the docker network (the tunnel's ingress rule, set in the Cloudflare Zero Trust dashboard, points at `http://traefik:80`), and Traefik then routes onward to backend services exactly as it already does for direct traffic - no change to how apps register with Traefik. Because `cloudflared` reaches Traefik over the `traefik` docker network, its connecting IP already falls inside the existing `TRAEFIK_ENTRYPOINTS_HTTPS_FORWARDEDHEADERS_TRUSTEDIPS` range (`172.16.0.0/12,10.0.0.0/8`) - no trustedIPs change needed for this topology. Deploying `cloudflared` doesn't remove Traefik's own published ports; the two paths coexist unless a target deliberately stops publishing `HTTP_PORT`/`HTTPS_PORT`. diff --git a/apps/traefik/docker-compose.yml b/apps/traefik/docker-compose.yml index 8800b12..3f3606b 100644 --- a/apps/traefik/docker-compose.yml +++ b/apps/traefik/docker-compose.yml @@ -1,15 +1,14 @@ x-vault-env: &vault-env TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_EMAIL: ${ADMIN_MAIL} TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_DNSCHALLENGE_PROVIDER: ${DNS_CHALLENGE_PROVIDER:-} + CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:-} + DO_AUTH_TOKEN: ${DO_AUTH_TOKEN:-} + HETZNER_API_TOKEN: ${HETZNER_API_TOKEN:-} + LINODE_TOKEN: ${LINODE_TOKEN:-} SELECTELV2_USERNAME: ${SELECTELV2_USERNAME:-} SELECTELV2_PASSWORD: ${SELECTELV2_PASSWORD:-} SELECTELV2_ACCOUNT_ID: ${SELECTELV2_ACCOUNT_ID:-} SELECTELV2_PROJECT_ID: ${SELECTELV2_PROJECT_ID:-} -x-dns-challenge-tokens: &dns-challenge-tokens - CF_DNS_API_TOKEN: ${DNS_CHALLENGE_TOKEN:-} - DO_AUTH_TOKEN: ${DNS_CHALLENGE_TOKEN:-} - HETZNER_API_TOKEN: ${DNS_CHALLENGE_TOKEN:-} - LINODE_TOKEN: ${DNS_CHALLENGE_TOKEN:-} x-internal-env: &internal-env TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_STORAGE: /letsencrypt/acme.json TRAEFIK_GLOBAL_CHECKNEWVERSION: false @@ -28,7 +27,7 @@ services: traefik: image: traefik environment: - <<: [*vault-env, *dns-challenge-tokens, *internal-env] + <<: [*vault-env, *internal-env] ports: - "${HTTP_PORT}:80" - "${HTTPS_PORT}:443" diff --git a/vaults/traefik.yml b/vaults/traefik.yml index 1d7e147..8e98061 100644 --- a/vaults/traefik.yml +++ b/vaults/traefik.yml @@ -4,6 +4,6 @@ keys: env: ADMIN_MAIL: ${ADMIN_MAIL} DNS_CHALLENGE_PROVIDER: ${DNS_CHALLENGE_PROVIDER} - DNS_CHALLENGE_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN} + CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN} HTTP_PORT: ${TRAEFIK_HTTP_PORT} HTTPS_PORT: ${TRAEFIK_HTTPS_PORT} From ebc24924ba9322dda058c5b9b2301ad2b78e269e Mon Sep 17 00:00:00 2001 From: Yuriy Kirillov Date: Sat, 10 Oct 2026 18:11:51 +0200 Subject: [PATCH 3/3] refactor(traefik): spell out Cloudflare/DigitalOcean in app-facing var names CF_DNS_API_TOKEN and DO_AUTH_TOKEN are lego's own forced container-side env var names (CF_/DO_ prefixes are hardcoded in lego, not our choice), but the app-facing ${VAR:-} name a target's vault maps into doesn't have to mirror that abbreviation. Point both at fully-spelled-out names instead: ${CLOUDFLARE_DNS_API_TOKEN:-} and ${DIGITALOCEAN_AUTH_TOKEN:-}. Hetzner/Linode/Selectel v2's forced names were already unabbreviated, so they're unchanged. Update vaults/traefik.yml's live mapping to match. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 4 +++- apps/traefik/docker-compose.yml | 4 ++-- vaults/traefik.yml | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 480d8cb..06e417d 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -204,7 +204,9 @@ Apps are accessible at `{app-name}.{DOMAIN}` with automatic SSL. `acmeresolver` `acmeresolver` only has `dnsChallenge` configured - a target's vault must provide `DNS_CHALLENGE_PROVIDER` (a lego provider name, e.g. `cloudflare`) and that provider's own credential variable(s). `httpChallenge` used to be configured unconditionally alongside it as a zero-config default, but Traefik picks http-01 whenever both challenge types are present on the same resolver, silently ignoring the configured DNS provider - that broke renewal outright on any target where the http-01 path isn't actually reachable (traffic to `/.well-known/acme-challenge/...` routed to an app container instead of Traefik's own ACME handler). See #202. -Every provider's credential variable(s) are declared directly in `apps/traefik/docker-compose.yml`'s `x-vault-env`, named exactly after lego's own expected env var name (see [go-acme.github.io/lego/dns](https://go-acme.github.io/lego/dns/) for the full ~200-provider list and each one's required variable(s)), each optional and defaulting to empty (`${VAR:-}`): Cloudflare's `CF_DNS_API_TOKEN`, DigitalOcean's `DO_AUTH_TOKEN`, Hetzner's `HETZNER_API_TOKEN`, Linode's `LINODE_TOKEN`, and Selectel v2's `SELECTELV2_USERNAME`/`SELECTELV2_PASSWORD`/`SELECTELV2_ACCOUNT_ID`/`SELECTELV2_PROJECT_ID` (lego's `selectelv2` provider authenticates via Keystone/OpenStack, needing all four at once; lego's older `selectel` provider is a different, legacy Selectel DNS API taking a single `SELECTEL_API_TOKEN` instead). lego only ever reads the credential(s) belonging to whichever provider was actually selected, so the rest sit unused and harmless. A single-token provider used to share one generic `DNS_CHALLENGE_TOKEN` vault variable fanned out into each provider's slot, but that made single-token and multi-credential providers (like Selectel v2) inconsistent with each other; naming every provider's variable(s) explicitly, with no fan-out, keeps the pattern uniform regardless of how many credentials a provider needs. Add a new provider by adding its lego variable name(s) as their own `${VAR:-}` entries in `x-vault-env`; a target's own vault then maps whichever of those it actually uses to its real GitHub Secret (see `vaults/traefik.yml`'s `CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN}` for an example). +Every provider's credential variable(s) are declared directly in `apps/traefik/docker-compose.yml`'s `x-vault-env`, each optional and defaulting to empty (`${VAR:-}`): Cloudflare's `CF_DNS_API_TOKEN`, DigitalOcean's `DO_AUTH_TOKEN`, Hetzner's `HETZNER_API_TOKEN`, Linode's `LINODE_TOKEN`, and Selectel v2's `SELECTELV2_USERNAME`/`SELECTELV2_PASSWORD`/`SELECTELV2_ACCOUNT_ID`/`SELECTELV2_PROJECT_ID` (lego's `selectelv2` provider authenticates via Keystone/OpenStack, needing all four at once; lego's older `selectel` provider is a different, legacy Selectel DNS API taking a single `SELECTEL_API_TOKEN` instead). lego only ever reads the credential(s) belonging to whichever provider was actually selected, so the rest sit unused and harmless. A single-token provider used to share one generic `DNS_CHALLENGE_TOKEN` vault variable fanned out into each provider's slot, but that made single-token and multi-credential providers (like Selectel v2) inconsistent with each other; naming every provider's variable(s) explicitly, with no fan-out, keeps the pattern uniform regardless of how many credentials a provider needs. Add a new provider by adding its lego variable name(s) (see [go-acme.github.io/lego/dns](https://go-acme.github.io/lego/dns/) for the full ~200-provider list and each one's required variable(s)) as their own entries in `x-vault-env`. + +The map *key* in `x-vault-env` (`CF_DNS_API_TOKEN`, `DO_AUTH_TOKEN`, ...) is the literal env var name the traefik container needs - lego reads it verbatim via `os.Getenv`, so it's non-negotiable, abbreviated prefix (`CF_`, `DO_`) and all. The `${VAR:-}` on the right is a different name: the app-facing variable a target's vault actually declares, free to spell the provider out in full since nothing reads it but `docker compose`'s own substitution - `CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN:-}`, not `${CF_DNS_API_TOKEN:-}`. Hetzner and Linode's forced key names already spell the provider in full, so their `${VAR:-}` reference matches the key as-is; only Cloudflare and DigitalOcean's lego-forced keys are actually abbreviated, so only those two need the two names to diverge. A target's own vault then maps that full app-facing name to its real GitHub Secret (see `vaults/traefik.yml`'s `CLOUDFLARE_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN}`). `apps/cloudflared/` is a separate, optional catalog app - not part of `apps/traefik/` itself - for targets that want to front Traefik with a Cloudflare Tunnel instead of (or alongside) exposing `HTTP_PORT`/`HTTPS_PORT` directly. It's a single `cloudflared` service joined to the external `traefik` network only (no `internal` network, no traefik labels - it isn't routed *to*, it originates the connection), running `cloudflared tunnel --no-autoupdate run` with the token-based (dashboard-managed) tunnel mode: `TUNNEL_TOKEN` in the environment is all `cloudflared` needs, no local tunnel config file. Connection direction is `cloudflared` → `traefik:80` over the docker network (the tunnel's ingress rule, set in the Cloudflare Zero Trust dashboard, points at `http://traefik:80`), and Traefik then routes onward to backend services exactly as it already does for direct traffic - no change to how apps register with Traefik. Because `cloudflared` reaches Traefik over the `traefik` docker network, its connecting IP already falls inside the existing `TRAEFIK_ENTRYPOINTS_HTTPS_FORWARDEDHEADERS_TRUSTEDIPS` range (`172.16.0.0/12,10.0.0.0/8`) - no trustedIPs change needed for this topology. Deploying `cloudflared` doesn't remove Traefik's own published ports; the two paths coexist unless a target deliberately stops publishing `HTTP_PORT`/`HTTPS_PORT`. diff --git a/apps/traefik/docker-compose.yml b/apps/traefik/docker-compose.yml index 3f3606b..12fda5f 100644 --- a/apps/traefik/docker-compose.yml +++ b/apps/traefik/docker-compose.yml @@ -1,8 +1,8 @@ x-vault-env: &vault-env TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_EMAIL: ${ADMIN_MAIL} TRAEFIK_CERTIFICATESRESOLVERS_ACMERESOLVER_ACME_DNSCHALLENGE_PROVIDER: ${DNS_CHALLENGE_PROVIDER:-} - CF_DNS_API_TOKEN: ${CF_DNS_API_TOKEN:-} - DO_AUTH_TOKEN: ${DO_AUTH_TOKEN:-} + CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN:-} + DO_AUTH_TOKEN: ${DIGITALOCEAN_AUTH_TOKEN:-} HETZNER_API_TOKEN: ${HETZNER_API_TOKEN:-} LINODE_TOKEN: ${LINODE_TOKEN:-} SELECTELV2_USERNAME: ${SELECTELV2_USERNAME:-} diff --git a/vaults/traefik.yml b/vaults/traefik.yml index 8e98061..93ef8c9 100644 --- a/vaults/traefik.yml +++ b/vaults/traefik.yml @@ -4,6 +4,6 @@ keys: env: ADMIN_MAIL: ${ADMIN_MAIL} DNS_CHALLENGE_PROVIDER: ${DNS_CHALLENGE_PROVIDER} - CF_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN} + CLOUDFLARE_DNS_API_TOKEN: ${CLOUDFLARE_DNS_API_TOKEN} HTTP_PORT: ${TRAEFIK_HTTP_PORT} HTTPS_PORT: ${TRAEFIK_HTTPS_PORT}