Cloudflare DNS + Local Domain Certificates

First I want to apologise, as I am still learning a lot around how Traefik (and Docker) work and the below is (especially to those who know what they're doing) a bit of a mess and a combination of multiple different tutorials, guides and trials.
Whilst I have a working container using Cloudflare DNS and my external domain running v2.10.5, it uses CLI within the docker compose which I have learnt has limitations which I now want to overcome.
I want to be able to route (using labels) BOTH for both my external domain (eg. dockerapp.mydomain.com) and use it for my local network (eg. dockerapp.my.lan) with certificates.
✓ Currently *.mydomain.com works perfectly with certificate from Cloudflare
*.my.lan routes correctly (forwarding setup in Adguard) using labels
✗ Haven't been able to get/use a certificate made via mkcert for *.my.lan (Tried using smallstep/step-ca and another online service but decided to simplify)
☐ Note: I intend to add Authelia etc and further expand the security but only once I have the base working)

If anyone is able to help indicate what I am missing, that would be greatly appreciated. I sort of know the logic of what I want to achieve here (using different routers and defining when to use certresolver=dns-cloudflare and when I want to fall-back to a locally stored one) but I haven't been able to achieve it yet.
I know it is something to do with where I am declaring what, and how my labels work, but despite 30+ versions and variations, no joy.

Q?: Given I have a semi working version for essential external sites and I'm asking for help here, I'm also questioning if for this replacement setup I should use Traefik 3?

Here is where I am so far:
Host folders:

$APPDATA/traefikv2-pluslocal
├──acme
│	└──acme.json
├──config
│	└─traefik.yml
└──rules
	├─middlewares-chains.yml
	├─middlewares.yml
	├─routers.yml
	└─tls.yml

$LOGS/traefikv2-pluslocal
├─access.log
└─traefik.log

$SHARED
└─.htpasswd

$CERTS/LAN:
├─cert.pem
└─key.pem

Traefik 'docker-compose' (via Portainer Stack)

version: "3.9"
networks:
  default:
    driver: bridge
  t2_proxy:
    name: t2_proxy
    driver: bridge
    ipam:
      config:
        - subnet: 172.19.0.0/24
    
services:
############################# FRONTENDS
# Traefik 2 - Reverse Proxy
  traefik:
    container_name: traefik
    image: traefik:2.10.5
    networks:
      t2_proxy:
        ipv4_address: 172.19.0.254
    ports:
      - target: 80
        published: 80
        protocol: tcp
        mode: host
      - target: 443
        published: 443
        protocol: tcp
        mode: host
      - target: 8080 
        published: 8080
        protocol: tcp
        mode: host
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock:ro # Change to Proxy once working
      - $CERTS/LAN:/lancerts
      - $SHARED:/shared     
      - $LOGS/traefikv2-pluslocal:/logs 	  
      - $APPDATA/traefikv2-pluslocal/config:/etc/traefik/ # config file
      - $APPDATA/traefikv2-pluslocal/rules:/rules # file provider directory
      - $APPDATA/traefikv2-pluslocal/acme/acme.json:/acme.json # cert location
      - $APPDATA/acme/acme-local.json:/acme-local.json # secondary/local cert? Unsure if needed?
    environment:
      - TZ=$TZ
      - CF_API_EMAIL=$CLOUDFLARE_EMAIL
      - CF_API_KEY=$CLOUDFLARE_API_KEY
      - DOMAINNAME
      - PUID=1000
      - PGID=1000
    security_opt:
      - no-new-privileges:true
    env_file:
      - stack.env
    restart: always
    labels:
      - "traefik.enable=true"
    # HTTP-to-HTTPS Redirect
      - "traefik.http.routers.http-catchall.entrypoints=http"
      - "traefik.http.routers.http-catchall.rule=HostRegexp(`{host:.+}`)"
      - "traefik.http.routers.http-catchall.middlewares=redirect-to-https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
    # HTTP Routers
      - "traefik.http.routers.traefik-rtr.entrypoints=https"
      - "traefik.http.routers.traefik-rtr.rule=Host(`traefik.$DOMAINNAME`)"
      - "traefik.http.routers.traefik-rtr.tls=true"
    # Traefik Local Router
      - "traefik.http.routers.traefik-local-rtr.entrypoints=https"
      - "traefik.http.routers.traefik-local-rtr.rule=Host(`traefik.$LOCAL_DOMAIN`)"
      - "traefik.http.routers.traefik-local-rtr.tls=true"
    # Certs
      - "traefik.http.routers.traefik-rtr.tls.certresolver=dns-cloudflare" # Unsure if needed here? Have tried with and without
      - "traefik.http.routers.traefik-rtr.tls.domains[0].main=$DOMAINNAME"
      - "traefik.http.routers.traefik-rtr.tls.domains[0].sans=*.$DOMAINNAME"
      - "traefik.http.routers.traefik-local-rtr.tls.domains[1].main=$LOCAL_DOMAIN" # Cert for second domain?
      - "traefik.http.routers.traefik-local-rtr.tls.domains[1].sans=*.$LOCAL_DOMAIN" # Cert for second domain
    ## Services - API
      - "traefik.http.routers.traefik-rtr.service=api@internal"
      - "traefik.http.routers.traefik-local-rtr.service=api@internal"
    ## Middlewares
      - "traefik.http.routers.traefik-rtr.middlewares=chain-basic-auth@file"
      - "traefik.http.routers.traefik-local-rtr.middlewares=chain-basic-auth@file"

