DEV Community

Cover image for One wildcard cert and a script: local domains for every app you run
Ron Northcutt
Ron Northcutt

Posted on

One wildcard cert and a script: local domains for every app you run

NOTE: Yes, I created a shell script that will do all of this for you, so you can be lazy (see below). But I still recommend doing it at least once by hand. It's a great way to learn (or remember) and it's pretty quick. Learn it by hand once, and then get lazy.

Part one got one local app onto a local domain with a green padlock. That setup has a ceiling, and you hit it fast: every new app means a new cert, a new ACL, a new backend, and another trip through the same four commands.

This post removes most of that. One cert covers every .omni domain you'll ever create, the config pattern scales to as many apps as you want to run, and a script collapses the whole thing into one command.


One wildcard cert for everything

Generating a cert per app works, but you can skip that step forever with a single wildcard cert:

mkcert -cert-file ~/.config/haproxy/certs/omni-wildcard.crt \
       -key-file ~/.config/haproxy/certs/omni-wildcard.key \
       "*.omni"

cat ~/.config/haproxy/certs/omni-wildcard.crt \
    ~/.config/haproxy/certs/omni-wildcard.key \
    > ~/.config/haproxy/certs/omni-wildcard.pem
Enter fullscreen mode Exit fullscreen mode

Point the crt line at omni-wildcard.pem and every app.omni you add is already trusted. Adding a new app drops to three steps: hosts entry, ACL + backend, reload. No new cert, ever.

That next app is two lines in the frontend and two in a new backend:

    acl is_work req.hdr(host),host_only -i work.omni
    use_backend work_backend if is_work

backend work_backend
    server loopback 127.0.0.1:3000
Enter fullscreen mode Exit fullscreen mode

Two things:
1) Name the pem something generic like omni-wildcard.pem, since it now belongs to the whole setup instead of one app.
2) The wildcard only matches one label deep: *.omni covers omnideck.omni and work.omni, but not api.omnideck.omni. If you nest subdomains, add them to the cert explicitly.


Putting it together: three instances at once

Here's what this actually looks like day to day. I run three Omnideck instances with different configs and different model backends: one for work, one for personal projects, one for breaking things.

Hosts entries first, one per instance, since the hosts file has no wildcards:

sudo tee -a /etc/hosts <<'EOF'
127.0.0.1 work.omni
127.0.0.1 home.omni
127.0.0.1 lab.omni
EOF
Enter fullscreen mode Exit fullscreen mode

The wildcard cert from above covers all three. And the whole config, start to finish:

global
    log stdout format raw local0

defaults
    log global
    mode http
    option httplog
    option forwardfor
    timeout connect 5s
    timeout client  1h
    timeout server  1h
    timeout tunnel  1h

frontend http_in
    bind *:80
    tcp-request connection reject if !{ src 127.0.0.0/8 ::1 }
    http-request redirect scheme https code 301

frontend https_in
    bind *:443 ssl crt /Users/you/.config/haproxy/certs/omni-wildcard.pem alpn h2,http/1.1

    tcp-request connection reject if !{ src 127.0.0.0/8 ::1 }

    http-request set-header X-Forwarded-Proto https

    acl is_work req.hdr(host),host_only -i work.omni
    acl is_home req.hdr(host),host_only -i home.omni
    acl is_lab  req.hdr(host),host_only -i lab.omni

    use_backend work_backend if is_work
    use_backend home_backend if is_home
    use_backend lab_backend  if is_lab

backend work_backend
    server loopback 127.0.0.1:46176

backend home_backend
    server loopback 127.0.0.1:46177

backend lab_backend
    server loopback 127.0.0.1:46178
Enter fullscreen mode Exit fullscreen mode

Check it, reload, and confirm all three:

haproxy -c -f $(brew --prefix)/etc/haproxy.cfg
brew services restart haproxy

for host in work home lab; do
  curl -s -o /dev/null -w "$host.omni -> %{http_code}\n" "https://$host.omni"
done
Enter fullscreen mode Exit fullscreen mode

Three 200s and you're done. A 503 means the request reached HAProxy but no ACL matched, which is usually a typo in the hosts file or a missing use_backend line. A connection refused means the instance behind that port isn't running.

NOTE: the tcp-request connection reject lines carry over from part one, and they matter more here. Three unauthenticated instances behind a wildcard bind is three times the surface on conference wifi. See the FAQ if you want the stricter version.


Automate it with a script

Doing this by hand for every new app still gets repetitive. This script wraps the hosts entry, the cert generation, and the HAProxy reload into one command:

👉 GitHub Gist: add-omni-domain.sh

Usage:

./add-omni-domain.sh omnideck.omni 46176
Enter fullscreen mode Exit fullscreen mode

It takes a slightly different route on certs than the wildcard above. Instead of one cert for everything, it generates a cert per domain and points HAProxy's crt at the whole directory, letting SNI pick the right one per request. Same outcome, no config edit either way.

NOTE: This script requires sudo, so don't trust it blindly. Read it first, or at least have AI check it before you run it. Better yet, use it as a guide and build your own. Put in your own responses, make the emojis better, and extend it to do other cool things.


What else?

That's the whole setup. Local apps answer to real domain names with HTTPS, and adding the next one takes a single command. I'm actually thinking about adding it as an optional step for the omnideck cli installer, but I'm not sure. Thoughts?


FAQ

Can HAProxy route wildcard .omni domains?

Yes. Generate a wildcard cert with mkcert "*.omni", point crt at the combined .pem, and match the Host header with an ACL to send each subdomain to its own backend.

Can I stop editing /etc/hosts for every app?

Yes, with dnsmasq. Install it via brew, add address=/.omni/127.0.0.1 to its config, and on macOS create /etc/resolver/omni containing nameserver 127.0.0.1. Every .omni name then resolves without a hosts entry, and adding an app drops to ACL, backend, reload. Pair it with the wildcard cert and new apps cost you four lines of config.

Is the reject rule enough, or should I bind to loopback?

The reject rule is enough for a laptop you carry around. The port stays open and answers nothing, which is the practical outcome you want.

If you'd rather the port never open at all, bind the interface instead of the wildcard:

    bind 127.0.0.1:443 ssl crt /Users/you/.config/haproxy/certs/omni-wildcard.pem alpn h2,http/1.1
    bind [::1]:443 ssl crt /Users/you/.config/haproxy/certs/omni-wildcard.pem alpn h2,http/1.1
Enter fullscreen mode Exit fullscreen mode

On Linux the sysctl from part one already covers this. On macOS it won't work as your user, because Mojave's low-port exception applies only to wildcard binds. You'd need sudo brew services start haproxy, which installs HAProxy as a system daemon rather than a user agent. That's a real tradeoff for a dev box, which is why the reject rule is the default in these posts.

Do I have to restart HAProxy every time I add an app?

brew services restart is a full stop and start, so in-flight connections drop. On a dev box that's usually fine. If you're mid-stream on something you care about, haproxy -sf $(pgrep haproxy) reloads with the new config and lets the old process finish what it started.

My agent run dies partway through. Is that the proxy?

Probably, if it dies at a suspiciously round interval. HAProxy's default 50s client and server timeouts cut off idle SSE and WebSocket connections, and a streaming run looks idle to the proxy between chunks. The 1h timeouts in the config above, timeout tunnel especially, are there for exactly this.


Photo by Homa Appliances on Unsplash

Top comments (0)