Service Network

Homelab

thumbnail

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!