Docker compose security review and help with labels

Hi All,

Been using traefik for a while for my homelab and everything seems to work correctly, but wanted to check with the community if my setup is correct and if any security improvements could be made.

In addition I have some questions.

Labels
When adding taefik to a service, based on my setup, these are the labels I add. Do the TLS labels also need to added or are these loaded automatically? I have only have on container with them and all the rest without but not sure this is correct

    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.${APP_NAME}.entryPoints=https"
      # is this needed or is it loaded automatically?
      #- "traefik.http.routers.${APP_NAME}.tls=true"
      #- "traefik.http.routers.${APP_NAME}.tls.certResolver=leresolver"
      - "traefik.http.routers.${APP_NAME}.rule=Host(`${APP_SUBDOMAIN}.${GLOBAL_DOMAIN}`)"
      - "traefik.http.routers.${APP_NAME}.middlewares=security-headers@file"

Forward Headers
I currently have the ForwardHeaders and TrustedIPs commented out. Is this benficial to have and if yes, how do I determine which IPs to put?

Security Headers
Am I missing anything important?


Below is my setup (I am using docker-socket-proxy, I use authentik for authentication, and also have nextcloud which required additional middlewares).

Thanks for and feedback you may have!

docker-compose.yml

services:
  reverse-proxy:
    image: traefik:latest
    container_name: traefik
    ports:  
      - "30080:80" # The HTTP port
      - "30443:443" # The HTTPS port (is this needed?)
      - "38080:8080" # The Web UI (enabled by --api.insecure=true)
    env_file: stack.env
    security_opt:
      - no-new-privileges=true
    volumes:
      - ${GLOBAL_DATA_PATH}/traefik/traefik.yml:/traefik.yml:ro # Configuration with global options
      - ${GLOBAL_DATA_PATH}/traefik/traefik-dynamic.yml:/traefik-dynamic.yml:ro # Configuration for with global dynamic options    
      - ${GLOBAL_DATA_PATH}/traefik/acme.json:/acme.json # LetsEncrypt ACME Configuration    
      - ${GLOBAL_DATA_PATH}/traefik/logs:/logs # Log File (optional)
    labels:
      - "com.centurylinklabs.watchtower.monitor-only=true" # Watchtower watch-only 
    healthcheck:
      test: ["CMD", "traefik", "healthcheck", "--ping"]
    restart: unless-stopped
    networks:
      - traefik-public
      - socket-proxy # docker socket proxy

networks:
  traefik-public:
    external: true
  socket-proxy:
    external: true

traefik.yml

global:
  checkNewVersion: true
  sendAnonymousUsage: false

# Enable the API in insecure mode, which means that the API will be
# available directly on the entryPoint named traefik. If the entryPoint
# named traefik is not configured, it will be automatically created on
# port 8080.
api:
  insecure: true

# Connection to docker host system (docker.sock)
# Attach labels to your containers and let Traefik do the rest!
# Traefik works with both Docker (standalone) Engine and Docker Swarm Mode.
# See: https://docs.traefik.io/providers/docker/
providers:
  docker:
    endpoint: "tcp://docker-socket-proxy:2375"
    watch: true
    exposedbydefault: false
    network: socket-proxy
  # filename is used for dynamic configuration
  file:
    filename: /traefik-dynamic.yml

# EntryPoints are the network entry points into Traefik. They define
# the port which will receive the packets, and whether to listen for
# TCP or UDP.
# See: https://docs.traefik.io/routing/entrypoints/
# NOTE: If a TLS section (i.e. any of its fields) is defined in your docker-compose.yml file,
# then the default configuration does not apply at all.
entryPoints:
  # Standard HTTP redirects to HTTPS
  http:
    address: :80
    http:
      redirections:
        entryPoint:
          to: https
          scheme: https
          permanent: true
    # should I have this? what IPs should I use      
    #forwardedHeaders:
    #  trustedIPs:
    #    - 127.0.0.1/32
    #    - 192.168.0.0/16
    #    - 172.16.0.0/12
  # Standard HTTPS
  https:
    address: :443
    http:
      tls:
        certResolver: leresolver
        domains:
          - main: MYDOMAIN.COM
            # SANS are any other hostnames which Traefik should obtain a certificate for.
            # If you are using DNS for LetsEncrypt, you can set a wildcard.
            # Include all possible hostnames of this server.
            sans:
              - "*.MYDOMAIN.COM"
    # should I have this? what IPs should I use    
    #forwardedHeaders:
    #  trustedIPs:
    #    - 127.0.0.1/32
    #    - 192.168.0.0/16
    #    - 172.16.0.0/12
  # SMTP
  smtp:
    address: :25

# Enable ACME (Let's Encrypt): automatic SSL.
certificatesresolvers:
  leresolver:
    acme:
      email: MY@EMAIL.COM
      storage: acme.json
      # Use HTTP-01 ACME challenge
      # entryPoint: http
      # Use a DNS-01 ACME challenge rather than HTTP-01 challenge.
      # Mandatory for wildcard certificate generation.
      dnsChallenge:
        # Update this to your provider of choice and then ensure necessary variables are in the .env file to support it.
        # see variable here: https://doc.traefik.io/traefik/https/acme/
        provider: MYPROVIDER
        delayBeforeCheck: 0
        # A DNS server used to check whether the DNS is set up correctly before
        # making the ACME request. Ideally a DNS server that isn't going to cache an old entry.
        resolvers:
          - "1.1.1.1:53"

