Living out in the boonies has its charms: quiet nights, open skies, and an estate that keeps me busy, but internet choices aren’t one of them. Options are slim, and Carrier-Grade NAT (CGNAT) makes life rough if you want to self-host.
Static, routable IPs are what you really need, but out here that’s a luxury. I didn’t want to rely on Cloudflare tunnels, ngrok, or similar middlemen. For a while, I leaned on Tailscale as a DIY SD-WAN (basically a secure mesh network overlay across your devices). It’s great, but not every service or device plays nicely over it.
OpenBSD has been near and dear to me for decades, and its philosophy always made sense. Logical. Careful. The kind of software you trust. So naturally, I turned to it for this project.
Problem Statement
Allow apps to broadly hit internal services using DNS relaying with a proper TLS endpoint.
Ancillary Benefits
This setup helps me move off a handful of third-party services:
- Tailscale for networking
- Dreamhost Jekyll Blog
- Feedly for RSS
- Beeper for messaging
- The now-defunct Pocket

Tech Stack
- Wireguard on Linux (internal server)
- Wireguard on OpenBSD (external endpoint)
- relayd(8) for TLS termination and proxying
- ACME LetsEncrypt for certs
- httpd(8) for my Hugo blog
- Synapse Matrix Server with bridges for Meta, Twitter, Gmessages, WhatsApp
- Miniflux for RSS
- Shiori to replace Pocket
- ReactFlux for self-hosted reading
- Plex/Jellyfin for media
DNS Setup
On my main provider, I pointed A records at the OpenBSD relay. Nothing exotic:
media.example.com
→ Plex/Jellyfinmatrix.example.com
→ Synapserss.example.com
→ Minifluxlater.example.com
→ Shioriexample.com
→ Hugo blog
Since OpenBSD ships with relayd
and acme-client
in base, I could
configure TLS and relaying without extra packages.
Configuration
/etc/acme-client.conf
authority letsencrypt {
api url "https://acme-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-privkey.pem"
}
authority letsencrypt-staging {
api url "https://acme-staging-v02.api.letsencrypt.org/directory"
account key "/etc/acme/letsencrypt-staging-privkey.pem"
}
domain example.com {
alternative names { www.example.com }
domain key "/etc/ssl/private/example.com.key"
domain full chain certificate "/etc/ssl/example.com.crt"
sign with letsencrypt
}
domain media.example.com {
domain key "/etc/ssl/private/media.example.com.key"
domain full chain certificate "/etc/ssl/media.example.com.crt"
sign with letsencrypt
}
domain rss.example.com {
domain key "/etc/ssl/private/rss.example.com.key"
domain full chain certificate "/etc/ssl/rss.example.com.crt"
sign with letsencrypt
}
domain matrix.example.com {
domain key "/etc/ssl/private/matrix.example.com.key"
domain full chain certificate "/etc/ssl/matrix.example.com.crt"
sign with letsencrypt
}
domain later.example.com {
domain key "/etc/ssl/private/later.example.com.key"
domain full chain certificate "/etc/ssl/later.example.com.crt"
sign with letsencrypt
}
/etc/relayd.conf
# Hosts
table <acme> { 127.0.0.1 }
acme_port="8080"
table <www> { 127.0.0.1 }
www_port="8080"
table <plex> { 172.16.0.2 }
plex_port="33400"
table <matrix> {127.0.0.1}
matrix_port="8008"
table <miniflux> {127.0.0.1}
miniflux_port="7777"
table <shiori> {127.0.0.1}
shiori_port="8888"
log state changes
log connection
http protocol "http" {
match header log "Host"
match header log "X-Forwarded-For"
match header log "User-Agent"
match header log "Referer"
match url log
match request header set "X-Forwarded-For" value "$REMOTE_ADDR"
match request header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
tcp { nodelay, socket buffer 65536, backlog 100 }
block path "/cgi-bin/index.cgi" value "*command=*"
pass request quick path "/.well-known/acme-challenge/*" forward to <acme>
block request
}
http protocol "https" {
match header log "Host"
match header log "X-Forwarded-For"
match header log "User-Agent"
match header log "Referer"
match url log
match header set "X-Forwarded-For" value "$REMOTE_ADDR"
match header set "X-Forwarded-By" value "$SERVER_ADDR:$SERVER_PORT"
match header set "X-Forwarded-Proto" value "https"
match header set "Keep-Alive" value "$TIMEOUT"
match request path "/.well-known/matrix/*" tag "matrix-cors"
match response tagged "matrix-cors" header set "Access-Control-Allow-Origin" value "*"
pass quick path "/_matrix/*" forward to <matrix>
pass quick path "/_synapse/client/*" forward to <matrix>
tcp { nodelay, socket buffer 65536, backlog 100 }
tls no tlsv1.0
tls ciphers "HIGH"
tls keypair example.com
tls keypair matrix.example.com
tls keypair media.example.com
tls keypair rss.example.com
tls keypair later.example.com
pass request quick header "Host" value "example.com" forward to <www>
pass request quick header "Host" value "rss.example.com" forward to <miniflux>
pass request quick header "Host" value "later.example.com" forward to <shiori>
pass request quick header "Host" value "media.example.com" forward to <plex>
pass request quick header "Host" value "matrix.example.com" forward to <matrix>
block request
}
http protocol "matrix" {
tls { no tlsv1.0, ciphers "HIGH" }
tls keypair matrix.example.com
block
pass quick path "/_matrix/*" forward to <matrix>
pass quick path "/_synapse/client/*" forward to <matrix>
}
relay "matrix_federation" {
listen on egress port 8448 tls
protocol "matrix"
forward to <matrix> port $matrix_port check tcp
}
relay "http_proxy" {
listen on 46.23.94.100 port 80
protocol "http"
forward to <acme> port $acme_port
forward to <www> port $www_port
}
relay "https_proxy" {
listen on 46.23.94.100 port 443 tls
protocol "https"
forward to <plex> port $plex_port
forward to <matrix> port $matrix_port
forward to <miniflux> port $miniflux_port
forward to <shiori> port $shiori_port
forward to <www> port $www_port
}
/etc/httpd.conf
types { include "/usr/share/misc/mime.types" }
server "example.com" {
listen on 127.0.0.1 port 8080
root "/htdocs/geekyschmidt.com"
gzip-static
location "/.well-known/acme-challenge/*" {
root "/acme"
request strip 2
}
}
server "www.example.com" {
listen on 127.0.0.1 port 8080
block return 301 "$HTTP_HOST$REQUEST_URI"
log style forwarded
}
server "example.com" {
alias "www.example.com"
listen on * port 80
block return 301 "$HTTP_HOST$REQUEST_URI"
}
Notes
- Relay tables keep things tidy and let you redirect to local services behind CGNAT.\
- Some services (Plex in particular) don’t love strict header rules—relax as needed.\
- Remember to tell Plex about your new DNS name in plex.tv so apps and TVs trust it.

Closing
This setup scratched the itch: I can finally host internal services in a way that feels first-class without leaning on external tunnels. OpenBSD’s “batteries included” approach made it straightforward once I pieced together the configs.
I’ll cover the individual services in separate posts—because each deserves its own war story.