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:


Tech Stack


DNS Setup

On my main provider, I pointed A records at the OpenBSD relay. Nothing exotic:

  • media.example.com → Plex/Jellyfin
  • matrix.example.com → Synapse
  • rss.example.com → Miniflux
  • later.example.com → Shiori
  • example.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.


Next posts in this series