Traefik 2 with docker autodiscovery + automatic https + http to https redirection + secure api + basic auth + influxdb metrics

20/04/2020

Minimal version

http server with docker autodiscovery
Create a docker-compose.yml with this content

version: "3.3"

services:
  traefik:
    image: "traefik:v2.0.0"
    command:
      - --entrypoints.web.address=:80
      - --providers.docker=true
    ports:
      - "80:80"
    volumes:
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
  
  myapp:
    image: containous/whoami:v1.3.0
    labels:
      - traefik.http.routers.myapp.rule=Host(`test.raphaelpiccolo.com`)
    ports:
      - "8989:80"
      

Then

docker-compose up -d

curl http://test.raphaelpiccolo.com
curl http://test.raphaelpiccolo.com:8989

Both url work, to block second url, bind to localhost port :

- "127.0.0.1:8989:80"

It's also possible to remove ports specification completely if the image exposes the port, because traefik will find it.

complete version :

http server with docker autodiscovery
automatically redirect http to https if using websecure entrypoint
automatically generate https when needed
This file will store https certificates : /traefik/acme.json
wait 10 secs for letsencrypt to generate certificates. In the meantime the site is working but there is a certificate alert.
traefik api is now available https://traefik.raphaelpiccolo.com

Create a docker-compose.yml

version: "3.3"

