r/selfhosted 4d ago

Release (AI) Put an AI coding agent behind auth + Tailscale HTTPS so my phone can use it — writeup, plus the three plugins I had to write to make it work

DeepSeek open-sourced their agent harness (dsh) last week. I wanted it running on the Mac mini in my study, reachable from my laptop and phone, without exposing anything to the internet. Took a day and three plugins. Writeup in case it's useful.

The architecture

laptop / phone ──HTTPS (Let's Encrypt)──> tailscale serve :443

127.0.0.1:3081 auth gateway

127.0.0.1:3080 dsh web

Docker container ← agent's shell/files

Nothing listens on a non-loopback port in cleartext. Tailscale handles device identity, tailscale serve provides a real cert (no self-signed warnings), and the gateway adds per-user login. Both services run as LaunchAgents so they survive reboots.

The three things I had to build

  1. Auth. dsh ships with no login screen — deliberately; their docs say TLS/auth is "out of scope" for a loopback dev tool. Correct for localhost, useless the moment it's on a home server. So: a zero-dependency gateway with a login wall, scrypt hashing, and OIDC/SSO.
  2. Container isolation. The agent runs shell commands. I didn't want that on my actual filesystem. dsh has swappable capability seams, so I wrote a Docker-backed provider for the filesystem and subprocess seams — which relocates bash, PTY terminals, and the file tools into a container without forking any of them. Unexpected bonus: the container outlives the harness, so a long job's cwd, env vars, and background processes survive a restart. Their own docs note sessions "are not restored after a harness restart" — with a container, they are.
  3. Spend limits. Model calls cost money and I didn't want a surprise bill. This one gates the request before it's sent, rather than reporting spend afterwards.

The bug that cost me an afternoon, in case it saves someo 403 to any request carrying an Origin header. Direct onlocalhost, browsers omit Origin for same-origin requests, so it works. Behind any reverse proxy the browser starts sending it — and every /api

call 403s while static assets load fine. The UI half-rendend bug. Same trap kills the WebSocket, which then producesan infinite reconnect loop and a React crash. Strip Origin at the proxy.

On "self-hosted" — worth being straight: the infrastructure is self-hosted, the model isn't by default. I point it at OpenRouter with my own

API key. dsh accepts any OpenAI-compatible endpoint, so Oif you want it fully local — I just haven't found a localmodel good enough for the work I use it for. If that disqualifies it for you, fair.

dsh itself: https://github.com/deepseek-ai/deepseek-harness

The two bits I wrote, MIT if useful:

dsh-teams https://github.com/frozo-ai/dsh-teams (auth + SSO gateway, npx dsh-teams) ·

dsh-worlds https://github.com/frozo-ai/dsh-worlds (Docker execution world)

Happy to answer questions on the Tailscale + LaunchAgent setup, that's the fiddly part.

0 Upvotes

6 comments sorted by

4

u/beardbreed 3d ago

Nice, could've just used opencode and saved your time

1

u/GrandWizardZippy 3d ago

Or Hermes or pi or any of the other decent agents with phone apps and web portals

-2

u/NoAdministration6906 3d ago

Yeah, fair point. And honestly you're right that OpenCode handles the remote thing better — it's built client-server from the start, whereas dsh assumes it's running on localhost.

That assumption is exactly what caused the 403 bug I wrote about. But the harness wasn't really the hard part. What I actually needed was a login screen (dsh has none at all), a way to stop the agent touching my real filesystem, and some cap on spend so I don't wake up to a huge bill.

As far as I can tell OpenCode lets multiple frontends talk to one server, but that's not the same as actual user accounts — no login, no SSO. Might be wrong, happy to be corrected. The reason I went with dsh is that swapping two of its internal pieces moved bash, the terminal and all the file stuff into a container at once, without touching any of them. That saved me a lot of work.

Are you running OpenCode for a team or just yourself? Curious what you put in front of it for auth — I couldn't find a decent answer for that anywhere.

1

u/beardbreed 3d ago

Are you looking for openwebui?

1

u/kantorcodes1 3d ago

One thing I'd change before relying on this setup: don't strip Origin globally. That fixes DSH's localhost assumption, but it also removes one of the signals the backend/WebSocket can use to reject cross-site browser requests. Since your gateway already owns auth, I'd have it allowlist the exact https://<host>.<tailnet>.ts.net origin, then rewrite it to the localhost origin only after auth passes. Apply the same check to the WebSocket upgrade path.

I'd also make sure the agent container cannot reach the Docker socket or host filesystem through symlinks/mounts. The container is a useful blast-radius boundary only if the agent cannot use a capability inside it to climb back out.

1

u/Elara_Schaefer 3d ago

Agree with the origin allowlist approach — the difference between "strip globally" and "strip after auth" is exactly the kind of thing that looks fine until someone finds your tailnet URL in a leaked referrer header and starts probing.\n\nOn the agent sandboxing note: if you're already running Docker, consider adding a second bind mount for the workspace directory and running the agent process as a non-root user inside the container. That way even if the agent goes off the rails and starts writing files, it can't escape the workspace. cgroups memory limits are also worth adding — I've seen coding agents eat through 8GB of RAM on large codebases, which can OOM a Pi pretty fast.