diff --git a/docker-compose/core/authelia/configuration.yml b/docker-compose/core/authelia/configuration.yml new file mode 100644 index 0000000..2310563 --- /dev/null +++ b/docker-compose/core/authelia/configuration.yml @@ -0,0 +1,81 @@ +# Authelia Configuration +# Copy to /opt/stacks/authelia/configuration.yml +# IMPORTANT: Replace 'kelin-hass.duckdns.org' with your actual DuckDNS domain + +server: + host: 0.0.0.0 + port: 9091 + +log: + level: info + +theme: dark + +jwt_secret: ${AUTHELIA_JWT_SECRET} + +default_redirection_url: https://auth.kelin-hass.duckdns.org + +totp: + issuer: kelin-hass.duckdns.org + period: 30 + skew: 1 + +authentication_backend: + file: + path: /config/users_database.yml + password: + algorithm: argon2id + iterations: 1 + key_length: 32 + salt_length: 16 + memory: 1024 + parallelism: 8 + +access_control: + default_policy: deny + + rules: + # Bypass Authelia for Jellyfin (allow app access) + - domain: jellyfin.kelin-hass.duckdns.org + policy: bypass + + # Bypass for Plex (allow app access) + - domain: plex.kelin-hass.duckdns.org + policy: bypass + + # Bypass for Home Assistant (has its own auth) + - domain: ha.kelin-hass.duckdns.org + policy: bypass + + # Protected: All other services require authentication + - domain: "*.kelin-hass.duckdns.org" + policy: one_factor + + # Two-factor for admin services (optional) + # - domain: + # - "admin.kelin-hass.duckdns.org" + # - "portainer.kelin-hass.duckdns.org" + # policy: two_factor + +session: + name: authelia_session + secret: ${AUTHELIA_SESSION_SECRET} + expiration: 1h + inactivity: 5m + remember_me_duration: 1M + domain: kelin-hass.duckdns.org + +regulation: + max_retries: 3 + find_time: 2m + ban_time: 5m + +storage: + encryption_key: ${AUTHELIA_STORAGE_ENCRYPTION_KEY} + local: + path: /data/db.sqlite3 + +notifier: + # File-based notifications (for development/testing) + filesystem: + filename: /data/notification.txt diff --git a/docker-compose/core/authelia/users_database.yml b/docker-compose/core/authelia/users_database.yml new file mode 100644 index 0000000..59a8408 --- /dev/null +++ b/docker-compose/core/authelia/users_database.yml @@ -0,0 +1,12 @@ +############################################################### +# Users Database # +############################################################### + +users: + kelin: + displayname: "kelin" + password: "$argon2id$v=19$m=65536,t=3,p=4$GirJvw4ecHIr1nnM3ALpwg$7+XjPev3P7AwEveRw5yiq5OmsitXYQp5xR8AxWjDNbI" + email: kelinfoxy@gmail.com + groups: + - admins + - dev diff --git a/docker-compose/core/docker-compose.yml b/docker-compose/core/docker-compose.yml new file mode 100644 index 0000000..f0537d4 --- /dev/null +++ b/docker-compose/core/docker-compose.yml @@ -0,0 +1,151 @@ +# Core Infrastructure Stack +# Essential services required for the homelab to function +# Deploy this stack FIRST before any other services +# Place in /opt/stacks/core/docker-compose.yml + +services: + # DuckDNS - Dynamic DNS updater + # Updates your public IP automatically for Let's Encrypt SSL + duckdns: + image: lscr.io/linuxserver/duckdns:latest + container_name: duckdns + restart: unless-stopped + environment: + - PUID=${PUID:-1000} + - PGID=${PGID:-1000} + - TZ=${TZ} + - SUBDOMAINS=${DUCKDNS_SUBDOMAINS} # Your subdomain(s), comma separated + - TOKEN=${DUCKDNS_TOKEN} # Your DuckDNS token + - UPDATE_IP=ipv4 # or ipv6, or both + volumes: + - /opt/stacks/core/duckdns:/config + labels: + - "homelab.category=infrastructure" + - "homelab.description=Dynamic DNS updater" + + # Traefik - Reverse proxy with automatic SSL + # Routes all traffic and manages Let's Encrypt certificates + traefik: + image: traefik:v2.11 + container_name: traefik + restart: unless-stopped + security_opt: + - no-new-privileges:true + networks: + - traefik-network + ports: + - "80:80" # HTTP + - "443:443" # HTTPS + - "8080:8080" # Dashboard (protected with Authelia) + dns: + - 1.1.1.1 + - 8.8.8.8 + volumes: + - /etc/localtime:/etc/localtime:ro + - /var/run/docker.sock:/var/run/docker.sock:ro + - /opt/stacks/core/traefik/traefik.yml:/traefik.yml:ro + - /opt/stacks/core/traefik/dynamic:/dynamic:ro + - /opt/stacks/core/traefik/acme.json:/acme.json + environment: + - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN} # If using Cloudflare DNS challenge + - DUCKDNS_TOKEN=${DUCKDNS_TOKEN} # If using DuckDNS + - LEGO_DISABLE_CNAME_SUPPORT=true # Disable CNAME support to avoid authoritative NS queries + - LEGO_EXPERIMENTAL_DNS_TCP_SUPPORT=true # Use TCP for DNS queries + - LEGO_DNS_TIMEOUT=60 # DNS timeout in seconds + - LEGO_DNS_RESOLVERS=1.1.1.1:53,8.8.8.8:53 # Force use of specific DNS resolvers + - LEGO_DISABLE_CP=true # Disable authoritative nameserver propagation check + - DUCKDNS_PROPAGATION_TIMEOUT=600 # Increase propagation timeout to 10 minutes + labels: + - "dockge.managed=true" + - "dockge.url=https://traefik.${DOMAIN}" + - "traefik.enable=true" + # Dashboard + - "traefik.http.routers.traefik.rule=Host(`traefik.${DOMAIN}`)" + - "traefik.http.routers.traefik.entrypoints=websecure" + - "traefik.http.routers.traefik.tls.certresolver=letsencrypt" + - "traefik.http.routers.traefik.tls.domains[0].main=${DOMAIN}" + - "traefik.http.routers.traefik.tls.domains[0].sans=*.${DOMAIN}" + - "traefik.http.routers.traefik.middlewares=authelia@docker" + - "traefik.http.routers.traefik.service=api@internal" + # Global HTTP to HTTPS redirect + - "traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)" + - "traefik.http.routers.http-catchall.entrypoints=web" + - "traefik.http.routers.http-catchall.middlewares=redirect-to-https" + - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https" + depends_on: + - duckdns + + # Authelia - SSO authentication + # Protects all admin services with single sign-on + authelia: + image: authelia/authelia:4.37 + container_name: authelia + restart: unless-stopped + networks: + - traefik-network + volumes: + - /opt/stacks/core/authelia/configuration.yml:/config/configuration.yml:ro + - /opt/stacks/core/authelia/users_database.yml:/config/users_database.yml + - authelia-data:/data + environment: + - TZ=${TZ} + - AUTHELIA_JWT_SECRET=${AUTHELIA_JWT_SECRET} + - AUTHELIA_SESSION_SECRET=${AUTHELIA_SESSION_SECRET} + - AUTHELIA_STORAGE_ENCRYPTION_KEY=${AUTHELIA_STORAGE_ENCRYPTION_KEY} + labels: + - "dockge.managed=true" + - "dockge.url=https://auth.${DOMAIN}" + - "traefik.enable=true" + - "traefik.http.routers.authelia.rule=Host(`auth.${DOMAIN}`)" + - "traefik.http.routers.authelia.entrypoints=websecure" + - "traefik.http.routers.authelia.tls=true" + - "traefik.http.services.authelia.loadbalancer.server.port=9091" + # Authelia middleware for other services + - "traefik.http.middlewares.authelia.forwardauth.address=http://authelia:9091/api/verify?rd=https://auth.${DOMAIN}" + - "traefik.http.middlewares.authelia.forwardauth.trustForwardHeader=true" + - "traefik.http.middlewares.authelia.forwardauth.authResponseHeaders=Remote-User,Remote-Groups,Remote-Name,Remote-Email" + depends_on: + - traefik + + # Gluetun - VPN client (Surfshark WireGuard) + # Routes download clients through VPN for security + gluetun: + image: qmcgaw/gluetun:latest + container_name: gluetun + restart: unless-stopped + cap_add: + - NET_ADMIN + devices: + - /dev/net/tun:/dev/net/tun + networks: + - homelab-network + - traefik-network + ports: + - "8888:8888/tcp" # HTTP proxy + - "8388:8388/tcp" # Shadowsocks + - "8388:8388/udp" # Shadowsocks + - "8081:8080" # qBittorrent web UI + - "6881:6881" # qBittorrent + - "6881:6881/udp" # qBittorrent + volumes: + - /opt/stacks/core/gluetun:/gluetun + environment: + - VPN_SERVICE_PROVIDER=surfshark + - VPN_TYPE=openvpn + - OPENVPN_USER=${SURFSHARK_USERNAME} + - OPENVPN_PASSWORD=${SURFSHARK_PASSWORD} + - SERVER_COUNTRIES=${VPN_SERVER_COUNTRIES:-Netherlands} + - TZ=${TZ} + labels: + - "homelab.category=infrastructure" + - "homelab.description=VPN client for secure downloads" + +volumes: + authelia-data: + driver: local + +networks: + traefik-network: + external: true + homelab-network: + external: true diff --git a/docker-compose/core/duckdns/logrotate.conf b/docker-compose/core/duckdns/logrotate.conf new file mode 100644 index 0000000..828dfc6 --- /dev/null +++ b/docker-compose/core/duckdns/logrotate.conf @@ -0,0 +1,5 @@ +/config/duck.log { + rotate 5 + size 100k + compress +} diff --git a/docker-compose/core/traefik/acme.json b/docker-compose/core/traefik/acme.json new file mode 100644 index 0000000..3a63215 --- /dev/null +++ b/docker-compose/core/traefik/acme.json @@ -0,0 +1,16 @@ +{ + "letsencrypt": { + "Account": { + "Email": "kelinfoxy@gmail.com", + "Registration": { + "body": { + "status": "valid" + }, + "uri": "https://acme-v02.api.letsencrypt.org/acme/acct/2959423246" + }, + "PrivateKey": "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", + "KeyType": "4096" + }, + "Certificates": null + } +} \ No newline at end of file diff --git a/docker-compose/core/traefik/dynamic/routes.yml b/docker-compose/core/traefik/dynamic/routes.yml new file mode 100644 index 0000000..cdaf10e --- /dev/null +++ b/docker-compose/core/traefik/dynamic/routes.yml @@ -0,0 +1,31 @@ +# Traefik Dynamic Configuration +# Copy to /opt/stacks/traefik/dynamic/routes.yml +# Add custom routes here that aren't defined via Docker labels + +http: + routers: + # Example custom route + # custom-service: + # rule: "Host(`custom.example.com`)" + # entryPoints: + # - websecure + # middlewares: + # - authelia@docker + # tls: + # certResolver: letsencrypt + # service: custom-service + + services: + # Example custom service + # custom-service: + # loadBalancer: + # servers: + # - url: "http://192.168.1.100:8080" + + middlewares: + # Additional middlewares can be defined here + # Example: Rate limiting + # rate-limit: + # rateLimit: + # average: 100 + # burst: 50 diff --git a/docker-compose/core/traefik/traefik.yml b/docker-compose/core/traefik/traefik.yml new file mode 100644 index 0000000..bbcba25 --- /dev/null +++ b/docker-compose/core/traefik/traefik.yml @@ -0,0 +1,56 @@ +# Traefik Static Configuration +# Copy to /opt/stacks/traefik/traefik.yml + +global: + checkNewVersion: true + sendAnonymousUsage: false + +api: + dashboard: true + insecure: false # Dashboard accessible via Traefik route with Authelia + +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + + websecure: + address: ":443" + http: + tls: + certResolver: letsencrypt + +certificatesResolvers: + letsencrypt: + acme: + email: kelinfoxy@gmail.com + storage: /acme.json + # Use DNS challenge for wildcard certificate support + dnsChallenge: + provider: duckdns + delayBeforeCheck: 300 # Wait 5 minutes before checking DNS propagation + resolvers: + - "1.1.1.1:53" + - "8.8.8.8:53" + +providers: + docker: + endpoint: "unix:///var/run/docker.sock" + exposedByDefault: false # Only expose services with traefik.enable=true + network: traefik-network + + file: + directory: /dynamic + watch: true + +log: + level: DEBUG # DEBUG, INFO, WARN, ERROR + filePath: /var/log/traefik/traefik.log + +accessLog: + filePath: /var/log/traefik/access.log + bufferingSize: 100 diff --git a/docker-compose/dashboards/docker-compose.yml b/docker-compose/dashboards/docker-compose.yml new file mode 100644 index 0000000..e7c09b5 --- /dev/null +++ b/docker-compose/dashboards/docker-compose.yml @@ -0,0 +1,71 @@ +# Dashboard Services +# Homepage and Homarr for homelab dashboards +# Place in /opt/stacks/dashboards/docker-compose.yml + +services: + # Homepage - Application dashboard (AI-configurable via YAML) + # Access at: https://home.${DOMAIN} + homepage: + image: ghcr.io/gethomepage/homepage:latest + container_name: homepage + restart: unless-stopped + networks: + - homelab-network + - traefik-network + - dockerproxy-network + volumes: + - /opt/stacks/homepage/config:/app/config + - /var/run/docker.sock:/var/run/docker.sock:ro # For Docker integration + - /opt/stacks:/opt/stacks:ro # To discover other stacks + environment: + - PUID=${PUID:-1000} + - PGID=${PGID:-1000} + - TZ=${TZ} + - HOMEPAGE_ALLOWED_HOSTS=home.${DOMAIN} + labels: + - "homelab.category=dashboard" + - "homelab.description=Application dashboard (AI-configurable)" + - "dockge.managed=true" + - "dockge.url=https://home.${DOMAIN}" + - "traefik.enable=true" + - "traefik.http.routers.homepage.rule=Host(`home.${DOMAIN}`)" + - "traefik.http.routers.homepage.entrypoints=websecure" + - "traefik.http.routers.homepage.tls=true" + - "traefik.http.routers.homepage.middlewares=authelia@docker" + - "traefik.http.services.homepage.loadbalancer.server.port=3000" + + # Homarr - Modern dashboard + # Access at: https://homarr.${DOMAIN} + homarr: + image: ghcr.io/ajnart/homarr:latest + container_name: homarr + restart: unless-stopped + networks: + - homelab-network + - traefik-network + volumes: + - /opt/stacks/homarr/configs:/app/data/configs + - /opt/stacks/homarr/data:/data + - /opt/stacks/homarr/icons:/app/public/icons + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - TZ=${TZ} + labels: + - "homelab.category=dashboard" + - "homelab.description=Modern homelab dashboard" + - "dockge.managed=true" + - "dockge.url=https://homarr.${DOMAIN}" + - "traefik.enable=true" + - "traefik.http.routers.homarr.rule=Host(`homarr.${DOMAIN}`)" + - "traefik.http.routers.homarr.entrypoints=websecure" + - "traefik.http.routers.homarr.tls=true" + - "traefik.http.routers.homarr.middlewares=authelia@docker" + - "traefik.http.services.homarr.loadbalancer.server.port=7575" + +networks: + homelab-network: + external: true + traefik-network: + external: true + dockerproxy-network: + external: true diff --git a/docker-compose/infrastructure/docker-compose.yml b/docker-compose/infrastructure/docker-compose.yml new file mode 100644 index 0000000..cb68aac --- /dev/null +++ b/docker-compose/infrastructure/docker-compose.yml @@ -0,0 +1,169 @@ +# Infrastructure Services +# Core services that other services depend on +# Place in /opt/stacks/infrastructure/docker-compose.yml +# NOTE: Traefik, Authelia, DuckDNS, and Gluetun have their own separate stacks +# See /opt/stacks/traefik/, /opt/stacks/authelia/, etc. + +services: + # Dockge - Docker Compose Stack Manager (PRIMARY - preferred over Portainer) + # Access at: https://dockge.${DOMAIN} + dockge: + image: louislam/dockge:1 + container_name: dockge + restart: unless-stopped + networks: + - homelab-network + - traefik-network + ports: + - 5001:5001 # Optional: direct access + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - /opt/stacks:/opt/stacks # Dockge manages stacks in this directory + - /opt/dockge/data:/app/data + environment: + - DOCKGE_STACKS_DIR=/opt/stacks + - DOCKGE_ENABLE_CONSOLE=true + labels: + - homelab.category=infrastructure + - homelab.description=Docker Compose stack manager (PRIMARY) + - dockge.managed=true + - dockge.url=https://dockge.${DOMAIN} + - traefik.enable=true + - traefik.http.routers.dockge.rule=Host(`dockge.${DOMAIN}`) + - traefik.http.routers.dockge.entrypoints=websecure + - traefik.http.routers.dockge.tls=true + - traefik.http.routers.dockge.middlewares=authelia@docker + - traefik.http.services.dockge.loadbalancer.server.port=5001 + # Pi-hole - Network-wide ad blocker and DNS server + # Access at: https://pihole.${DOMAIN} + pihole: + image: pihole/pihole:2024.01.0 + container_name: pihole + restart: unless-stopped + networks: + - homelab-network + - traefik-network + ports: + - 53:53/tcp # DNS TCP + - 53:53/udp # DNS UDP + volumes: + - /opt/stacks/pihole/etc-pihole:/etc/pihole + - /opt/stacks/pihole/etc-dnsmasq.d:/etc/dnsmasq.d + environment: + - TZ=${TZ:-America/New_York} + - WEBPASSWORD=${PIHOLE_PASSWORD:-changeme} + - FTLCONF_LOCAL_IPV4=${SERVER_IP} + dns: + - 127.0.0.1 + - 1.1.1.1 + cap_add: + - NET_ADMIN + labels: + - homelab.category=infrastructure + - homelab.description=Network-wide ad blocking and DNS - dockge.managed=true + - dockge.url=https://pihole.${DOMAIN} - traefik.enable=true + - traefik.http.routers.pihole.rule=Host(`pihole.${DOMAIN}`) + - traefik.http.routers.pihole.entrypoints=websecure + - traefik.http.routers.pihole.tls=true + - traefik.http.routers.pihole.middlewares=authelia@docker + - traefik.http.services.pihole.loadbalancer.server.port=80 + # Watchtower - Automatic container updates + # Runs silently in background, no UI + watchtower: + image: containrrr/watchtower:latest + container_name: watchtower + restart: unless-stopped + networks: + - homelab-network + volumes: + - /var/run/docker.sock:/var/run/docker.sock + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_INCLUDE_RESTARTING=true + - WATCHTOWER_SCHEDULE=0 0 4 * * * # 4 AM daily + - WATCHTOWER_NOTIFICATIONS=shoutrrr + - WATCHTOWER_NOTIFICATION_URL=${WATCHTOWER_NOTIFICATION_URL} + - DOCKER_API_VERSION=1.44 + labels: + - homelab.category=infrastructure + - homelab.description=Automatic Docker container updates + # Dozzle - Real-time Docker log viewer + # Access at: https://dozzle.${DOMAIN} + dozzle: + image: amir20/dozzle:latest + container_name: dozzle + restart: unless-stopped + networks: + - homelab-network + - traefik-network + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - DOZZLE_LEVEL=info + - DOZZLE_TAILSIZE=300 + - DOZZLE_FILTER=status=running + labels: + - homelab.category=infrastructure + - homelab.description=Real-time Docker log viewer + - dockge.managed=true + - dockge.url=https://dozzle.${DOMAIN} + - traefik.enable=true + - traefik.http.routers.dozzle.rule=Host(`dozzle.${DOMAIN}`) + - traefik.http.routers.dozzle.entrypoints=websecure + - traefik.http.routers.dozzle.tls=true + - traefik.http.routers.dozzle.middlewares=authelia@docker + - traefik.http.services.dozzle.loadbalancer.server.port=8080 + # Docker Proxy - Socket proxy for security + # Used by services that need Docker socket access + dockerproxy: + image: tecnativa/docker-socket-proxy:latest + container_name: dockerproxy + restart: unless-stopped + networks: + - dockerproxy-network + ports: + - 127.0.0.1:2375:2375 + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - CONTAINERS=1 + - SERVICES=1 + - TASKS=1 + - NETWORKS=1 + - NODES=1 + labels: + - homelab.category=infrastructure + - homelab.description=Docker socket proxy for security + # Glances - System monitoring + # Access at: https://glances.${DOMAIN} + glances: + image: nicolargo/glances:latest-full + container_name: glances + restart: unless-stopped + networks: + - homelab-network + - traefik-network + pid: host + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - /opt/stacks/glances/config:/glances/conf + environment: + - GLANCES_OPT=-w + labels: + - homelab.category=infrastructure + - homelab.description=System and Docker monitoring + - dockge.managed=true + - dockge.url=https://glances.${DOMAIN} + - traefik.enable=true + - traefik.http.routers.glances.rule=Host(`glances.${DOMAIN}`) + - traefik.http.routers.glances.entrypoints=websecure + - traefik.http.routers.glances.tls=true + - traefik.http.routers.glances.middlewares=authelia@docker + - traefik.http.services.glances.loadbalancer.server.port=61208 +networks: + homelab-network: + driver: bridge + dockerproxy-network: + driver: bridge + traefik-network: + external: true diff --git a/docs/troubleshooting/SSL-CERTIFICATES-DUCKDNS.md b/docs/troubleshooting/SSL-CERTIFICATES-DUCKDNS.md new file mode 100644 index 0000000..c1ab75b --- /dev/null +++ b/docs/troubleshooting/SSL-CERTIFICATES-DUCKDNS.md @@ -0,0 +1,223 @@ +# SSL Certificate Issues with DuckDNS DNS Challenge + +## Issue Summary +Wildcard SSL certificate acquisition via DuckDNS DNS-01 challenge consistently fails due to network connectivity issues with DuckDNS authoritative nameservers. + +## Root Cause Analysis + +### Why Both Domain and Wildcard are Required +Let's Encrypt requires validation of BOTH domains when using SAN (Subject Alternative Name) certificates: +- `kelin-hass.duckdns.org` (apex domain) +- `*.kelin-hass.duckdns.org` (wildcard) + +This is a Let's Encrypt policy - you cannot obtain just the wildcard certificate. Both must be validated simultaneously. + +### Technical Root Cause: Unreachable Authoritative Nameservers + +**Problem**: DuckDNS authoritative nameservers (ns1-ns9.duckdns.org) are **unreachable** from the test system's network. + +**Evidence**: +```bash +# Direct ping to DuckDNS nameservers - 100% packet loss +ping -c 2 ns1.duckdns.org # FAIL: 100% packet loss +ping -c 2 99.79.143.35 # FAIL: 100% packet loss (direct IP) + +# DNS queries to authoritative servers - timeout +dig @99.79.143.35 kelin-hass.duckdns.org # FAIL: timeout +dig @35.182.183.211 kelin-hass.duckdns.org # FAIL: timeout +dig @3.97.58.28 kelin-hass.duckdns.org # FAIL: timeout + +# Queries to recursive resolvers - SUCCESS +dig @8.8.8.8 kelin-hass.duckdns.org # SUCCESS +dig @1.1.1.1 kelin-hass.duckdns.org # SUCCESS + +# Traceroute analysis +traceroute 99.79.143.35 +# Shows traffic reaching hop 5 (74.41.143.193) then black hole +# DuckDNS nameservers are hosted on Amazon AWS +# Suggests AWS security groups or ISP blocking +``` + +**Why This Matters**: +Traefik's ACME client (lego library) requires verification against authoritative nameservers after setting TXT records. Even though: +- DuckDNS API successfully sets TXT records ✅ +- TXT records propagate to public DNS (8.8.8.8, 1.1.1.1) ✅ +- Recursive DNS queries work ✅ + +The lego library **must** also query the authoritative nameservers directly to verify propagation, and this step fails due to network unreachability. + +## Attempted Solutions + +### Configuration Optimizations Tried + +1. **Increased propagation delay** - `delayBeforeCheck: 300` (5 minutes) + - Result: Delay worked, but authoritative NS check still failed + +2. **Extended timeout** - `DUCKDNS_PROPAGATION_TIMEOUT=600` (10 minutes) + - Result: Longer timeout observed, but same NS unreachability issue + +3. **LEGO environment variables**: + ```yaml + - LEGO_DISABLE_CNAME_SUPPORT=true + - LEGO_EXPERIMENTAL_DNS_TCP_SUPPORT=true + - LEGO_DNS_TIMEOUT=60 + - LEGO_DNS_RESOLVERS=1.1.1.1:53,8.8.8.8:53 + - LEGO_DISABLE_CP=true + ``` + - Result: Forced use of recursive resolvers for some queries, but SOA lookups still failed + +4. **Explicit Docker DNS configuration**: + ```yaml + dns: + - 1.1.1.1 + - 8.8.8.8 + ``` + - Result: Container used correct resolvers, but lego still attempted authoritative NS queries + +5. **VPN routing test** (through Gluetun container) + - Result: DuckDNS nameservers also unreachable through VPN + +### Error Messages Observed + +**Phase 1: Direct authoritative nameserver timeout** +``` +propagation: time limit exceeded: last error: authoritative nameservers: +DNS call error: read udp 172.19.0.2:53666->3.97.58.28:53: i/o timeout +[ns=ns6.duckdns.org.:53, question='_acme-challenge.kelin-hass.duckdns.org. IN TXT'] +``` + +**Phase 2: SOA record query failure** +``` +propagation: time limit exceeded: last error: could not find zone: +[fqdn=_acme-challenge.kelin-hass.duckdns.org.] +unexpected response for 'kelin-hass.duckdns.org.' +[question='kelin-hass.duckdns.org. IN SOA', code=SERVFAIL] +``` + +## Working Configuration (Self-Signed Certificates) + +Current deployment is **fully functional** with self-signed certificates: +- All services accessible via HTTPS ✅ +- Can proceed through browser certificate warnings ✅ +- Traefik routing works correctly ✅ +- Authelia SSO functional ✅ +- All stacks deployed successfully ✅ + +## Recommended Solutions for Next Test Run + +### Option 1: Switch to Cloudflare DNS (RECOMMENDED) +**Pros**: +- Cloudflare nameservers are highly reliable and globally accessible +- Supports wildcard certificates via DNS-01 challenge +- Better performance and propagation times +- Well-tested with Traefik + +**Steps**: +1. Move domain to Cloudflare (free tier sufficient) +2. Obtain Cloudflare API token (Zone:DNS:Edit permission) +3. Update `traefik.yml`: + ```yaml + dnsChallenge: + provider: cloudflare + delayBeforeCheck: 30 # Cloudflare propagates quickly + resolvers: + - "1.1.1.1:53" + - "1.0.0.1:53" + ``` +4. Update `docker-compose.yml`: + ```yaml + environment: + - CF_DNS_API_TOKEN=${CF_DNS_API_TOKEN} + ``` + +### Option 2: Investigate Network Blocking +**Diagnostic Steps**: +1. Test from different network (mobile hotspot, different ISP) +2. Contact ISP to check if AWS IP ranges are blocked +3. Check router/firewall for DNS filtering or AWS blocking +4. Test with different VPN provider + +**If network is the issue**: +- May need to use VPN or proxy for Traefik container +- Consider hosting Traefik on different network segment + +### Option 3: HTTP-01 Challenge (Non-Wildcard) +**Pros**: +- More reliable (no DNS dependencies) +- Works with current DuckDNS setup +- No external nameserver queries required + +**Cons**: +- ❌ No wildcard certificate (must specify each subdomain) +- Requires port 80 accessible from internet +- Separate certificate for each subdomain + +**Steps**: +1. Update `traefik.yml`: + ```yaml + httpChallenge: + entryPoint: web + ``` +2. Remove wildcard domain label from Traefik service: + ```yaml + # Remove this line: + - "traefik.http.routers.traefik.tls.domains[0].sans=*.${DOMAIN}" + ``` +3. Add explicit TLS configuration to each service's labels + +### Option 4: Use Alternative DNS Provider with DuckDNS +Keep DuckDNS for dynamic IP updates, but use different DNS for certificates: +1. Use Cloudflare for DNS records +2. Keep DuckDNS container for IP updates +3. Create CNAME in Cloudflare pointing to DuckDNS +4. Use Cloudflare for certificate challenge + +## Files to Update in Repository + +### ~/AI-Homelab/stacks/core/traefik/traefik.yml +Document both HTTP and DNS challenge configurations with clear comments. + +### ~/AI-Homelab/stacks/core/docker-compose.yml +Ensure wildcard domain configuration is correct (it is currently): +```yaml +- "traefik.http.routers.traefik.tls.domains[0].main=${DOMAIN}" +- "traefik.http.routers.traefik.tls.domains[0].sans=*.${DOMAIN}" +``` +**This is correct** - keep both apex and wildcard. + +### ~/AI-Homelab/docs/service-docs/traefik.md +Add troubleshooting section for DuckDNS DNS challenge issues. + +## Success Criteria for Next Test + +### Must Have: +- [ ] Valid wildcard SSL certificate obtained +- [ ] Certificate automatically renews +- [ ] No browser certificate warnings +- [ ] Documented working configuration + +### Should Have: +- [ ] Certificate acquisition completes in < 5 minutes +- [ ] Reliable across multiple test runs +- [ ] Clear error messages if failure occurs + +## Timeline Analysis + +**First Test Run**: Certificates reportedly worked +**Current Test Run**: Consistent failures + +**Possible Explanations**: +1. DuckDNS infrastructure changes (AWS security policies) +2. ISP routing changes +3. Increased AWS security after abuse/attacks +4. Different network environment during first test + +## Conclusion + +**Current Status**: System is production-ready except for SSL certificate warnings. + +**Blocking Issue**: DuckDNS authoritative nameservers unreachable from current network environment. + +**Recommendation**: **Switch to Cloudflare DNS** for next test run. This is the most reliable solution and is the industry standard for automated certificate management with Traefik. + +**Alternative**: If staying with DuckDNS is required, investigate network connectivity issues with ISP and consider using HTTP-01 challenge (losing wildcard capability).