services:
  traefik:
    image: "traefik:v2.0.0"
    command:
      # listen port 443 and name it websecure
      - --entrypoints.websecure.address=:443
      # listen port 80 and name it web
      - --entrypoints.web.address=:80
      # you need to enable traefik on each container
      - --providers.docker.exposedByDefault=false
      # listen for docker changes
      - --providers.docker=true
      # activate traefik api
      - --api
      # activate traefik ping service
      - --ping
      - --ping.manualrouting=true
      - --ping.entryPoint=websecure
      # use letencrypt to generate certificates
      - --certificatesresolvers.le.acme.email=rafi.piccolo@gmail.com
      - --certificatesresolvers.le.acme.storage=/traefik/acme.json
      - --certificatesresolvers.le.acme.tlschallenge=true
      # set logging level
      - --log.level=INFO
      # activate accesslog in a separate file
      - --accesslog=true
      - --accesslog.filepath=/traefik/access.log
      # activate metrics exporting to influxdb
      - --metrics.influxdb=true
      - --metrics.influxdb.protocol=http
      - --metrics.influxdb.address=https://influxdb.raphaelpiccolo.com
      - --metrics.influxdb.database=mydb
      - --metrics.influxdb.username=admin
      - --metrics.influxdb.password=${PASSWORD}
      # config file for more settings : tls ciphers for grade A on ssllabs
      - --providers.file.filename=/traefik/traefik.toml
      - --providers.file.watch=true
      # add a custom uniqid on every request
      - --tracing.jaeger=true
      - --tracing.jaeger.samplingParam=0
      - --tracing.jaeger.traceContextHeaderName=X-Request-ID

    labels:
      - traefik.enable=true
    
      # catch all traefik errors and send them to the errorpage service
      - "traefik.http.routers.globalerrorpage.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.globalerrorpage.tls.certresolver=le"
      - "traefik.http.routers.globalerrorpage.entrypoints=websecure"
      - "traefik.http.routers.globalerrorpage.service=errorpage"
      - "traefik.http.routers.globalerrorpage.priority=1"

      # create a middleware to replace 404 errors by a standard page
      - "traefik.http.middlewares.errorpage.errors.status=404"
      - "traefik.http.middlewares.errorpage.errors.service=errorpage"
      - "traefik.http.middlewares.errorpage.errors.query=/{status}"

      # create a middleware named redirect-to-https, to redirect http to https 
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"
      - "traefik.http.middlewares.redirect-to-https.redirectscheme.permanent=true"
      
      # create a middleware to redirect to www
      - traefik.http.middlewares.redirect-to-www.redirectregex.regex=(https|http)://(?:www.)?(.*)
      - traefik.http.middlewares.redirect-to-www.redirectregex.replacement=https://www.$${2}
      
      # create a middleware to redirect to non-www
      - traefik.http.middlewares.redirect-to-nonwww.redirectregex.regex=(https|http)://(www\.(.*))
      - traefik.http.middlewares.redirect-to-nonwww.redirectregex.replacement=https://$${3}

      # apply middleware "redirect-to-https", to all hosts connected to "web" entrypoint
      - "traefik.http.routers.redirs.rule=hostregexp(`{host:.+}`)"
      - "traefik.http.routers.redirs.entrypoints=web"
      - "traefik.http.routers.redirs.middlewares=redirect-to-https"

      # create a middleware named auth, to request a basic authentification of users
      # you can generate a user:password pair with this command :
      # echo $(htpasswd -nb user password) | sed -e s/\\$/\\$\\$/g
      - traefik.http.middlewares.auth.basicauth.users=user:$$apr1$$WH5iVjXW$$bda85jX7RQGGcCs3hnP8b0

      # create a middleware named admin, to request a basic authentification for admin
      - traefik.http.middlewares.admin.basicauth.users=admin:$$apr1$$SIIXcmeY$$GAkMymzdfhwACgQvM9PNy1

      # create a middleware named securityheaders to automatically set security headers to pass : https://securityheaders.com/
      - "traefik.http.middlewares.securityheaders.headers.framedeny=true"
      - "traefik.http.middlewares.securityheaders.headers.customFrameOptionsValue=sameorigin"
      - "traefik.http.middlewares.securityheaders.headers.referrerPolicy=same-origin"
      - "traefik.http.middlewares.securityheaders.headers.BrowserXssFilter=true"
      - "traefik.http.middlewares.securityheaders.headers.ContentTypeNosniff=true"
      - "traefik.http.middlewares.securityheaders.headers.ForceSTSHeader=true"
      - "traefik.http.middlewares.securityheaders.headers.SSLRedirect=true"
      - "traefik.http.middlewares.securityheaders.headers.STSIncludeSubdomains=true"
      - "traefik.http.middlewares.securityheaders.headers.STSPreload=true"
      - "traefik.http.middlewares.securityheaders.headers.STSSeconds=315360000"

      # api secure
      - "traefik.http.routers.traefik.rule=Host(`traefik.raphaelpiccolo.com`)"
      - "traefik.http.routers.traefik.service=api@internal"
      - "traefik.http.routers.traefik.middlewares=securityheaders,admin"
      - "traefik.http.routers.traefik.tls.certresolver=le"
      - "traefik.http.routers.traefik.entrypoints=websecure"

      # ping secure
      - "traefik.http.routers.ping.rule=Host(`traefik.${DOMAIN}`) && PathPrefix(`/ping`)"
      - "traefik.http.routers.ping.service=ping@internal"
      - "traefik.http.routers.ping.middlewares=securityheaders"
      - "traefik.http.routers.ping.tls.certresolver=le"
      - "traefik.http.routers.ping.entrypoints=websecure"
        
    ports:
      # web entrypoint
      - "80:80"
      # websecure entrypoint
      - "443:443"
    volumes:
      # to listen docker changes
      - "/var/run/docker.sock:/var/run/docker.sock:ro"
      # to save https certificates
      - "./traefik:/traefik"

  myapp:
    image: containous/whoami:v1.3.0
    labels:
      # domain name for this container : you can specify multiple domains
      - traefik.http.routers.myapp.rule=Host(`raphaelpiccolo.com`, `www.raphaelpiccolo.com`)
      # set a basic auth on a container + securityheaders
      - traefik.http.routers.myapp.middlewares=auth,securityheaders,redirect-to-www
      # use https
      - traefik.http.routers.myapp.tls.certresolver=le
      - traefik.http.routers.myapp.entrypoints=websecure
    ports:
      - "127.0.0.1:9090:80"

  # proxy to host (172.17.0.1) port (19999)
  # get docker host ip (usable from inside a container) : ip addr show docker0
  # displays : 172.17.0.1
  netdata:
    image: alpine/socat
    container_name: netdata
    restart: always
    command: tcp-listen:80,fork,reuseaddr tcp-connect:172.17.0.1:19999
    labels:
      - traefik.http.routers.netdata.rule=Host(`netdata.flatbay.fr`)
      # when the container has many ports you can indicate to traefik which one he should connect to the domain
      - traefik.http.services.netdata.loadbalancer.server.port=80
      # if you want to specify tls options (see traefik.toml)
      - traefik.http.routers.netdata.tls.options=notsafe@file
      - traefik.http.routers.netdata.tls.certresolver=le
      - traefik.http.routers.netdata.entrypoints=websecure
      - traefik.http.routers.netdata.middlewares=securityheaders,admin

  portainer:
    image: portainer/portainer
    container_name: portainer
    restart: always
    ports:
      - "127.0.0.1:19000:9000"
    # no auth because we add the admin basic auth with traefik
    command: --no-auth
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - ./portainer:/data
    labels:
      - traefik.http.routers.portainer.rule=Host(`portainer.flatbay.fr`)
      - traefik.http.routers.portainer.tls.certresolver=le
      - traefik.http.routers.portainer.entrypoints=websecure
      - traefik.http.routers.portainer.middlewares=securityheaders,admin

create traefik.toml
only usefull if grade A on ssllabs

[tls.options]
  [tls.options.default]
    minVersion = "VersionTLS12"
    cipherSuites = [
        "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256",
        "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384",
        "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305",
        "TLS_AES_128_GCM_SHA256",
        "TLS_AES_256_GCM_SHA384",
        "TLS_CHACHA20_POLY1305_SHA256"
    ]
    sniStrict = true
  [tls.options.notsafe]