# healthcheck
ping: {}

# enable logs
log:
  filePath: "/logs/traefik.log"
  # level: DEBUG

# enable access logs  
accessLog:
  filePath: "/logs/access.log"

retry: {}

traefik-dynamic.yml

# HTTP Middlewares Configuration
http:
  middlewares:
    # Security Headers Middleware
    security-headers:
      headers:
        accessControlAllowMethods: 'GET, OPTIONS, PUT'
        accesscontrolmaxage: 100
        addVaryHeader: true
        permissionsPolicy: "camera 'none'; geolocation 'none'; microphone 'none'; payment 'none'; usb 'none'; vr 'none';"
        # The stsSeconds is the max-age of the Strict-Transport-Security header. If set to 0, would NOT include the header.
        stsSeconds: 315360000
        # The stsIncludeSubdomains is set to true, the includeSubDomains directive will be appended to the Strict-Transport-Security header.
        stsIncludeSubdomains: true
        # Set stsPreload to true to have the preload flag appended to the Strict-Transport-Security header.
        stsPreload: true
        # Set forceSTSHeader to true, to add the STS header even when the connection is HTTP.
        forceSTSHeader: true
        # Set frameDeny to true to add the X-Frame-Options header with the value of DENY.
        frameDeny: true
        # Set contentTypeNosniff to true to add the X-Content-Type-Options header with the value nosniff.
        contentTypeNosniff: true
        # Set browserXssFilter to true to add the X-XSS-Protection header with the value 1; mode=block.
        browserXSSFilter: true
        # The customFrameOptionsValue allows the X-Frame-Options header value to be set with a custom value. This overrides the FrameDeny option.
        customFrameOptionsValue: 'SAMEORIGIN'
        customRequestHeaders:
          # The customResponseHeaders option lists the Header names and values to apply to the response.
          X-Robots-Tag: 'noindex,nofollow,nosnippet,noarchive,notranslate,noimageindex'
        customResponseHeaders:
          server: ''
          x-powered-by: ''

    # Basic authentication middleware
    simple-auth:
      basicAuth:
        users:
          - "USERNAME:HASHEDPASSWORD"

    # Authentik authentication
    middlewares-authentik:
      forwardAuth:
        address: "http://server:9000/outpost.goauthentik.io/auth/traefik"
        trustForwardHeader: true
        authResponseHeaders:
          - X-authentik-username
          - X-authentik-groups
          - 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

    # The redirects for CalDAV or CardDAV does not work if Nextcloud is running behind a reverse proxy.
    # https://docs.nextcloud.com/server/24/admin_manual/configuration_server/reverse_proxy_configuration.html
    nextcloud-redirectregex-dav:
      redirectRegex:
        permanent: true
        regex: "https://(.*)/.well-known/(card|cal)dav"
        replacement: "https://${1}/remote.php/dav/"

    # https://docs.nextcloud.com/server/24/admin_manual/issues/general_troubleshooting.html#service-discovery
    nextcloud-redirectregex-other:
      redirectRegex:
        permanent: true
        regex: "https://(.*)/.well-known/(webfinger|nodeinfo)"
        replacement: "https://${1}/index.php/.well-known/{$2}"

  # For linuxserver-nextcloud container
  serversTransports:
    ignore-cert:
      insecureSkipVerify: true


# Set secure options by disabling insecure older TLS/SSL versions and insecure ciphers.
# SNIStrict disabled leaves TLS1.0 open. If you have problems with older clients, you can may need to relax these minimums.
# This configuration will give you an A+ SSL security score supporting TLS1.2 and TLS1.3
tls:
  options:
    default:
      sniStrict: true
      minVersion: VersionTLS12
      curvePreferences:
        - secp521r1
        - secp384r1
      cipherSuites:
        - 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_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
        - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
    mintls13:
      minVersion: VersionTLS13

Congrats to think about security. You even use a Docker socket proxy. You don’t state what you use, but might it be an image from a random stranger on the Internet? :wink:

Why would you use this, when you even overwrite return header server for security?

Check simple Traefik dockersocket example for dashboard in sub-domain with auth.

For clean config, assign TLS to entrypoint, no need for it on router.

Thanks for the feedback!

I am using Tecnativa/docker-socket-proxy, I know there is debate around this method.

The reason I set the api to insecure was because I did not want to expose the dashboard to the internet but keep it on the local network. I will update it though :sweat_smile:

And if I understand correctly, since I already have the tls in the htttps entrypoint I can remove the labels:

      - "traefik.http.routers.${APP_NAME}.tls=true"
      - "traefik.http.routers.${APP_NAME}.tls.certResolver=leresolver"

Yes, you can remove the labels, if the router is using all or the specific entrypoint.

1 Like