mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
Codeman can now be mounted under a sub-path behind a reverse proxy that forwards the prefix unchanged (e.g. https://host/codeman/). Default is `/` (root), which is byte-identical to the historical behavior. Design — few choke points, mirrored ingress/egress: - src/config/base-path.ts: pure single-source normalize/validate/join/strip. - Server ingress: stripBasePath() inside Fastify rewriteUrl, so routes stay declared prefix-agnostic; un-prefixed requests (hooks, health, docker bridge hitting the raw port) pass through unchanged. - Server egress: one onSend hook prepends the base to root-absolute Location headers (covers all redirects). - HTML: renderIndexHtml points <base href> at the mount and injects window.__CODEMAN_BASE__ — ONLY when a base is set (inert at root). - Frontend runtime URLs: CodemanBase.url() route builder in constants.js, applied transparently by a fetch wrapper and explicitly at the EventSource/WebSocket/window.open/<img|iframe|a>-src sites. - sw.js derives its base from self.location; manifest uses relative start_url/scope. - Web-tab proxy: proxyPrefixFor(cap, basePath) is the single base-aware root that cascades to the injected <base>, HTML/attr rewrites, runtimeUrlShim, Set-Cookie Path and Location; capabilityFromReferer strips the base off the browser Referer, while the ingress parsers stay base-agnostic (rewriteUrl already stripped it). --base-url rides the daemon relaunch (buildWebArgs) and the service unit (resolveServicePlan). constants.js is guarded against a missing `window` for isolated unit-test contexts. Tests: test/base-path.test.ts (pure helpers), base-path coverage in webview-proxy/render-index-html/daemon-control; CodemanBase stubbed in the vm-isolated panels-ui test contexts. Docs: Remote-Access.md (sub-path section + nginx example), security-architecture.md env table, CLAUDE.md pattern. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XUkPBxbumnct6qSrx4JDju
255 lines
12 KiB
Markdown
255 lines
12 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. To retrofit it onto an existing
|
|
install:
|
|
|
|
```bash
|
|
install.sh tailscale
|
|
```
|
|
|
|
By hand:
|
|
|
|
```bash
|
|
tailscale serve --bg 3000
|
|
tailscale serve status
|
|
```
|
|
|
|
Then open `https://<machine>.<tailnet>.ts.net` from any device on your tailnet.
|
|
|
|
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.
|
|
|
|
## 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.
|