Valid TLS cert for local services with Traefik
I run an Unraid server locally. Remembering ports for each and every service is not going to happen. Remembering names is much easier. Thus a reverse proxy was the way to go. As a starter and for some sane defaults, I followed the Traefik guide by IBRACORP.
A few weeks ago, I had another look into Traefik for my setup. The interesting part: it integrates with Docker. Instead of updating a configuration file and restarting the server, traefik does these things automatically by monitoring labels attached to Docker containers. On the plus side, it also gets TLS certificates automatically via Let’s Encrypt.
Running traefik as a container itself works perfectly fine:
docker run -d --name=traefik --net=bridge
-e CF_DNS_API_TOKEN=xxx
-l traefik.http.routers.api.rule=Host(`traefik.domain.com`)
-l traefik.http.routers.api.entryPoints=https
-l traefik.http.routers.api.service=[email protected]
-l traefik.enable=true
-p 443:443/tcp
-p 80:80/tcp
-p 8183:8080/tcp
-v /mnt/user/appdata/traefik:/etc/traefik:rw
-v /var/run/docker.sock:/var/run/docker.sock:r
traefik:latest
Simply adding a traefik.enable=true
label to a container will make Traefik aware of it. By adding the traefik.http.routers.api.rule=Host(`traefik.domain.com`)
and traefik.enable=true
label to the Traefik container, I get access to Traefik’s dashboard which may come in handy for monitoring everything.
Traefik tries to determine the port for proxying traffic automatically but on some occasions, a container exposes more than one port. To tell Traefik
which port it should use, the following label can be assigned to a container: traefik.http.services.example.loadbalancer.server.port=31338
.
In order to request certificates, Traefik has to perform the ACME challenge. To do so, it integrates with several providers to set the required TXT DNS records. Credentials can be passed as environment variables while the rest of the configuration can be found below. This method is required for all wildcard certificates and since I put most of my servers behind Cloudflare anyway, using its API and DNS configuration was the way to go for me. Since I have a local DNS Server, my domain doesn’t even have any A/AAAA records. Nothing from the internet points to it. The domain’s sole purpose is to enable me to use Let’s Encrypt and no self-signed certificates based on a root cert that I then have to install on all my devices.
The following snippet contains the traefik.yml
configuration file. It applies some basic settings to every “host”. Settings like default entrypoints for
http and https. Due to this config, it will perform an auto-redirect from http to https as well. It also enables the Docker provider and uses
Go templates to automatically generate hostnames for every container based on the container’s name unless overwritten
by the parameter mentioned above.
Since not all services run on my server via Docker, a file-based config can be used as well. I use it for my 3D printers and for the Unraid server’s dashboard.
Beside that it also configures the provider for the ACME challenge. Due to the entrypoint configuration, it only generates a certificate for domain.com
and *.domain.com
.
So basically what I want, because that way I am not exposing anything via certificate transparency. Even though none of my services are exposed
to the internet anyway.
global:
checkNewVersion: true
sendAnonymousUsage: false
serversTransport:
insecureSkipVerify: true
entryPoints:
http:
address: :80
http:
redirections:
entryPoint:
to: https
scheme: https
https:
address: :443
http:
tls:
certResolver: letsencrypt
domains:
- main: domain.com
sans:
- '*.domain.com'
middlewares:
- [email protected]
providers:
providersThrottleDuration: 2s
file:
watch: true
filename: /etc/traefik/fileConfig.yml
docker:
watch: true
defaultRule: "Host(`{{ lower (trimPrefix `/` .Name )}}.domain.com`)"
exposedByDefault: false
api:
dashboard: true
insecure: true
log:
level: INFO
certificatesResolvers:
letsencrypt:
acme:
email: [email protected]
storage: /etc/traefik/acme.json
dnsChallenge:
provider: cloudflare
resolvers:
- "1.1.1.1:53"
- "1.0.0.1:53"
As mentioned above, here’s the fileConfig.yml
, which is being loaded as a file provider by Traefik. It’s configured to auto-reload the file in case of changes.
As an example I left my Unraid’s server snippet in there. It also provides some sane defaults for TLS and some HTTP security headers. A lot more
info about that can be found on Scott Helme’s Blog.
http:
routers:
unraid:
entrypoints:
- https
rule: 'Host(`unraid.domain.com`)'
service: unraid
middlewares:
- "securityHeaders"
services:
unraid:
loadBalancer:
servers:
- url: http://192.168.0.7:8080/
middlewares:
securityHeaders:
headers:
customResponseHeaders:
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
X-Forwarded-Proto: "https"
server: ""
customRequestHeaders:
X-Forwarded-Proto: "https"
sslProxyHeaders:
X-Forwarded-Proto: "https"
referrerPolicy: "same-origin"
hostsProxyHeaders:
- "X-Forwarded-Host"
contentTypeNosniff: true
browserXssFilter: true
forceSTSHeader: true
stsIncludeSubdomains: true
stsSeconds: 63072000
stsPreload: true
tls:
options:
default:
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
So, to sum things up. My local DNS server points *.domain.com
to my unraid server. So all subdomains will resolve
to only that server unless configured otherwise like for my 3D printers for example. Traefik is listening on prot
80
and 443
on my server. Thus requests like foobar.domain.com
will go to Traefik. If there’s a container named
foobar
, Traefik will proxy the requests to that container.
This setup even works in Tailscale thanks to Global DNS configured to my local DNS.