I need to do some additional networking to ensure all the services I run on my home lab are reachable with a unique name.
The app.nooney.casa
Subdomain
So far I’ve reserved the host.nooney.casa
subdomain to represent hostnames for
individual machines. I want to use a different subdomain to reach all of my
services. For these services, I’ll use the app.nooney.casa
subdomain.
Since the services could run on any machine in the docker swarm, I need to set up a reverse proxy to route requests to each service. Traefik Proxy is suitable for this purpose.
In order to reach the network reliably, I run the Traefik service on my NUC,
sirius.host.nooney.casa
. This allows me to set a wildcard DNS record for
app.nooney.casa
in pfSense which points to the Traefik service. This will also
be the only service that exposes ports on my home lab; every other service must
run behind this proxy. The diagram below explains this setup in more detail.
In pfSense, I followed the documentation to set up the following custom options in the DNS Resolver service:
server:
local-zone: "app.nooney.casa" redirect
local-data: "app.nooney.casa 86400 IN A <ip_address>"
In the option above, I replace <ip_address>
with the static IP address for my
NUC. Now all requests to *.app.nooney.casa
redirect to the NUC.
Setting up HTTPS
I don’t want to deal with browser warnings regarding insecure websites, so I need to get a certificate for my domain. The Automated Certificate Management Environment (ACME) protocol is widely supported for obtaining certificates.
Based on the physical network layout, I want a top level certificate for
nooney.casa
, a wildcard certificate *.nooney.casa
, and a wildcard
certificate for my services, *.app.nooney.casa
. Any domains matching one of
these URL patterns will successfully validate in a browser.
pfSense supports a plugin to perform the ACME protocol to obtain a certificate for the network. There are three challenge types to obtain a certificate: HTTP, DNS, and TLS-ALPN. More information about each challenge type can be viewed in the Let’s Encrypt docs.
The HTTP challenge requires a web server to be reachable via the internet. Since I don’t expose any ports on my home network, I can’t use this challenge type. Therefore, I opted to use the DNS challenge.
The DNS challenge requires placing a TXT record in the DNS entry under the
hostname that the certificate is for. I purchased my domain through Namecheap;
however, they don’t provide a good API to update DNS entries programmatically.
Cloudflare does though! I set up a Cloudflare account and added nooney.casa
site. In the Namecheap interface, I updated the nameservers to point to my
assigned Cloudflare servers.
I then configured the ACME client in pfSense to use my Cloudflare credentials to update the TXT record required by the DNS challenge. With all that in place, I obtained the certificates I needed! I also added a post-certificate action in pfSense to copy the certificate to a shared location on my home lab so that each of my machines can access the certificate.
Traefik Proxy
Traefik Proxy is the service that I run in front of all other services. It will
route incoming requests to the relevant services. Configuration for this is
managed via the docker-compose.yml
file for each service. The nice part is
that Traefik itself is just another service, and can be configured in the same
manner as all other services.
Below are some relevant settings used to start the Traefik service (from the
Traefik docker-compose.yml
):
# filename: docker-compose.yml
version: "3.8"
networks:
traefik-public:
external: true
volumes:
traefik:
driver: local-persist
driver_opts:
mountpoint: ${VOLUME_PATH}
services:
traefik:
image: "traefik:latest"
environment:
- CERTIFICATE_NAME
- CERTIFICATE_PATH
command:
- "--log.level=DEBUG"
- "--api=true"
- "--api.dashboard=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.web.http.redirections.entryPoint.to=websecure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.web.http.redirections.entrypoint.permanent=true"
- "--entrypoints.websecure.address=:443"
- "--providers.docker=true"
- "--providers.docker.network=traefik-public"
- "--providers.docker.exposedByDefault=false"
- "--providers.file.directory=${CONFIG_PATH}"
- "--providers.file.watch=true"
networks:
- traefik-public
ports:
- "80:80"
- "443:443"
volumes:
- "/var/run/docker.sock:/var/run/docker.sock:ro"
- "${CERTIFICATE_PATH}:${CERTIFICATE_PATH}:ro"
- "traefik:${CONFIG_PATH}:ro"
labels:
- "traefik.enable=true"
- "traefik.http.routers.dashboard.rule=Host(`${FQDN}`)"
- "traefik.http.routers.dashboard.entrypoints=websecure"
- "traefik.http.routers.dashboard.tls=true"
- "traefik.http.routers.dashboard.service=api@internal"
- "traefik.http.routers.dashboard.middlewares=lan"
- "traefik.http.middlewares.lan.ipwhitelist.sourcerange=${IPWHITELIST_RANGE}"
- "traefik.http.routers.dashboard.middlewares=auth"
- "traefik.http.middlewares.auth.basicauth.users=${USERPASS}"
I set up an external Docker network called traefik-public
. Keeping the network
separate allows me to upgrade Traefik without bringing down the entire network.
I expose the standard HTTP and HTTPS ports for Traefik, and use the commands to
set up the entrypoints. In this scheme, I always redirect HTTP to HTTPS, so none
of my services have to worry about establishing HTTPS. I use the
providers.file.directory
option and a file called certificates.yml
to let
Traefik use the certificate I obtained in the previous section. I add
CERTIFICATE_NAME
and CERTIFICATE_PATH
to the environment section so that
I can refer to them from certificates.yml
.
# filename: certificates.yml
tls:
certificates:
- certFile: "{{ env "CERTIFICATE_PATH" }}/{{ env "CERTIFICATE_NAME" }}.fullchain"
keyFile: "{{ env "CERTIFICATE_PATH" }}/{{ env "CERTIFICATE_NAME" }}.key"
stores:
- default
stores:
default:
defaultCertificate:
certFile: "{{ env "CERTIFICATE_PATH" }}/{{ env "CERTIFICATE_NAME" }}.fullchain"
keyFile: "{{ env "CERTIFICATE_PATH" }}/{{ env "CERTIFICATE_NAME" }}.key"
The labels section of the docker-compose.yml
file configures Traefik’s
dashboard, which I can use to visualize my routes and diagnose any failed
configurations.
To start the service, I ran the following command from my workstation:
# Start the stack
docker-compose config | docker -H ssh://docker.nooney.casa stack deploy -c - traefik
Once the command completed, I visited the dashboard to confirm that Traefik was
running. With all of this set up, I can begin running services under
app.nooney.casa
and serve them over HTTPS!