other technique to create a service to redirect to host

in traefik.toml

[http]
  [http.routers]
    [http.routers.netdata]
      rule = "Host(`netdata.flatbay.fr`)"
      service = "netdata"

  [http.services]
    [http.services.netdata.loadBalancer]
      [[http.services.netdata.loadBalancer.servers]]
        url = "http://172.17.0.1:19999/"

create an standard error page :

create a new service

errorpage:
    build: ./errorpage
    restart: always
    container_name: errorpage
    volumes:
        - ./errorpage:/usr/app/
    working_dir: /usr/app
    command: forever -w server.js
    labels:
        - traefik.enable=true
        - traefik.http.services.errorpage.loadbalancer.server.port=3000
        - traefik.http.routers.errorpage.rule=Host(`error.raphaelpiccolo.com`)
        - traefik.http.routers.errorpage.tls.certresolver=le
        - traefik.http.routers.errorpage.entrypoints=websecure
        - traefik.http.routers.errorpage.middlewares=securityheaders
    healthcheck:
        test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
        

add this in a service in which you want to replace the 404 page with the one from the errorpage service

- traefik.http.routers.d2cpartners.middlewares=securityheaders,redirect-to-nonwww,errorpage

custom header uuid

add this in command

--tracing.jaeger=true
--tracing.jaeger.samplingParam=0
--tracing.jaeger.traceContextHeaderName=X-Request-ID

TCP :

in this exemple we relay the mysql connection through traefik in plain unencrypted tcp/IP.
ps: we need one entrypoint for every tcp connection because we can't switch services by hostname like we do on http (unless we use tls)

traefik:
    ...
    command:
        - --entrypoints.mysql.address=:3306
    ports:
        - "3306:3306"

mysql:
    ...
    labels:
        - "traefik.enable=true"
        - "traefik.tcp.routers.mysql.rule=HostSNI(`*`)"
        - "traefik.tcp.services.mysql.loadbalancer.server.port=3306"
        - "traefik.tcp.routers.mysql.entrypoints=mysql"

TCP with tls :

ps: tls doesnt work because of a bug in mysql, but this is how it would work :

add this label on mysql

- "traefik.tcp.routers.mysql.tls=true"

or this one to let mysql deal with tls itself

  - "traefik.tcp.routers.mysql.tls.passthrough=true"

trusted ips and forwarded headers

when you make on request on traefik, it will add a header when it will call your service.
X-Forwarded-For, with the ip of the client initiating connection.
for exemple:

X-Forwarded-For: X.X.X.X

if you add this in commands :

- --entryPoints.web.forwardedHeaders.insecure
- --entryPoints.websecure.forwardedHeaders.insecure

and you set a custom X-forwarded-For before calling traefik, then X-forwarded-For will be the ip you specified, plus the real ip :

$> curl -H 'X-Forwarded-For: 1.1.1.1' https://xxx.com/
X-Forwarded-For: 1.1.1.1, X.X.X.X

you could also allow trust only your ip : only your ip (X.X.X.X) can set a X-Forwarded-For header.

--entryPoints.web.forwardedHeaders.trustedIPs=127.0.0.1/32,X.X.X.X

full exemple :

$> curl -H 'X-Forwarded-For: 1.1.1.1' https://xxx.com/
Hostname: 5c1b8960e103
IP: 127.0.0.1
IP: 172.21.0.46
RemoteAddr: 172.21.0.43:60138
GET / HTTP/1.1
Host: xxx.com
User-Agent: curl/7.70.0
Accept: */*
Accept-Encoding: gzip
X-Forwarded-For: 1.1.1.1, X.X.X.X
X-Forwarded-Host: xxx.com
X-Forwarded-Port: 443
X-Forwarded-Proto: https
X-Forwarded-Server: 00625e8b8443
X-Real-Ip: X.X.X.X
X-Request-Id: 276a624864bd6164:56bfa7b03c583401:4f64762abc3cc711:0

insecure ping service

add this

command:
    - --ping
    - --entrypoints.traefik.address=:8080
ports:
    - "8080:8080"

call this to check

curl http://X.X.X.X:8080/ping

https ping service

add this

command:
    - --ping
    - --ping.manualrouting=true
    - --ping.entryPoint=websecure

labels:
    - "traefik.http.routers.ping.rule=Host(`traefik.${DOMAIN}`) && PathPrefix(`/ping`)"
    - "traefik.http.routers.ping.service=ping@internal"
    - "traefik.http.routers.ping.middlewares=securityheaders"
    - "traefik.http.routers.ping.tls.certresolver=le"
    - "traefik.http.routers.ping.entrypoints=websecure"

call this to check

curl https://traefik.raphaelpiccolo.com/ping