From ee4e8804c5d12de92b6f095340a7ca74b0c2835e Mon Sep 17 00:00:00 2001 From: mifi Date: Thu, 16 Jul 2026 20:02:07 -0300 Subject: [PATCH] Initial Traefik compose stack commit --- .gitignore | 47 +++++++ README.md | 217 ++++++++++++++++++++++++++++++++ compose.yml | 62 +++++++++ example.env | 7 ++ traefik/conf.d/middlewares.yaml | 189 ++++++++++++++++++++++++++++ traefik/conf.d/routers.yaml | 19 +++ traefik/conf.d/services.yaml | 7 ++ traefik/traefik.yaml | 74 +++++++++++ 8 files changed, 622 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 compose.yml create mode 100644 example.env create mode 100644 traefik/conf.d/middlewares.yaml create mode 100644 traefik/conf.d/routers.yaml create mode 100644 traefik/conf.d/services.yaml create mode 100644 traefik/traefik.yaml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..91464e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,47 @@ +### macOS +# Finder metadata +.DS_Store + +# Thumbnails +._* + +# Custom folder icons +Icon + +# Volume root files +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +### Windows +# Windows thumbnail cache files +Thumbs.db + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows shortcuts +*.lnk + +### Linux +# Backup files +*~ + +# Temporary files from deleted open files +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder +.Trash-* + +# NFS temporary files +.nfs* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..a134856 --- /dev/null +++ b/README.md @@ -0,0 +1,217 @@ +# Traefik reverse proxy stack + +Docker Compose stack for [Traefik](https://traefik.io/) with Let’s Encrypt (Cloudflare DNS challenge), a secured dashboard, and a reusable middleware library for security headers, CSP tiers, Authentik SSO, redirects, and IP allowlists. + +## Prerequisites + +- Docker Engine with Compose v2 +- A Cloudflare account with API access for the domains you terminate here +- DNS for those domains pointed at the host that runs this stack +- A shared Docker network that your backend services also join (default: `docknet`) + +## Quick start + +```bash +git clone traefik && cd traefik +cp example.env .env +``` + +Edit `.env` (see [Environment variables](#environment-variables)), then: + +```bash +docker compose --env-file .env up -d +``` + +Traefik will join (or create) the Docker network named by `NETWORK_NAME`, watch containers on that host for Traefik labels, and issue certificates via Cloudflare DNS. ACME data is stored in the Docker named volume `traefik-acme` (created automatically; no manual file setup). + +## Layout + +``` +. +├── compose.yml # Traefik service, ports, dashboard labels, ACME volume +├── example.env # Env template (copy to .env) +└── traefik/ + ├── traefik.yaml # Static config: entrypoints, providers, TLS SANs + └── conf.d/ + ├── middlewares.yaml # Shared middleware library + ├── routers.yaml # Optional file-based routers + └── services.yaml # Optional file-based services +``` + +Static config lives in `traefik/traefik.yaml`. Dynamic config (middlewares, optional routers/services) is loaded from `traefik/conf.d/` and hot-reloads when files change. Certificate state lives in the `traefik-acme` volume, not in the repo. + +## Environment variables + +Copy `example.env` to `.env` and fill in real values: + +| Variable | Purpose | +| --- | --- | +| `CLOUDFLARE_EMAIL` | Cloudflare account email (ACME + API) | +| `CLOUDFLARE_API_KEY` | Cloudflare Global API Key (or scoped token with DNS edit) | +| `CONSOLE_HOST` | Hostname for the Traefik dashboard (e.g. `traefik.example.com`) | +| `NETWORK_NAME` | Docker network name shared with backends (default `docknet`) | +| `TRAEFIK_CONSOLE_AUTH` | Basic-auth users for the dashboard (`htpasswd` format) | +| `TRAEFIK_YAML` | Host path to static config (default `./traefik/traefik.yaml`) | +| `TRAEFIK_CONFD` | Host path to dynamic config dir (default `./traefik/conf.d`) | + +Generate dashboard credentials: + +```bash +# Install apache2-utils / httpd-tools if needed, then: +htpasswd -nbB admin 'your-strong-password' +``` + +Paste the output into `TRAEFIK_CONSOLE_AUTH`. If the value contains `$`, escape them for Compose (use `$$`) or quote carefully in `.env`. + +## Ports + +| Host | Container | Role | +| --- | --- | --- | +| `8342` | `80` | HTTP → redirects to HTTPS | +| `8767` | `443` | HTTPS | +| `8080` | `8080` | Traefik API / dashboard (also exposed via `CONSOLE_HOST` with TLS + basic auth) | + +Point your public reverse path or firewall at `8767` (and `8342` if you want HTTP→HTTPS at the edge). Adjust the left-hand ports in `compose.yml` if you prefer `80`/`443` on the host. + +## TLS and certificates + +- Challenge: **DNS-01** via Cloudflare (`certificatesResolvers.letsencrypt`) +- Storage: Docker named volume `traefik-acme` mounted at `/letsencrypt` (Traefik creates `acme.json` on first run) +- Default certificate SANs for the TLS store are defined under `tls.stores.default` in `traefik/traefik.yaml` — edit that list for your domains + +The volume survives `docker compose down` / `up`. Prefer `docker compose down` over `docker compose down -v` unless you intentionally want to wipe certs (which will re-hit Let’s Encrypt rate limits). + +Ensure Cloudflare credentials can create/delete `_acme-challenge` TXT records for every zone you serve. + +## Dashboard + +The dashboard is enabled and routed with Docker labels on the Traefik container: + +- URL: `https:///` +- Entrypoint: `websecure` +- Auth: middleware `traefik-auth` (basic auth from `TRAEFIK_CONSOLE_AUTH`) +- Certificate: Let’s Encrypt via `letsencrypt` resolver + +## Using middlewares on your services + +Connect apps to the same Docker network (`NETWORK_NAME`), enable Traefik, and attach middlewares by name with the `@file` provider: + +```yaml +services: + myapp: + image: myapp:latest + networks: + - docknet + labels: + - traefik.enable=true + - traefik.http.routers.myapp.rule=Host(`app.example.com`) + - traefik.http.routers.myapp.entrypoints=websecure + - traefik.http.routers.myapp.tls.certresolver=letsencrypt + - traefik.http.routers.myapp.middlewares=security-supermax@file,gzip@file + - traefik.http.services.myapp.loadbalancer.server.port=8080 + +networks: + docknet: + external: true + name: docknet +``` + +Chain multiple middlewares with commas. File-provider names resolve as `@file`. + +## Middleware catalog + +Defined in `traefik/conf.d/middlewares.yaml`. + +### Security chains (pick one) + +| Middleware | Intent | +| --- | --- | +| `default` | CSP strict + frame deny + gzip + HSTS + baseline security headers | +| `secure-all` | Permissive CSP + same-origin frames + HSTS + security headers | +| `security-mallcop` | Open referrer + open CSP + HSTS + security headers | +| `security-lax` | Lax referrer + lax CSP + HSTS + security headers | +| `security-medium` | Permissive referrer/CSP + same-origin frames + HSTS + security headers | +| `security-prison` | Medium referrer + balanced CSP + same-origin frames + HSTS + security headers | +| `security-supermax` | No-referrer + strict CSP + frame deny + HSTS + security headers | +| `security-supermax-with-analytics` | Supermax plus GA/Clarity/Umami-friendly CSP and COOP | + +### CSP building blocks + +| Middleware | Notes | +| --- | --- | +| `csp-open-header` | Very permissive (legacy / hard-to-lock apps) | +| `csp-lax-header` | Self + common inline/eval allowances | +| `csp-permissive-header` | Self-focused with inline/eval for scripts/styles | +| `csp-balanced-header` | Self + common CDNs (jsDelivr, cdnjs, unpkg, Google Fonts) | +| `csp-strict-header` | Self-only | +| `csp-strict-header-with-ga` | Strict plus analytics/Clarity/GTM endpoints | + +### Other headers & utilities + +| Middleware | Role | +| --- | --- | +| `security-headers` | `X-Content-Type-Options: nosniff`, disables legacy `X-XSS-Protection` | +| `hsts-header` | HSTS 2y, includeSubDomains, preload | +| `frame-deny-header` | `X-Frame-Options: DENY` | +| `frame-sameorigin-header` | `X-Frame-Options: SAMEORIGIN` | +| `cross-origin-opener-strict-policy` | `Cross-Origin-Opener-Policy: same-origin` | +| `gzip` | Response compression | +| `referrer-*-header` | Referrer-Policy tiers (`open` → `supermax`) | +| `strip-server-path` | Strips `/db`, `/server`, `/traefik` prefixes | + +### Auth & access + +| Middleware | Role | +| --- | --- | +| `authentik-outpost` | ForwardAuth to Authentik outpost (`outpost.mifi.holdings`) | +| `authentik-no-cache` | Disable caching on Authentik responses | +| `authentik-static-immutable` | Long-cache immutable static assets | +| `internal-ips` | IP allowlist (edit `sourceRange` for your networks) | + +### Redirects + +| Middleware | Role | +| --- | --- | +| `redirect-www-to-non-www` | `www.` → apex | +| `redirect-non-www-to-www` | apex → `www.` | +| `redirect-postfixadmin` | Mail settings path redirect (site-specific) | + +Update Authentik outpost URL, IP allowlists, and domain-specific redirects in `middlewares.yaml` before deploying outside this environment. + +## File-based routers and services + +`traefik/conf.d/routers.yaml` and `services.yaml` are stubs for optional non-Docker backends. Uncomment or add entries when you need host/path rules that are not expressed as container labels. + +## Operations + +```bash +# Start / recreate after config or .env changes +docker compose --env-file .env up -d + +# Follow logs +docker compose logs -f traefik + +# Stop (keeps the ACME volume) +docker compose down + +# Inspect ACME volume location (optional) +docker volume inspect traefik_traefik-acme +``` + +Dynamic files under `conf.d/` reload without a full restart. Changes to `traefik.yaml`, Compose labels, ports, or env vars generally need `docker compose up -d` (or a container recreate). + +## Customization checklist + +1. Set `.env` from `example.env` and generate `TRAEFIK_CONSOLE_AUTH` +2. Edit TLS SANs in `traefik/traefik.yaml` for your domains +3. Adjust `internal-ips` and Authentik URLs in `middlewares.yaml` +4. Ensure backends use `traefik.enable=true` and join `NETWORK_NAME` +5. Publish host ports (`8342`/`8767`) as needed for your network edge + +## Security notes + +- Do not commit `.env` or real API keys +- Prefer a scoped Cloudflare API token limited to DNS edit on required zones when possible +- Keep the dashboard behind strong basic auth (and consider further restricting with `internal-ips` or Authentik) +- Review CSP chains per app; start stricter and loosen only where breakage requires it +- Do not use `docker compose down -v` in production unless you mean to discard certificates diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..05a3fe3 --- /dev/null +++ b/compose.yml @@ -0,0 +1,62 @@ +services: + traefik: + image: traefik:latest + container_name: traefik + restart: unless-stopped + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - ${TRAEFIK_YAML:-./traefik/traefik.yaml}:/etc/traefik/traefik.yaml + - ${TRAEFIK_CONFD:-./traefik/conf.d}:/etc/traefik/conf.d + - traefik-acme:/letsencrypt + - /var/run/docker.sock:/var/run/docker.sock + networks: + - docknet + labels: + - 'traefik.enable=true' + - 'traefik.http.routers.api.rule=Host(`${CONSOLE_HOST}`)' + - 'traefik.http.routers.api.entrypoints=websecure' + - 'traefik.http.routers.api.service=api@internal' + - 'traefik.http.routers.api.tls=true' + - 'traefik.http.routers.api.tls.certresolver=letsencrypt' + - 'traefik.http.routers.api.middlewares=traefik-auth' + - 'traefik.http.middlewares.traefik-auth.basicauth.users=${TRAEFIK_CONSOLE_AUTH}' + ports: + - 8342:80 + - 8767:443 + - 8080:8080 + environment: + - CLOUDFLARE_EMAIL=${CLOUDFLARE_EMAIL} + - CLOUDFLARE_API_KEY=${CLOUDFLARE_API_KEY} + command: + - --api + # Enable the Trafik dashboard + - --api.dashboard=true + # Tell Traefik to discover containers using the Docker API + - --providers.docker=true + - --providers.docker.exposedByDefault=false + # Set up LetsEncrypt + - --certificatesresolvers.letsencrypt.acme.dnschallenge=true + - --certificatesresolvers.letsencrypt.acme.dnschallenge.provider=cloudflare + - --certificatesresolvers.letsencrypt.acme.email=${CLOUDFLARE_EMAIL} + # Set up an insecure listener that redirects all traffic to TLS + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entrypoint.to=websecure + - --entrypoints.web.http.redirections.entrypoint.scheme=https + - --entrypoints.websecure.address=:443 + # Set up the TLS configuration for our websecure listener + - --entrypoints.websecure.http.tls=true + - --entrypoints.websecure.http.tls.certResolver=letsencrypt + - --log=true + - --log.level=INFO + logging: + driver: "json-file" + options: + max-size: "1m" + +networks: + docknet: + name: ${NETWORK_NAME:-docknet} + +volumes: + traefik-acme: \ No newline at end of file diff --git a/example.env b/example.env new file mode 100644 index 0000000..7808c16 --- /dev/null +++ b/example.env @@ -0,0 +1,7 @@ +CLOUDFLARE_EMAIL=[EMAIL] +CLOUDFLARE_API_KEY=[API_KEY] +CONSOLE_HOST=traefik.mifi.dev +NETWORK_NAME=docknet +TRAEFIK_CONSOLE_AUTH=[username]:[password] +TRAEFIK_YAML=./traefik/traefik.yaml +TRAEFIK_CONFD=./traefik/conf.d diff --git a/traefik/conf.d/middlewares.yaml b/traefik/conf.d/middlewares.yaml new file mode 100644 index 0000000..40a6c15 --- /dev/null +++ b/traefik/conf.d/middlewares.yaml @@ -0,0 +1,189 @@ +http: + middlewares: + default: + chain: + middlewares: + - csp-strict-header + - frame-deny-header + - gzip + - hsts-header + - security-headers + # middlewares + authentik-no-cache: + headers: + customResponseHeaders: + Cache-Control: "no-store, no-cache, must-revalidate, max-age=0" + Pragma: "no-cache" + Expires: "0" + authentik-outpost: + forwardAuth: + address: "https://outpost.mifi.holdings/outpost.goauthentik.io/auth/traefik" + trustForwardHeader: true + authResponseHeaders: + - X-authentik-username + - X-authentik-groups + - X-authentik-entitlements + - X-authentik-email + - X-authentik-name + - X-authentik-uid + - X-authentik-jwt + - X-authentik-meta-jwks + - X-authentik-meta-outpost + - X-authentik-meta-provider + - X-authentik-meta-app + - X-authentik-meta-version + authentik-static-immutable: + headers: + customResponseHeaders: + Cache-Control: "public, max-age=31536000, immutable" + cross-origin-opener-strict-policy: + headers: + customResponseHeaders: + Cross-Origin-Opener-Policy: "same-origin" + csp-open-header: + headers: + customResponseHeaders: + Content-Security-Policy: "default-src * data: blob: 'unsafe-inline' 'unsafe-eval'; script-src * data: blob: 'unsafe-inline' 'unsafe-eval'; style-src * data: blob: 'unsafe-inline'; img-src * data: blob:; font-src * data: blob:; frame-src *; frame-ancestors *; base-uri *; form-action *; frame-src *; connect-src 'self'; worker-src 'self' blob:;" + csp-lax-header: + headers: + customResponseHeaders: + Content-Security-Policy: "default-src 'self'; img-src 'self' data: https:; script-src 'self' 'unsafe-eval' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; font-src 'self' data: https:; frame-ancestors 'self'; frame-src 'self' https:; base-uri 'self'; form-action 'self' https:; connect-src 'self'; worker-src 'self' blob:;" + csp-permissive-header: + headers: + customResponseHeaders: + Content-Security-Policy: "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: https:; font-src 'self' data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self'; worker-src 'self';" + csp-balanced-header: + headers: + customResponseHeaders: + Content-Security-Policy: "default-src 'self'; script-src 'self' https://cdn.jsdelivr.net https://cdnjs.cloudflare.com https://unpkg.com; style-src 'self' 'unsafe-inline' https://fonts.googleapis.com; img-src 'self' data: https:; font-src 'self' https://fonts.gstatic.com data:; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self'; worker-src 'self';" + csp-strict-header: + headers: + customResponseHeaders: + Content-Security-Policy: "default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self'; font-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self'; worker-src 'self';" + csp-strict-header-with-ga: + headers: + customResponseHeaders: + Content-Security-Policy: "default-src 'self'; script-src 'self' https://analytics.mifi.holdings https://scripts.clarity.ms https://www.clarity.ms https://www.googletagmanager.com; style-src 'self' 'unsafe-inline'; img-src 'self' https://analytics.mifi.holdings https://*.clarity.ms https://c.bing.com https://www.google-analytics.com https://www.googletagmanager.com https://stats.g.doubleclick.net; font-src 'self'; frame-ancestors 'self'; base-uri 'self'; form-action 'self'; connect-src 'self' https://analytics.mifi.holdings https://*.clarity.ms https://c.bing.com https://www.google.com https://www.google-analytics.com https://region1.google-analytics.com https://stats.g.doubleclick.net; worker-src 'self'; require-trusted-types-for 'script'; trusted-types default forceInner goog#html mifi-ventures-policy svelte-trusted-html 'allow-duplicates';" + frame-deny-header: + headers: + frameDeny: true # X-Frame-Options: DENY + frame-sameorigin-header: + headers: + customResponseHeaders: + X-Frame-Options: "SAMEORIGIN" + gzip: + compress: {} + hsts-header: + headers: + # Enable HSTS (Strict Transport Security) + stsSeconds: 63072000 # 2 years + stsIncludeSubdomains: true + stsPreload: true # for preload list + internal-ips: + ipWhiteList: + sourceRange: + - 127.0.0.1/32 + - 192.168.50.1/32 + - 50.116.61.55 + - 173.255.238.101 + - 72.93.250.18 + redirect-postfixadmin: + redirectRegex: + regex: "^https://mail\\.mifi\\.holdings/settings/?(.*)" + replacement: "https://postmaster.mifi.holdings/$1" + permanent: true + redirect-www-to-non-www: + redirectRegex: + regex: "^https://www\\.(.*)" + replacement: "https://${1}" + permanent: true + redirect-non-www-to-www: + redirectregex: + permanent: true + regex: "^https?://(?:www\\.)?(.+)" + replacement: "https://www.${1}" + referrer-open-header: + headers: + customResponseHeaders: + Referrer-Policy: "unsafe-url" + referrer-lax-header: + headers: + customResponseHeaders: + Referrer-Policy: "origin-when-cross-origin" + referrer-permissive-header: + headers: + customResponseHeaders: + Referrer-Policy: "strict-origin-when-cross-origin" + referrer-medium-header: + headers: + customResponseHeaders: + Referrer-Policy: "same-origin" + referrer-supermax-header: + headers: + customResponseHeaders: + Referrer-Policy: "no-referrer" + security-headers: + headers: + contentTypeNosniff: true # X-Content-Type-Options: nosniff + customResponseHeaders: + X-XSS-Protection: "0" # Disable legacy browser XSS filter (modern best practice) + secure-all: + chain: + middlewares: + - csp-permissive-header + - frame-sameorigin-header + - hsts-header + - security-headers + security-mallcop: + chain: + middlewares: + - referrer-open-header + - csp-open-header + - hsts-header + - security-headers + security-lax: + chain: + middlewares: + - referrer-lax-header + - csp-lax-header + - hsts-header + - security-headers + security-medium: + chain: + middlewares: + - referrer-permissive-header + - csp-permissive-header + - frame-sameorigin-header + - hsts-header + - security-headers + security-prison: + chain: + middlewares: + - referrer-medium-header + - csp-balanced-header + - frame-sameorigin-header + - hsts-header + - security-headers + security-supermax: + chain: + middlewares: + - referrer-supermax-header + - csp-strict-header + - frame-deny-header + - hsts-header + - security-headers + security-supermax-with-analytics: + chain: + middlewares: + - referrer-supermax-header + - cross-origin-opener-strict-policy + - csp-strict-header-with-ga + - frame-deny-header + - hsts-header + - security-headers + strip-server-path: + stripPrefix: + prefixes: + - "/db" + - "/server" + - "/traefik" diff --git a/traefik/conf.d/routers.yaml b/traefik/conf.d/routers.yaml new file mode 100644 index 0000000..b759a9e --- /dev/null +++ b/traefik/conf.d/routers.yaml @@ -0,0 +1,19 @@ +http: + routers: + # redirect-postfixadmin: + # entryPoints: + # - websecure + # rule: "Host(`mail.mifi.holdings`) && PathPrefix(`/settings`)" + # middlewares: + # - redirect-postfixadmin@file + # service: webmin-service + # tls: + # certResolver: letsencrypt + # webmin: + # entryPoints: + # - websecure + # middlewares: strip-server-path@file + # rule: "Host(`mail.mifi.holdings`) && PathPrefix(`/server`)" + # service: webmin-service + # tls: + # certResolver: letsencrypt diff --git a/traefik/conf.d/services.yaml b/traefik/conf.d/services.yaml new file mode 100644 index 0000000..1550abf --- /dev/null +++ b/traefik/conf.d/services.yaml @@ -0,0 +1,7 @@ +http: + services: + # webmin-service: + # loadBalancer: + # passHostHeader: true + # servers: + # - url: "http://173.255.238.101:10000/" diff --git a/traefik/traefik.yaml b/traefik/traefik.yaml new file mode 100644 index 0000000..ea50591 --- /dev/null +++ b/traefik/traefik.yaml @@ -0,0 +1,74 @@ +api: + dashboard: true + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + websecure: + address: ":443" + traefik: + address: ":8080" + +ping: {} + +providers: + docker: + endpoint: unix:///var/run/docker.sock + watch: true + exposedByDefault: false + file: + directory: /etc/traefik/conf.d + +log: + level: info + +accesslog: + filepath: /dev/stdout + format: json + fields: + defaultmode: keep + headers: + defaultmode: keep + +certificatesResolvers: + letsencrypt: + acme: + email: badmf@mifi.dev + storage: /letsencrypt/acme.json + dnsChallenge: + provider: cloudflare + resolvers: + - 1.1.1.1:53 + - 1.0.0.1:53 + +tls: + stores: + default: + defaultGeneratedCert: + resolver: letsencrypt + domain: + main: mifi.holdings + sans: + - mifi.com.br + - mifi.dev + - mifi.ventures + - fitz.guru + - michael-gerard.com + - asgardianpunk.com + - camilla-rena.com + - dining-it.com + - goodfeatherfarms.com + - mylocalpro.biz + - officelift.net + - starnora.com + - starnoraelizabethfitzpatrick.com + - thenewenglandpalletguy.com + - umlautpress.com + +serversTransport: + insecureSkipVerify: false