blog

· 3 min read

One front door for every subdomain

#self-hosting#docker#linux

on this page
  1. The overall shape
  2. Routes as ordinary files
  3. One certificate for almost everything
  4. The catch-all route
  5. Traffic that can't use the front door
  6. Barricading the side entrances

Several unrelated applications share one server under xenyc.ge: this website, the panel, a collaborative map, a timer, a vault for pasted text and files, and a few experiments. They run in separate containers, written in different languages by different versions of me, yet every visitor arrives through the identical entrance. This is an anatomy of that entrance, including the details that caught me off guard.

The overall shape

visitor ──► Cloudflare ──► Traefik (:443) ──► matching container
                              │
                              └─► unrecognised name? ──► easter egg

Cloudflare occupies the outermost layer, handling DNS, caching and the concealment of the server's genuine address. Traefik is the only process on the machine listening on ports 80 and 443. It examines the hostname of each incoming request and forwards it across Docker's internal network to the corresponding container. No individual application publishes a port to the internet on its own.

Routes as ordinary files

Traefik can discover services by reading Docker labels, but I prefer declaring routes inside YAML files through what Traefik calls the file provider. Traefik monitors that directory and reloads its configuration the instant something changes, so introducing a new website means appending a few lines rather than restarting anything:

dynamic/sites.yml
http:
  routers:
    xenyc-sites:
      entryPoints: [websecure]
      rule: "Host(`xenyc.ge`) || Host(`www.xenyc.ge`) || Host(`map.xenyc.ge`)"
      priority: 60
      service: sites
      tls: {}

Labels function adequately when an individual maintains a solitary compose file. Once several independent projects depend on a shared proxy, a directory of compact, single-purpose files becomes far easier to audit at a glance, and far easier to include in a backup.

One certificate for almost everything

Traffic between Cloudflare and the server is encrypted with a Cloudflare Origin Certificate, which costs nothing, accepts wildcard names and remains valid for up to fifteen years. A single *.xenyc.ge certificate covers every subdomain, so a newborn application acquires HTTPS the moment it acquires a route.

Here's the subtlety that catches almost everybody. A wildcard certificate doesn't cover the bare domain. The pattern *.xenyc.ge matches map.xenyc.ge but never xenyc.ge itself, so the apex requires a separate entry or a certificate enumerating both names. Traefik selects the appropriate certificate by inspecting the SNI field of each connection:

yaml
tls:
  stores:
    default:
      defaultCertificate:
        certFile: /etc/traefik/certs/default.crt   # *.xenyc.ge
        keyFile: /etc/traefik/certs/default.key
  certificates:
    - certFile: /etc/traefik/certs/xenyc-apex.crt  # xenyc.ge
      keyFile: /etc/traefik/certs/xenyc-apex.key

Configure Cloudflare's encryption mode as Full (strict). Anything weaker accepts whatever certificate the origin presents, which defeats the purpose of installing one.

The catch-all route

What happens when somebody types banana.xenyc.ge? A wildcard DNS record delivers that request to the server regardless. Instead of an anonymous 404, a router matching any subdomain waits at the lowest conceivable priority:

yaml
catchall:
  entryPoints: [websecure]
  rule: "HostRegexp(`^[a-z0-9-]+\\.xenyc\\.ge$`)"
  priority: 1
  service: easter
  tls: {}

Every legitimate application carries a higher priority and always prevails. Whatever remains unclaimed lands on a small page I won't spoil here. Invent a subdomain and discover it yourself.

Traffic that can't use the front door

Cloudflare's proxy forwards nothing except web traffic, and a couple of services communicate through other protocols. SFTP, for uploading files to the panel, is one; the panel's remote-node agents, which authenticate with their own mutual TLS, are another. Those receive DNS-only records (the grey cloud icon) and dedicated ports, and they must perform their own authentication, since no intermediary filters anything on their behalf.

One web service retains its own certificate too. The vault terminates TLS inside its own container, so Traefik routes it with TLS passthrough: it reads the requested hostname from the handshake and relays the encrypted stream untouched, never observing its contents.

Barricading the side entrances

Should the server's genuine address ever leak, an attacker could circumvent Cloudflare altogether. Two complementary measures close that gap. The firewall accepts connections on ports 80 and 443 from nowhere except Cloudflare's published IP ranges. Beyond that, authenticated origin pulls require Cloudflare to present a client certificate during every handshake, and Traefik rejects any connection lacking one.

With both defences operating in tandem, the front door becomes the sole practical avenue toward any website hosted on this machine.