pr0xteus: Disposable VPN Exits for Apps You Wouldn’t Trust With Your Password

You’ve got some app that needs to leave the building through a VPN. A scraper, a browser bot, some agent poking at sites that would rather it came from somewhere else. And you’ve got three ways to make that happen, all of them stupid.
One: turn the whole host into a VPN client. Congratulations, now your SSH session, your monitoring, your package updates and every other thing on that box also go out through the tunnel, and the first time WireGuard hiccups you’ve locked yourself out of your own server. Two: hand the app your VPN provider login. Now a piece of software you wrote at 2am — or worse, didn’t write — is holding credentials that can rack up a bill and get your account banned, and it’s one bad log line away from leaking them. Three: stand up a plain SOCKS or HTTP proxy so the app can point at it. You just built an open relay, and somewhere on the internet a bot is already scanning for exactly that so it can push spam through your IP.
Those are the options. Pick your poison.
pr0xteus is the fourth one. It’s a small control plane that sits between your app and your WireGuard configs. The app never sees the configs, never touches your provider, and can’t turn into an open relay, because it doesn’t get a proxy — it gets to ask for one, and pr0xteus decides.

One request, one throwaway tunnel

Here’s the whole trick. A trusted client hits the control API and says “give me a US exit.” pr0xteus spins up a fresh Docker container — a cell — with exactly one of your WireGuard configs inside it. That cell brings up the tunnel, sits there with a default-drop firewall that lets nothing through except the WireGuard peer, and waits for the handshake to actually complete before it does anything else. Only once the tunnel is really up does it start cellproxy — a first-party SOCKS5 proxy written for this, not some off-the-shelf relay — and hand your client back a short-lived, credentialed socks5:// URL.
Your app points at that URL and its traffic goes out the VPN. When the app stops using it and the cell goes idle with no live connections, pr0xteus reaps it. Dead container, gone tunnel, no cleanup, no leftover proxy sitting around for someone to find. The name’s a proxy crossed with Proteus, the sea god who’d shapeshift and slip your grip the second you loosened it, and that’s about right — you get a different disposable exit each time and it vanishes the moment you let go.

The caller doesn’t get to name shit

This is the part that makes it safe to point an untrusted thing at. The caller’s entire input is a country or a pool name — “give me a UK exit,” “give me one from the residential pool.” That’s it. It cannot name a WireGuard config, a Docker image, a container argument, or a host path. The pools and the country routing are operator-owned files sitting on the controller; the caller picks from a menu you wrote and gets nothing else.
So the worst a compromised client can do is burn through the exits you already approved. It can’t ask for a config it shouldn’t have, can’t mount your filesystem, can’t talk to Docker. It orders off the menu or it gets a 4xx.

You can watch every exit and kill any of them

Every live cell is observable, and not through some in-memory list that drifts out of sync with reality — pr0xteus finds cells straight from Docker by label, and pulls each one’s live traffic snapshot off its cellproxy control port:

GET /v1/cells        # every live cell + its traffic
GET /v1/cells/{id}   # one cell, in detail
DELETE /v1/cells/{id}  # kill it right now

Each snapshot tells you how many requests went through that cell, bytes up and down, how many connections are live right now, and which destinations it’s been talking to. So if a cell is suddenly hammering somewhere it has no business hammering, you see it, and one DELETE burns it down mid-flight. There’s a /metrics endpoint for Prometheus too, on its own separate listener you keep private.

The isolation is the actual product

Anyone can shell out to wg-quick and a SOCKS proxy. The reason this exists is everything wrapped around that, so I’ll be specific:

  • The raw Docker socket is only ever mounted into a locked-down docker-socket-proxy. The controller and the cells never see it. The thing spinning up containers reaches Docker through a restricted, filtered socket, not the real one.
  • The controller itself runs non-root, read-only, capability-empty, resource-capped and log-capped, bound to loopback by default. You can strip every host port off it entirely for a purely internal gateway; publishing it on all interfaces is a thing you have to go out of your way to do.
  • A cell gets the bare minimum WireGuard needs — NET_ADMIN and /dev/net/tun, plus SETUID/SETGID for one thing only: a one-way drop to a non-root account before cellproxy runs. Default-drop firewall, WireGuard peer allowed, nothing else. And the cellproxy control server only ever listens on the internal cell network — never on the WireGuard side, so it’s not reachable out through the exit.
  • The bearer token isn’t stored. After startup pr0xteus keeps only a SHA-256 digest of it.
  • Tailnet access, if you want it, is a separate Tailscale sidecar that’s the only service holding /dev/net/tun, NET_ADMIN and NET_RAW. The privileged network shit lives in exactly one box, off by itself.

And then the honest part, in the README, unvarnished: these are real boundaries, not magic. Anyone who can edit your pool files, read your bearer token, or drive Docker on the host is already inside the trust boundary. It’s not pretending a determined operator-level attacker is stopped by a proxy. It’s stopping the caller — the app you don’t fully trust — from becoming the hole.

It’s built out of every other thing I’ve been posting about

Here’s the bit that made me want to write this up at all. Go look at pr0xteus’s go.mod. It’s standing on nearly the entire pile of little Go packages I’ve been boring you with for months.
It’s a servicepack service. It logs through slogging and pins request scope on the context with ctxscope, so a lease request keeps its ID across every line it produces. Its errors carry their call site through ctxerrors. Its config comes out of the environment via gonfiguration, and its HTTP layer — the control API, the middleware, the bearer auth — is aichteeteapee.
None of those packages is exciting on its own. That’s the point of them. pr0xteus is what happens when you’ve got all the boring shit already solved and can spend your actual attention on the one hard problem — safely handing an untrusted caller a disposable tunnel — instead of re-writing config parsing and log setup for the ninetieth time. 228 tests on top of it.

Running it

Linux, Docker, and a WireGuard .conf you’re allowed to use. Install is a curl-pipe-bash you should read first, per-user or system-wide:

curl -fsSL https://raw.githubusercontent.com/psyb0t/pr0xteus/main/install.sh | bash        # ~/.local/bin
curl -fsSL https://raw.githubusercontent.com/psyb0t/pr0xteus/main/install.sh | sudo bash   # /usr/local/bin, shared

The compose stack it ships binds the controller to loopback out of the box, and there’s a no-host-ports variant for when you want it reachable only over a tailnet or another internal gateway with no host-side SOCKS at all. Set PR0XTEUS_DISABLE_HOST_PORTS=true and there’s simply no host-reachable SOCKS address to find.
There’s an agent skill for it on the psyb0t/agents marketplace — claude plugin install pr0xteus@psyb0t, or codex plugin add pr0xteus@psyb0t. It’s a documentation skill that teaches an agent the real setup and the lease flow, and it’s blunt that pr0xteus is not an MCP server, because it isn’t one and won’t pretend to be.


The whole reason it exists is one rankrat or one stealthy-auto-browse that needs to go out through a specific exit without you either trusting it with the keys or routing your whole life through a VPN. Give the control plane the configs, give the app a disposable URL, and let the cells live and die on their own.
github.com/psyb0t/pr0xteus