OPNsense
OPNsense runs as a VM on Proxmox and handles routing, firewalling, and several network services for the homelab.
| Plugin / Service | Role |
|---|---|
| AdGuard Home | Network-wide DNS server with ad and tracker blocking |
| Unbound | Internal DNS resolver, upstream of AdGuard Home for local queries |
| dnsmasq | DHCP server |
| HAProxy | Reverse proxy for internal services |
| ACME client | Let's Encrypt certificate management |
| Tailscale | Subnet router for remote access |
DNS
DNS resolution is handled by a split setup, with AdGuard Home in front:
graph LR
client[Client] -->|:53| agh[AdGuard Home<br/>filtering + routing]
agh -->|DoT / DoH| quad9[Quad9<br/>internet queries]
agh -->|:5353| unbound[Unbound<br/>local + reverse queries] AdGuard Home runs as an OPNsense plugin and is the DNS server for all clients on the network, listening on the standard port 53. It filters ads and trackers at the DNS level, then routes queries by domain:
# Upstream DNS servers
tls://dns.quad9.net
https://dns.quad9.net/dns-query
[/tielinen.me/]127.0.0.1:5353
[/in-addr.arpa/]127.0.0.1:5353
# Private reverse DNS servers
127.0.0.1:5353
The [/domain/]server syntax is a conditional forwarding rule: those domains go to the listed upstream, everything else falls through to the default (Quad9). This gives a split-horizon setup where the same DNS server answers differently depending on the query:
| Query | Matches | Resolved by |
|---|---|---|
www.example.com | no domain rule | Quad9 over DoT / DoH — encrypted, ads filtered |
service.tielinen.me | [/tielinen.me/] | Unbound 127.0.0.1:5353 (local host overrides) |
| reverse PTR lookup | [/in-addr.arpa/] | Unbound 127.0.0.1:5353 (internal device names) |
Internet-bound queries stay encrypted and never leak to the ISP, while internal names resolve locally — so a hostname like service.tielinen.me points straight at HAProxy's LAN address instead of leaving the network.
It's worth being precise about what is and isn't public here, because the two halves are easy to conflate. The domain is public and has real DNS records, but they serve only two purposes: email, and proving control of the zone for certificate validation. The service hostnames beneath it have no public records at all — they exist only as host overrides in Unbound, so from the internet they don't resolve and there is nothing to connect to. Remote access reaches them through Tailscale, which puts the client on the inside of that split.
That combination is the whole point of validating by DNS: these names carry publicly trusted certificates while remaining unresolvable and unreachable from outside. The certificate authority verifies the zone, never the host — so a service can present a valid certificate without ever having accepted a connection from the internet.
Unbound handles internal name resolution. Internal services exposed through HAProxy are configured as host overrides in Unbound, pointing their hostnames to HAProxy's address.
dnsmasq handles DHCP and pushes AdGuard Home's address to clients as their DNS server.
Certificates
The ACME client fetches Let's Encrypt certificates automatically. Certificates are used in two places:
- OPNsense itself — for the web UI and other OPNsense services
- HAProxy — for TLS termination of internal services
Why the DNS-01 challenge
None of these services is reachable from the internet — the WAN side is default-deny with nothing forwarded — which rules out HTTP-01 validation, because that requires Let's Encrypt to fetch a file over inbound port 80. Validation therefore uses the DNS-01 challenge: domain control is proven by writing a TXT record into the zone rather than by serving anything, so a host that never accepts a connection from the internet can still hold a publicly trusted certificate.
The domain is registered and its DNS hosted at Cloudflare, so the challenge is answered through Cloudflare's API — which means OPNsense holds a token able to write DNS records for the zone. That's real authority to keep on the router, and it's the price of not opening a port. Worth being explicit about rather than leaving implied by "certificates are fetched automatically", and worth scoping the token as narrowly as the provider allows.
It also concentrates a dependency worth naming: the same Cloudflare account registers the domain, hosts the zone that proves certificate ownership, and deploys this site. Three distinct roles behind one login. That's a reasonable trade for a homelab — it's one less system to operate, which is the same argument made for the deployment pipeline — but it's a single point of failure, and the honest version of "fewer moving parts" is admitting which part everything now moves around.
A renewal that succeeds without taking effect
A renewed certificate does not serve itself. Unless the ACME automation includes a restart action for the service holding it, HAProxy carries on presenting the old certificate from memory while the new one sits on disk — and every log line reports a successful renewal. It's a silent failure with a 90-day fuse.
That case is covered by measurement rather than by remembering. Blackbox reads the certificate actually presented on the wire, so SSLCertExpiringSoon (probe_ssl_earliest_cert_expiry - time() < 86400 * 14) sees what a client would see, not the fresh file the renewal wrote. A renewal that succeeded but never loaded still trips the alert a fortnight before expiry. Same principle as the backup canary: assert on the artifact the user receives, not on the exit status of the job that produced it.
HAProxy
HAProxy acts as a reverse proxy for internal services, terminating HTTPS using the Let's Encrypt certificates from the ACME client. This allows internal services to be reached over HTTPS with valid certificates without exposing them to the internet.
Tailscale
Tailscale runs as a subnet router, making the entire homelab network accessible remotely through the Tailscale VPN. This avoids opening any ports to the internet — remote access goes through Tailscale's encrypted tunnel instead.
My experience
I had previously run OPNsense on a dedicated hardware appliance, so the software itself was familiar. For the Proxmox VM installation I followed a ServeTheHome guide and it went smoothly. The part that required the most care was mapping the network interfaces correctly between Proxmox and OPNsense — getting that wrong would mean no connectivity at all.
Before AdGuard Home I ran Pi-hole as an LXC container on Proxmox. Switching to the AdGuard Home plugin inside OPNsense reduced maintenance and felt like a more modern solution. The split DNS configuration took some work to get right, but I got there with a combination of AI assistance, documentation and trial and error.
HAProxy and Let's Encrypt certificates have worked well. One thing to remember is to add a service restart action to the ACME automation so that renewed certificates actually take effect. HAProxy configuration is quite manual, though — a declarative reverse proxy would be easier to maintain.
Tailscale runs as a subnet router on OPNsense. It works well for a homelab where access control isn't a major concern.