traefik.yml

global:
  checkNewVersion: true
  sendAnonymousUsage: true
serversTransport:
  insecureSkipVerify: true
entryPoints:
  http: # HTTP non-secure Entrypoint
    address: :80
    forwardedHeaders:
      trustedIPs:
        # Cloudflare IPs
        - 103.21.244.0/22
		# (List shortened for this post)
        # Local IPs
        - 10.0.0.0/8 #LocalDefault
        - 127.0.0.1/32 #Localhost
        - 192.168.0.0/16 #Local Network
        # Docker Networks
        - 172.18.0.0/16
        - 172.19.0.0/16 #t2_proxy
		# (List shortened for this post)
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
  https: # Secure Entrypoint
    address: :443
    forwardedHeaders:
      trustedIPs:
        # Cloudflare IPs
        - 103.21.244.0/22
		# (List shortened for this post)
        # Local IPs
        - 10.0.0.0/8 #LocalDefault
        - 127.0.0.1/32 #Localhost
        - 192.168.0.0/16 #Local Network
        # Docker Networks
        - 172.18.0.0/16
        - 172.19.0.0/16 #t2_proxy
		# (List shortened for this post)
    http:
      tls:
        options: tls-opts@file
        certResolver: dns-cloudflare
        domains:
          - main: mydomain.com
            sans: 
              - '*.mydomain.com'
api:
  dashboard: true
  debug: true
ping: {}
log:
  level: DEBUG
  filePath: "/logs/traefik.log"
accessLog:
  filePath: "/logs/access.log"
  bufferingSize: 100
  filters:
    statusCodes: 
     - "204-299"
     - "400-499"
     - "500-599"
 providers:
  docker:
    watch: true
    endpoint: "unix:///var/run/docker.sock"  # TODO Update to socket proxy once working
    exposedByDefault: false
    network: "t2_proxy"
    swarmMode: false
  file:
    directory: "/rules"  #Dynamic config files
    watch: true 
certificatesResolvers:
  dns-cloudflare:
    acme:
      email: "myemail@mydomain.com"
      storage: "/acme.json"
      dnsChallenge:
        provider: "cloudflare"
        resolvers: 
          - "1.1.1.1:53"
          - "1.0.0.1:53"

middlewares.yml

http:
  middlewares:
    middlewares-basic-auth:
      basicAuth:
        usersFile: "/shared/.htpasswd" # be sure to mount the volume through docker-compose.yml
        realm: "Traefik 2 Basic Auth"
    middlewares-rate-limit:
      rateLimit:
        average: 100
        burst: 50
    middlewares-https-redirectscheme:
      redirectScheme:
        scheme: https
        permanent: true
    middlewares-secure-headers:
      headers:
        accessControlAllowMethods:
          - GET
          - OPTIONS
          - PUT
        accessControlMaxAge: 100
        hostsProxyHeaders:
          - "X-Forwarded-Host"
        stsSeconds: 63072000
        stsIncludeSubdomains: true
        stsPreload: true
        forceSTSHeader: true
        #customFrameOptionsValue: "allow-from https:{{env "DOMAINNAME"}}" #CSP takes care of this but may be needed for organizr.
        contentTypeNosniff: true
        browserXssFilter: true
        # sslForceHost: true # add sslHost to all of the services
        # sslHost: "{{env "DOMAINNAME"}}"
        referrerPolicy: "same-origin"
        permissionsPolicy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), vr=()"
        customResponseHeaders:
          X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex,"
          server: ""
          # https://community.traefik.io/t/how-to-make-websockets-work-with-traefik-2-0-setting-up-rancher/1732
          # X-Forwarded-Proto: "https"
    middlewares-compress:
      compress: {}
    middlewares-local-ipwhitelist: # Only Allow Local networks
      ipWhiteList:
        sourceRange: 
          - 10.0.0.0/8 #LocalDefault
          - 127.0.0.1/32 #Localhost
          - 192.168.0.0/16 #Local Network
          # Docker Networks
          - 172.18.0.0/16
		  # (List shortened for this post)

