Files
Codeman/docs/wiki/Remote-Access.md
T
Codeman maintainer 1ba0684438 docs(install): describe installer v2 and the Tailscale naming options
README, the Installation / Remote-Access / Running-As-A-Service wiki pages,
docs/security-architecture.md and CLAUDE.md describe the three-question flow,
the flags, the subcommands, the sub-path answer for an occupied :443 and why
the rename is opt-in. docs/installer-v2-plan.md is the design and the
verification record (what was measured, what still needs a fresh machine);
docs/tailscale-installer-plan.md points at it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-20 21:38:12 +02:00

276 lines
14 KiB
Markdown

# Remote Access
Reaching your Codeman from a phone, a laptop on the other side of the house, or a hotel
network. This is the page to read carefully, because Codeman's dashboard is a
remote-code-execution surface by design: it starts agents with permission prompts skipped,
so whoever can reach it can run code on your machine.
## Start from the default
`codeman web` binds `127.0.0.1`. It is reachable from the machine running it and nothing
else, which is why the no-password default is safe out of the box. Every option below is a
deliberate step away from that.
Two rules that make the rest of this page simple:
1. **Never expose Codeman on a network without `CODEMAN_PASSWORD`.** Binding a non-loopback
host without one starts, but prints a loud warning with the fixes.
2. **Prefer keeping the loopback bind** and putting an authenticated tunnel in front of it,
over binding wide and relying on a password alone.
## Pick an approach
| Approach | Good for | Cost |
| --------------------- | ----------------------------------------------------- | --------------------------------------------------------- |
| **Tailscale** | Phone access, permanently. The recommended setup. | Install Tailscale on both devices. |
| **Cloudflare tunnel** | A public URL, quickly, from anywhere. | Public URL, so a password is mandatory. |
| **LAN + password** | Home network only, no extra software. | Every device on your LAN can reach the login page. |
| **SSH port forward** | You already SSH to the box. | Manual, per session, terminal-bound. |
## Tailscale (recommended)
Your devices join a private network, and Codeman stays bound to loopback. Nothing is
published to the internet, and you get real HTTPS with a real certificate.
The installer sets this up for you, including installing Tailscale, logging in, enabling
tailnet HTTPS, and verifying the result end to end. It ends on the URL with a QR code to
scan. To retrofit it onto an existing install, or to see the URL and QR code again:
```bash
install.sh tailscale
install.sh status
```
By hand:
```bash
tailscale serve --bg 3000
tailscale serve status
```
Then open `https://<machine>.<tailnet>.ts.net` from any device on your tailnet.
### The name in the URL
The URL is the machine's MagicDNS name, so on a machine called `tnode` it is
`https://tnode.<tailnet>.ts.net`. Three ways to influence that, from least to most work:
| You want | How |
| ------------------------------------------ | ----------------------------------------------------------------------------------------------------- |
| The machine's existing name (default) | Nothing. This is what the installer does unless you say otherwise. |
| `https://codeman-<hostname>.<tailnet>.ts.net` | Answer yes to the installer's name question, pass `--name codeman-<hostname>`, or run `install.sh name`. This renames the machine tailnet-wide (SSH included), which is why the installer defaults to no. `install.sh uninstall` offers to rename it back. |
| `https://codeman.<tailnet>.ts.net` | A [Tailscale Service](https://tailscale.com/docs/features/tailscale-services). Only a **tagged** node can host one (a device signed in with a user account cannot), the service is defined and approved in the admin console, and the feature is in beta. The installer does not set this up; it is a `tailscale serve --service=svc:codeman --https=443 127.0.0.1:3000` on a tagged host once the service exists. |
If `:443` on your node already belongs to another app, the installer offers Codeman under
`https://<machine>.<tailnet>.ts.net/codeman` (the default, via `tailscale serve --set-path`
plus Codeman's `--base-url`), on a second port (`https://<machine>.<tailnet>.ts.net:8443`),
or replacing the other mapping. It never replaces anything without asking.
Notes:
- Keep the loopback bind. `tailscale serve` connects to `127.0.0.1:3000` locally, so
binding wider adds exposure and buys nothing.
- Your tailnet is the authentication boundary. Setting `CODEMAN_PASSWORD` as well is
reasonable defence in depth, especially if other people have devices on your tailnet.
- Codeman's Host-header allowlist already accepts `.ts.net`, so no extra configuration is
needed.
- The installer never resets or rewrites `serve` mappings other than the one pointing at
Codeman's port, so unrelated serve configuration is left alone. It also never opens a
`tailscale funnel` (that is the public internet) and never advertises a Tailscale Service.
- On macOS, the App Store and standalone Tailscale apps only run once someone is logged in,
so a headless Mac needs the open-source `tailscaled` for the URL to come back after a
reboot on its own.
## Cloudflare tunnel
A free [quick tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/do-more-with-tunnels/trycloudflare/)
gives you a public HTTPS URL with no port forwarding, no DNS, and no static IP:
```
Browser → Cloudflare edge (HTTPS) → cloudflared → localhost:3000
```
Prerequisites: [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)
installed, and `CODEMAN_PASSWORD` set.
```bash
./scripts/tunnel.sh start # starts the tunnel, prints the public URL
./scripts/tunnel.sh url
./scripts/tunnel.sh status
./scripts/tunnel.sh stop
```
The quick-tunnel URL is a random `*.trycloudflare.com` address that changes every time the
tunnel restarts. For a stable hostname, `./scripts/tunnel.sh named setup` walks through a
named tunnel.
To survive reboots:
```bash
systemctl --user enable codeman-tunnel
loginctl enable-linger $USER
```
There is also a toggle in **App Settings → System → Remote access**.
**The tunnel refuses to start without a password.** That is on purpose: a public URL with no
authentication is a terminal on your machine handed to the internet. Acknowledging the risk
explicitly is possible from the UI toggle, and only from there; the API will not do it for
you.
## LAN plus password
```bash
export CODEMAN_PASSWORD='something long'
codeman web -H 0.0.0.0 --https
```
Every device on your local network can now reach the login page. `--https` generates a
self-signed certificate into `~/.codeman/certs/`, which your browser will warn about once.
`CODEMAN_USERNAME` defaults to `admin`.
The installer offers this path and prompts for the password. On re-runs it preserves
whichever binding you already chose.
## SSH port forward
No configuration at all, if you already have SSH access:
```bash
ssh -L 3000:localhost:3000 you@your-box
```
Then open `http://localhost:3000` on the local machine. Codeman keeps its loopback bind and
sees a local connection. Good for occasional access, awkward as a permanent arrangement
because it dies with the SSH session.
## Logging in from a phone
Typing a long password on a phone keyboard is miserable, so Codeman issues **single-use QR
tokens**. The desktop dashboard shows a QR code; scan it and the phone is authenticated.
How it behaves:
- The code rotates every 60 seconds, with a 90 second grace window so scanning during a
rotation still works.
- Each token is **single use**. The moment a phone consumes it, a new one is generated.
- The URL contains a 6-character lookup code, not the secret, so it does not leak through
browser history, `Referer` headers, or the tunnel provider's logs.
- The desktop shows a toast naming the device and browser that just authenticated, with a
one-click revoke.
- QR attempts are rate limited separately from password attempts, so a mistyped password
cannot lock out your QR login and vice versa.
Someone holding only the tunnel URL still meets the normal password prompt. The QR is the
fast path, not a bypass.
Design detail and the threat analysis it is built against:
[`docs/qr-auth-plan.md`](https://github.com/Ark0N/Codeman/blob/master/docs/qr-auth-plan.md).
## Behind a reverse proxy
Codeman enforces a Host-header allowlist on every request to block DNS rebinding, and the
same allowlist gates the cross-site Origin check. It accepts `localhost`, IP literals, the
bind host, `.ts.net`, `.trycloudflare.com`, `.cfargotunnel.com`, and the active managed
tunnel.
**Your own domain is not on that list.** Add it:
```bash
CODEMAN_ALLOWED_HOSTS='codeman.example.com,.internal.example.com'
```
A bare entry matches that exact host; a leading dot matches subdomains. Without this, a
correctly configured proxy still gets `403 host not allowed`, which reads like a proxy bug
and is not one.
Also make sure the proxy forwards WebSocket upgrades. The terminal is a WebSocket, and the
upgrade runs the same Host and Origin checks, closing with code `4003` on failure.
### Mounting under a sub-path
By default Codeman assumes it is served at the origin root (`/`). To mount it under a
sub-path — e.g. `https://example.com/codeman/` — start it with `--base-url` (or the
`CODEMAN_BASE_URL` env var):
```bash
codeman web --base-url /codeman
# or
CODEMAN_BASE_URL=/codeman codeman web
```
The value is a plain path prefix; `/` (the default) means "mounted at the root". With a
prefix set, Codeman emits every URL — the HTML shell and its assets, API/SSE/WebSocket
calls, redirects, the PWA manifest and the service worker — under that prefix, so a browser
loading `https://example.com/codeman/` stays inside the mount.
**Forward the prefix unchanged — do NOT strip it.** Codeman expects the proxy to pass the
full path (including `/codeman/`) straight through. A minimal nginx block:
```nginx
location /codeman/ {
proxy_pass http://127.0.0.1:3000; # note: no trailing slash — keep the /codeman/ prefix
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade; # WebSocket
proxy_set_header Connection "upgrade";
}
```
Notes and current limits:
- The prefix must still be paired with `CODEMAN_ALLOWED_HOSTS` for your domain, exactly as
above — the two are independent.
- Health checks, Claude Code hooks and the docker bridge connect to the raw port directly
(bypassing the proxy), so Codeman also keeps answering at the un-prefixed paths on the port
itself. Nothing about those flows changes.
- **Web-tab (dashboard) proxying** is base-path aware: proxied dashboards have their injected
`<base>` tag, root-absolute asset rewrites, runtime `fetch`/XHR shim, `Set-Cookie` paths, and
redirects all rebased onto the mount, so they load the same under `--base-url` as at the root.
## Session cookies and rate limits
The first request prompts for HTTP Basic credentials. On success the server issues an opaque
`codeman_session` cookie (24 hour lifetime, extended on activity, validated server-side so
it cannot be forged offline). Ten failed attempts from one IP produce a `429` with a 15
minute decay.
A valid cookie or a correct password recovers immediately even while an attacker is hammering
the same IP, which matters because all tunnel traffic arrives from one loopback address.
## Terminal alternatives
You do not have to use a browser. `codeman tui` is a full-screen session dashboard that
works well in SSH clients like Termius or Blink:
```bash
codeman tui # the dashboard
codeman tui 2 # attach straight to session 2
codeman tui --list # numbered list, then exit
```
`Enter` attaches into the pane and `F1` comes back. Under 72 columns it drops the preview
and becomes a single-column list, so it stays usable on a phone. The sessions are the same
ones the dashboard shows. See [docs/tui.md](https://github.com/Ark0N/Codeman/blob/master/docs/tui.md)
for the full guide.
## Common problems
| Symptom | Cause and fix |
| ----------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
| `403 host not allowed` | Your domain is not in the allowlist. Set `CODEMAN_ALLOWED_HOSTS`. |
| Assets 404 / blank page under a sub-path | Start Codeman with `--base-url /<prefix>` and have the proxy forward the prefix unchanged (don't strip it). |
| Phone shows the login page but the terminal never connects | The proxy is not forwarding WebSocket upgrades. |
| Browser warns about the certificate | Expected with `--https` and its self-signed certificate. Tailscale gives you a real one instead. |
| LAN IP does not respond, but a tunnel to the same box works | The server is bound to loopback. That is the default. A tunnel reaches it; a LAN browser cannot. |
| Hooks stopped working after switching to HTTPS | Hook callbacks need `-k` for the self-signed certificate. Recent versions self-heal existing cases; if yours predates that, recreate the case's hooks. |
| Everything is slow over the tunnel | Quick tunnels route through Cloudflare's edge. Tailscale is usually a direct connection and much faster. |
## Read next
- [Security](Security) - the whole model, and the hardening checklist.
- [Mobile Guide](Mobile-Guide) - once you can reach it from the phone.
- [Running As A Service](Running-As-A-Service) - keeping server and tunnel up across reboots.
- [`docs/security-architecture.md`](https://github.com/Ark0N/Codeman/blob/master/docs/security-architecture.md) - the full model.