Skip to content

Exposing this beyond localhost

Run with python3 -m backend.main or make dev and the app only answers requests from the machine it’s running on (defaults to binding 127.0.0.1). make serve — the project’s own without---reload run command — binds every network interface instead (0.0.0.0), reachable from any other device on your LAN the moment it starts, with no separate “expose” step. make daemon-start is the same story: it loads a launchd service (~/Library/LaunchAgents/ai.opennetworth.backend.plist) that runs that exact LAN-bound command — the Makefile’s own comment on the serve target says so directly. This plist is your own machine’s local state, not something this repo ships or generates, so if you wrote it yourself with a different invocation, its actual bind host is whatever you put in it — but the project’s own convention for it is LAN-bound, same as make serve, not 127.0.0.1. Read this page either way: this app holds your complete financial picture behind a single password, and reachability beyond your own machine changes what protects it.

What protects you today, and what doesn’t

Out of the box, the app is protected by exactly one thing: your instance password. No built-in encryption in transit, no second factor, no per-device approval — anyone who has (or guesses) that password, and can reach the port the app is listening on, is in.

  • The app itself doesn’t speak HTTPS. It’s a plain HTTP server. Expose it directly — port- forwarded on your router, or bound to a public interface with nothing in front of it — and your password and session cookie travel over the network in plain text, readable by anything positioned to see that traffic.
  • Login lockout resets on restart. Five wrong attempts from the same address locks further attempts out for 15 minutes, but that counter lives in memory — restarting the backend clears it.
  • There’s no IP allowlist or VPN built in. Reachability is entirely about what network the app’s port is exposed to; the app itself doesn’t restrict who can attempt to log in from wherever it’s reachable.

None of this is a bug — it’s the honest state of a self-hosted, single-operator app that assumes you’re the one deciding how it’s reached.

What to put in front of it before you expose it

Put a reverse proxy with real TLS in front of the app, or reach it over a VPN/tunnel — don’t bind it directly to a public interface. One of:

  • A reverse proxy (Caddy, nginx, Traefik) terminating HTTPS with a real certificate, forwarding to the app locally.
  • A private tunnel (Tailscale, WireGuard, or similar), so the app is only ever reachable over a network you control, never the open internet.

Either way, your password and session cookie are encrypted in transit and the app never directly faces an untrusted network.

The one setting a same-machine reverse proxy makes critical

By default, the app trusts any request reaching it from the machine it’s running on — the convenience that skips login for local use. A reverse proxy you run on that same machine connects to the backend from that same machine, so — unless you turn this off — every request it forwards, including ones that started on the public internet a moment earlier, is trusted exactly as if you’d typed the URL on the server’s own keyboard. No password is asked for at all. This is the single most important setting on this page: a reverse proxy is not, by itself, a login screen.

Before putting any reverse proxy in front of this app on the same host, turn this trust off (set AUTH_TRUST_LOOPBACK=false and restart) so the app checks a real password/session for every forwarded request, exactly like any remote client. Leave it on only if the app truly never receives traffic from anywhere but your own logged-in shell on that box — a private tunnel (Tailscale, WireGuard) terminating as a normal network interface has the same consideration: check where the connection the app actually sees comes from, not just what reaches your router.

If you loosen CORS

The app’s cross-origin policy defaults to two local development addresses — nothing else can make browser-based requests to it out of the box. Serving the frontend from a different origin than the backend (a real production layout) means widening this to your own domain. Avoid opening it to every origin (*) — a session cookie isn’t sent on a cross-origin request under that setting anyway, so opening it wide buys nothing while loosening a boundary that costs nothing to leave narrow.

The honest summary

Treat the instance password the way you’d treat the one credential standing between the internet and your bank statements — because that’s what it is here. TLS via a reverse proxy or a private network is not optional the moment this leaves your own machine; it’s the one piece the app deliberately leaves to you.

  • operations/security-and-privacy — what’s protected already, and what data exists
  • operations/multi-device — reaching your instance from more than one device, once exposed safely