routers.yml

http:
  routers:
    localrouter:
      entryPoints:
        - "https"
      rule: "Host(`my.lan`)"
      middlewares:
        - "chain-no-auth"  
      tls:
        domains:
          - "my.lan"
          - "*.my.lan"
    http-catchall:
      entryPoints:
        - "http"
      rule: HostRegexp(`{host:.+}`)"
      middlewares:
        - "redirect-to-https"
  serversTransports:
    localtransport:
      serverName: localserver
      certificates:
        - certFile: /lancerts/cert.pem
          keyFile: /lancerts/key.pem

tls.yml

tls:
  certificates:
    - certFile: /lancerts/cert.pem
      keyFile: /lancerts/key.pem
  options:
    tls-opts:
      minVersion: VersionTLS12
      cipherSuites:
        - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
        - TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
        - TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
        - TLS_AES_128_GCM_SHA256
        - TLS_AES_256_GCM_SHA384
        - TLS_CHACHA20_POLY1305_SHA256
        - TLS_FALLBACK_SCSV # Client is doing version fallback. See RFC 7507
      curvePreferences:
        - CurveP521
        - CurveP384
      sniStrict: true

middlewares-chains.yml

http:
  middlewares:
    chain-no-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-https-redirectscheme
          - middlewares-secure-headers
          - middlewares-compress
    chain-basic-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-https-redirectscheme
          - middlewares-secure-headers
          - middlewares-basic-auth
          - middlewares-compress
    chain-local-no-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-https-redirectscheme
          - middlewares-secure-headers
          - middlewares-compress
          - middlewares-local-ipwhitelist       
    chain-local-basic-auth:
      chain:
        middlewares:
          - middlewares-rate-limit
          - middlewares-https-redirectscheme
          - middlewares-secure-headers
          - middlewares-basic-auth
          - middlewares-compress
          - middlewares-local-ipwhitelist

Example Service 'docker-compose' (via Portainer Stack)

---
version: '3'
services:
  overseerr:
    image: overseerr
    container_name: overseerr
	  │
    ports:
      - 28055:5055
    restart: unless-stopped
    env_file:
      - stack.env    
	labels:
      - "traefik.enable=true"
      ## HTTP Routers
      - "traefik.http.routers.overseerr-rtr.entrypoints=https"
      - "traefik.http.routers.overseerr-rtr.rule=Host(`request.$DOMAINNAME`,`request.$LOCAL_DOMAIN`)"

      - "traefik.http.routers.overseerr-rtr.tls=true"
      ## Middlewares
      - "traefik.http.routers.overseerr-rtr.middlewares=chain-no-auth@file"
      ## HTTP Services
      - "traefik.http.routers.overseerr-rtr.service=overseerr-svc"
      - "traefik.http.services.overseerr-svc.loadbalancer.server.port=5055"
networks:
  default:
    external: true
    name: t2_proxy

I think in general a custom TLS cert and a certresolver should work together.

You can start preparing for v3 by using
rule=Host() || Host()

What is this supposed to do without a service?

http:
  routers:
    localrouter:
      entryPoints:
        - "https"
      rule: "Host(`my.lan`)"
      middlewares:
        - "chain-no-auth"  
      tls:
        domains:
          - "my.lan"
          - "*.my.lan"

Is your custom TLS cert loaded? Does it have the internal domain names included?

  • Noted on V3 - Futureproofing myself is the plan.
  • The missing service is what I was playing around with in the 30+ versions. At one point I had it in there, but then I realised that the labels within docker-compose also 'create services' and after a few combinations of trial and lots of error I gave up and this is where I needed guidance in untangling how/where I define what, and then how/where I route it.
  • Short of declaring them in tls.yml (and serversTransports in my routers.yml), I'm not 100% sure where and how to load them. I believe I need to get Traefik to trust the creator but again, needed guidance.
  • Yes the cert (/lancerts/cert.pem + /lancerts/key.pem) was created with *.my.lan & my.lan

I did a quick test and it seems to me LE will always try to get a cert with all listed domains from rule, including the internal ones, and will therefore fail.

To work around this, you would need to create multiple routers for the same service, one for the internal domain with TLS=true and one for external domain with certresolver.

Alternatively you can use something like app.internal.example.com and use LE with dnsChallenge to get a valid cert. You can have that external domain point to an internal IP without a problem.

1 Like

UPDATE: I think I was wrong. If you have the correct internal TLS with CNAME in place and loaded, then you get a LE cert only for the missing or expired domains, so it works.

But I think there is one caveat: if you forget to renew you internal custom cert and it expires, then LE will try to get one and fail - and it will probably also fail to renew the old LE cert after it expired.

1 Like