From 7330210ced3b75ccce0b50bcc1610477b5bc2ab3 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Tue, 5 May 2026 22:22:54 +0000 Subject: [PATCH 1/2] docs: update exposure and pairing guidance --- social-images-manifest.json | 4 +- src/content/docs/cli/init.md | 22 ++++-- src/content/docs/deployment/docker.md | 26 ++++++- src/content/docs/deployment/exposure-modes.md | 78 ++++++++++++++++--- .../docs/guides/pairing-remote-devices.md | 42 ++++++---- src/content/docs/security/hardening.md | 5 +- src/content/docs/security/security-model.md | 3 + 7 files changed, 141 insertions(+), 39 deletions(-) diff --git a/social-images-manifest.json b/social-images-manifest.json index 550de12..cdd013d 100644 --- a/social-images-manifest.json +++ b/social-images-manifest.json @@ -105,8 +105,8 @@ "imageUrl": "https://hcti.io/v1/image/292fbcccd9914a9c91a0738a6761c5e415c793b6e3d97a3b282f6ba340486bb7" }, "deployment/exposure-modes": { - "hash": "e8baf14472f7f6848d6f325662cfaaa8e5c218eb0c503a22021e748f5d6a7969", - "imageUrl": "https://hcti.io/v1/image/e8df67e916d7e662c134601345647d510b2b90f2af1b85537817761cb4efc698" + "hash": "d18cf6800d78805419d7324432070556d829d05997a231f786d93193f5e970ad", + "imageUrl": "https://hcti.io/v1/image/7ddfb5e506a8c2c157d85ec9a438cc8ceda6a6eda0fe876428895f70f55df6ce" }, "deployment/systemd": { "hash": "8d2c632b152b487fe516506b4120ae564770130dae5cbe524830ee01f86006cd", diff --git a/src/content/docs/cli/init.md b/src/content/docs/cli/init.md index 5a41650..3596026 100644 --- a/src/content/docs/cli/init.md +++ b/src/content/docs/cli/init.md @@ -3,7 +3,7 @@ title: "netclaw init" description: "Interactive setup wizard for providers, channels, security, and network exposure." --- -First-run wizard. Configures your provider, security policy, channels, identity, skills, and network exposure in one pass, then starts the daemon. +Interactive first-run setup. Configures your provider, security policy, channels, identity, skills, and network exposure in one pass, then starts the daemon. ## Usage @@ -25,11 +25,12 @@ Have these ready before starting: - **Slack tokens** (if using Slack) — Bot Token (`xoxb-...`) + App Token (`xapp-...`). See the [Slack quickstart](https://api.slack.com/start/quickstart) - **Discord bot token** (if using Discord) — from the [Discord developer portal](https://discord.com/developers/docs/getting-started) -No other dependencies needed. +For local-only setup, that is enough. For non-local exposure modes, you may also need `tailscaled`, `cloudflared`, or a working reverse proxy depending on the mode you choose. + ## Wizard steps -The wizard adapts to your choices — steps get skipped based on your security posture and feature selections. +The wizard skips steps based on your choices, security posture, and feature selections. ### 1. LLM Provider @@ -124,11 +125,12 @@ Subscribe to skill feeds — curated skill collections from the community or you | Mode | Reachability | Requires | |------|-------------|----------| | `local` | Loopback only (this machine) | Nothing | +| `reverse-proxy` | Whatever your proxy exposes | Reverse proxy + trusted proxy list | | `tailscale-serve` | Your [Tailscale](https://tailscale.com/kb/) tailnet | `tailscaled` running | | `tailscale-funnel` | Public internet via Tailscale | `tailscaled` running | | `cloudflare-tunnel` | Public internet via [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) | `cloudflared` running | -The internet-facing modes (`tailscale-funnel`, `cloudflare-tunnel`) make you type an explicit confirmation. They're not kidding about the warning. +The internet-facing modes (`tailscale-funnel`, `cloudflare-tunnel`) make you type an explicit confirmation because they expose the daemon to the internet. ![Webhook configuration](/screenshots/output/init-09-webhooks.png) @@ -138,13 +140,17 @@ If you enabled webhooks, you'll configure inbound routes here. ![Health check running](/screenshots/output/init-10-health-check.png) -Validates config files, tests provider connectivity, verifies channel tokens, checks tunnel prerequisites. +Validates config files, tests provider connectivity, verifies channel tokens, and checks tunnel or reverse-proxy prerequisites. ![Health check complete](/screenshots/output/init-10-health-check-complete.png) -All green? The daemon starts automatically. +If all checks pass, the daemon starts automatically. + +If you picked a non-local exposure mode, the first successful daemon start also keeps a bootstrap pairing path available so the local CLI can finish remote-auth setup. + +If something fails, the wizard tells you which check broke, shows exposure-mode validation failures directly, and suggests running `netclaw doctor` for detailed diagnostics. -If something fails, the wizard tells you which check broke and suggests running `netclaw doctor` for detailed diagnostics. +If `netclaw init` fails partway through, the files it already wrote stay on disk. It is safe to rerun `netclaw init`, or inspect the saved config and use `netclaw doctor` before trying again. ## What it creates @@ -152,6 +158,7 @@ If something fails, the wizard tells you which check broke and suggests running |------|---------| | `~/.netclaw/config/netclaw.json` | Main configuration (includes security posture) | | `~/.netclaw/config/secrets.json` | Encrypted credentials | +| `~/.netclaw/config/devices.json` | Paired device registry, including first-launch bootstrap device when needed | | `~/.netclaw/identity/` | Agent identity and personality | ## After init @@ -180,3 +187,4 @@ netclaw chat - [Slack Socket Mode](https://api.slack.com/apis/socket-mode) — How netclaw connects to Slack without a public endpoint - [Tailscale Funnel documentation](https://tailscale.com/kb/1223/funnel/) — Exposing services to the public internet via Tailscale - [Cloudflare Tunnel documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — Exposing services via Cloudflare's network +- [Traefik reverse proxy docs](https://doc.traefik.io/traefik/routing/routers/) — reverse-proxy option if you're exposing netclaw behind Traefik diff --git a/src/content/docs/deployment/docker.md b/src/content/docs/deployment/docker.md index 58e4376..fc654f1 100644 --- a/src/content/docs/deployment/docker.md +++ b/src/content/docs/deployment/docker.md @@ -3,7 +3,7 @@ title: "Docker Deployment" description: "Run the netclaw daemon in a Docker container with persistent config, health checks, and zero-downtime upgrades." --- -The netclaw Docker image runs the daemon (`netclawd`) in a supervised container. The CLI stays on your host and connects over HTTP at `127.0.0.1:5199`. Your interactive terminal stays outside the container; the daemon runs headless. +The netclaw Docker image runs the daemon (`netclawd`) in a supervised container. The CLI can stay on your host or run inside the container with `docker exec`. By default the local control-plane endpoint is `127.0.0.1:5199` when you publish it that way; non-local exposure modes add more auth and proxy rules. ## Before you begin @@ -13,15 +13,18 @@ The netclaw Docker image runs the daemon (`netclawd`) in a supervised container. ## Quick start +This path assumes you already have an initialized `~/.netclaw` directory. + ```bash docker run -d \ --name netclaw \ -v ~/.netclaw:/root/.netclaw \ -p 127.0.0.1:5199:5199 \ + -e NETCLAW_Daemon__Host=0.0.0.0 \ ghcr.io/netclaw-dev/netclaw ``` -The port binding is loopback-only (`127.0.0.1:5199`) because the health check endpoint is unauthenticated — don't expose it to the network. The volume mount persists identity, config, credentials, session state, and logs across restarts. Self-update is disabled in the image; the image tag _is_ the version. Update availability checks still run, so you'll know when a new release exists. +The port binding is loopback-only (`127.0.0.1:5199`) because the health check endpoint is unauthenticated — don't expose it to the network. `NETCLAW_Daemon__Host=0.0.0.0` makes the daemon listen on the container interface so Docker's published port can reach it. The volume mount persists identity, config, credentials, session state, and logs across restarts. Self-update is disabled in the image. Use the image tag as the version. Update availability checks still run, so you'll know when a new release exists. **Tags:** `:latest` tracks the most recent release. Pin to a version tag (e.g., `:1.2.3`) in production. @@ -43,6 +46,8 @@ docker exec -it netclaw netclaw init The interactive wizard works the same way over `docker exec -it`. +If you choose a non-local exposure mode during first-run setup, netclaw now seeds a one-shot local bootstrap credential before the first successful non-local daemon start. That prevents first boot from getting stuck waiting on pairing. + ## Configuration via environment variables Pass provider credentials and model config as `NETCLAW_`-prefixed environment variables. Double underscores separate path segments, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider). Env vars take highest priority, overriding both `netclaw.json` and `secrets.json`. @@ -52,6 +57,7 @@ docker run -d \ --name netclaw \ -v ~/.netclaw:/root/.netclaw \ -p 127.0.0.1:5199:5199 \ + -e NETCLAW_Daemon__Host=0.0.0.0 \ -e NETCLAW_Providers__openrouter__Type=openrouter \ -e NETCLAW_Providers__openrouter__ApiKey=sk-or-v1-... \ -e NETCLAW_Models__Main__Provider=openrouter \ @@ -78,6 +84,7 @@ services: volumes: - netclaw-home:/root/.netclaw environment: + NETCLAW_Daemon__Host: 0.0.0.0 NETCLAW_Providers__local-ollama__Type: ollama NETCLAW_Providers__local-ollama__Endpoint: http://ollama:11434 NETCLAW_Models__Main__Provider: local-ollama @@ -108,6 +115,8 @@ docker exec ollama ollama pull qwen3:30b Netclaw references Ollama by service name (`http://ollama:11434`) since Compose puts both containers on the same network. +`qwen3:30b` is a large model. For first-time testing on modest hardware, pick a smaller Ollama model. + ### Docker socket access If you want the agent to manage Docker containers as part of its tool use, mount the socket: @@ -118,6 +127,8 @@ If you want the agent to manage Docker containers as part of its tool use, mount Add this to the `volumes` section of the netclaw service in your Compose file or to the `docker run` command. +Treat this as host-level access. A process that can talk to the Docker socket can usually control the host. + ## Volume layout Everything the daemon persists lives under `/root/.netclaw`: @@ -137,7 +148,7 @@ Everything the daemon persists lives under `/root/.netclaw`: └── logs/ # crash-*.log, session logs ``` -Back up this volume before upgrades. `config/` and `identity/` are the critical directories; everything else can be regenerated. +Back up this volume before upgrades. `config/` and `identity/` are the critical directories; everything else can be regenerated. If you use a bind mount, copy `~/.netclaw` directly. If you use a named volume, export it with your normal Docker volume backup process before upgrades. ## Health checks @@ -163,6 +174,8 @@ docker inspect --format='{{.State.Health.Status}}' netclaw The entrypoint script is a PID 1 supervisor. If the daemon exits (config-update restart, crash, `netclaw init` wizard completion), the entrypoint waits 2 seconds and restarts it. The container stays alive, so `docker exec` sessions survive daemon restarts. +That also helps with pairing: if the daemon is running in a container and you need a daemon-host bootstrap or recovery path, `docker exec netclaw netclaw daemon pair` works the same way as running it on a bare-metal daemon host. + `docker stop` sends SIGTERM, which the entrypoint forwards to the daemon for a clean shutdown. Docker's default 10-second stop timeout is plenty. ## Upgrading @@ -181,6 +194,7 @@ docker run -d \ --name netclaw \ -v ~/.netclaw:/root/.netclaw \ -p 127.0.0.1:5199:5199 \ + -e NETCLAW_Daemon__Host=0.0.0.0 \ ghcr.io/netclaw-dev/netclaw:latest # Wait for readiness @@ -221,6 +235,8 @@ ss -tlnp | grep 5199 docker logs netclaw ``` +If the container is configured for `reverse-proxy`, also check whether the daemon is still bound to loopback. Reverse-proxy mode rejects loopback final-hop topologies. + ### Container keeps restarting The entrypoint restarts the daemon on every exit, and that's by design. If it's a crash loop, check `docker logs netclaw` for the cause. Common culprits: missing provider config, invalid API key, or a required field missing from `netclaw.json`. @@ -232,7 +248,7 @@ The 30-second start period gives the daemon time to initialize. If it's still un ## Related pages - [Models](/configuration/models/) — model slot configuration -- [Exposure Modes](/deployment/exposure-modes/) — remote access via Tailscale or Cloudflare Tunnel +- [Exposure Modes](/deployment/exposure-modes/) — remote access via reverse proxy, Tailscale, or Cloudflare Tunnel - [systemd Service](/deployment/systemd/) — bare-metal Linux alternative - [OpenTelemetry](/observability/opentelemetry/) — daemon metrics and log export @@ -240,6 +256,8 @@ The 30-second start period gives the daemon time to initialize. If it's still un - [Docker Engine installation guide](https://docs.docker.com/engine/install/) — platform-specific install instructions - [Docker Compose documentation](https://docs.docker.com/compose/) — multi-container orchestration +- [Docker security guide](https://docs.docker.com/engine/security/) — baseline guidance for socket mounts and daemon exposure - [GitHub Container Registry (GHCR)](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) — pulling and authenticating with ghcr.io - [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention +- [Docker exec reference](https://docs.docker.com/reference/cli/docker/container/exec/) — run CLI commands inside a live container - [Ollama model library](https://ollama.com/library) — browse available models for local inference diff --git a/src/content/docs/deployment/exposure-modes.md b/src/content/docs/deployment/exposure-modes.md index c97312e..039bcbf 100644 --- a/src/content/docs/deployment/exposure-modes.md +++ b/src/content/docs/deployment/exposure-modes.md @@ -1,13 +1,14 @@ --- title: Exposure Modes -description: Configure how netclaw is exposed to the network — local, Tailscale Serve, Tailscale Funnel, or Cloudflare Tunnel. +description: Configure how netclaw is exposed to the network — local, reverse proxy, Tailscale Serve, Tailscale Funnel, or Cloudflare Tunnel. --- -Exposure mode controls how the daemon is reachable over the network. Most setups stay on local mode. You only need a tunnel mode when something external (GitHub webhooks, CI, a second machine) needs to reach the daemon. +Exposure mode controls how the daemon is reachable over the network. Most setups use local mode. You only need a non-local mode when something external (GitHub webhooks, CI, a second machine, or a reverse proxy) needs to reach the daemon. ## Before you begin - Netclaw installed and initialized (`netclaw init`). See [Installation](/getting-started/installation/) if needed. +- For reverse proxy: a working proxy already configured, a non-loopback internal IP for the daemon, and the proxy source IP or CIDR ready for `TrustedProxies`. - For Tailscale modes: [`tailscaled` installed and running](https://tailscale.com/download). - For Cloudflare Tunnel: [`cloudflared` installed and configured](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/). @@ -16,6 +17,7 @@ Exposure mode controls how the daemon is reachable over the network. Most setups | Mode | Config value | Required process | Reachability | Risk | |------|-------------|-----------------|-------------|------| | **Local** | `local` | None | Loopback only | Lowest | +| **Reverse Proxy** | `reverse-proxy` | Your reverse proxy | Whatever the proxy exposes | Medium | | **Tailscale Serve** | `tailscale-serve` | `tailscaled` | Your Tailscale network (called a [tailnet](https://tailscale.com/kb/1136/tailnet)), HTTPS | Low | | **Tailscale Funnel** | `tailscale-funnel` | `tailscaled` | Public internet via Tailscale | High | | **Cloudflare Tunnel** | `cloudflare-tunnel` | `cloudflared` | Public internet via Cloudflare | High | @@ -26,6 +28,27 @@ The `netclaw init` wizard covers this at step 9: Options marked with a warning triangle expose the daemon to the public internet. Tailscale Serve is the recommended remote mode: tailnet-only access, no public exposure. +### Reverse Proxy + +```json +{ + "Daemon": { + "Host": "10.0.0.5", + "ExposureMode": "reverse-proxy", + "TrustedProxies": ["10.0.0.10"] + } +} +``` + +Use this when nginx, Caddy, Traefik, HAProxy, or another reverse proxy is the public edge. + +In this mode: + +- `Daemon.Host` must be a non-loopback internal address. `127.0.0.1`, `::1`, and `localhost` are rejected in `reverse-proxy` mode. +- `TrustedProxies` must list the proxy's source IP or CIDR. Forwarded headers are only honored from those peers. + +If the proxy runs on the same machine, the final hop into netclaw still needs to target a non-loopback internal IP. A same-host reverse proxy is fine, but the final hop into netclaw still cannot use loopback. + ## Configuration Set the mode in the `Daemon` section of `~/.netclaw/config/netclaw.json`. @@ -74,6 +97,8 @@ Funnel extends Serve to the public internet. Anyone with the URL can reach the d Routes traffic through Cloudflare's network to the daemon. Set up `cloudflared` with a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) pointed at `127.0.0.1:5199`, and pair it with a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) policy to restrict who can connect. +If `cloudflared` runs outside the daemon's process namespace, set `Daemon.SkipTunnelProcessCheck` to `true` so startup validation skips the local process probe without skipping the rest of the auth checks. + ### Full Daemon section @@ -82,7 +107,9 @@ Routes traffic through Cloudflare's network to the daemon. Set up `cloudflared` |-------|------|---------|-------| | `Host` | string | `127.0.0.1` | IP address the daemon binds to | | `Port` | int | `5199` | TCP port | -| `ExposureMode` | string | `local` | `local`, `tailscale-serve`, `tailscale-funnel`, or `cloudflare-tunnel` | +| `ExposureMode` | string | `local` | `local`, `reverse-proxy`, `tailscale-serve`, `tailscale-funnel`, or `cloudflare-tunnel` | +| `TrustedProxies` | string[] | `[]` | Required in `reverse-proxy`; literal IPs or CIDRs only | +| `SkipTunnelProcessCheck` | bool | `false` | Skip local tunnel process detection for sidecar or host-managed tunnel topologies | ```json { @@ -94,7 +121,7 @@ Routes traffic through Cloudflare's network to the daemon. Set up `cloudflared` } ``` -Case-insensitive — `tailscale-serve`, `TailscaleServe`, and `TAILSCALE-SERVE` all work. +Case-insensitive — `reverse-proxy`, `ReverseProxy`, and `REVERSE-PROXY` all work, same as the tunnel modes. **Docker users:** if you switch to a tunnel mode, update your container's port binding to match the `Host` and `Port` values here. See [Docker Deployment](/deployment/docker/) for details. @@ -103,9 +130,10 @@ Case-insensitive — `tailscale-serve`, `TailscaleServe`, and `TAILSCALE-SERVE` Override any field with `NETCLAW_Daemon__` prefixed env vars: ```bash -NETCLAW_Daemon__ExposureMode=tailscale-serve -NETCLAW_Daemon__Host=127.0.0.1 +NETCLAW_Daemon__ExposureMode=reverse-proxy +NETCLAW_Daemon__Host=10.0.0.5 NETCLAW_Daemon__Port=5199 +NETCLAW_Daemon__TrustedProxies__0=10.0.0.10 ``` Double underscores separate path segments, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider). @@ -132,9 +160,13 @@ They do nothing in local mode. ## Device pairing -Non-local modes require at least one paired device or an alternative remote authentication scheme. Without one, the daemon refuses to start — remote clients have no way to authenticate. +Non-local modes require at least one paired device or an alternative remote authentication scheme. Without one, the daemon refuses to start because remote clients have no way to authenticate. + +On fresh setup-owned installs, netclaw automatically creates a one-time bootstrap path for the first non-local start. Before the first successful non-local daemon start, it seeds one local paired device and matching local client token when no paired devices already exist. That covers the init wizard, first Docker boot, and manual setups that still use the daemon's local state directory. + +After the first successful non-local start, that auto-seeding stops. Normal CLI use is paired-device auth from that point on. -The init wizard handles this: it generates a bootstrap device token when you pick a tunnel mode, writing it to `~/.netclaw/config/devices.json` and `~/.netclaw/config/secrets.json`. For manual setup, pair a device before starting the daemon: +If the daemon is already running and you need to add another device, pair from the daemon host: ```bash netclaw daemon pair @@ -161,6 +193,8 @@ Netclaw. Both are fatal. The daemon won't start until you fix the underlying issue. +In `reverse-proxy` mode, startup also fails if the daemon is still bound to loopback or if `TrustedProxies` is empty or malformed. + ## Troubleshooting ### Start here: `netclaw doctor` @@ -197,18 +231,38 @@ Or switch to local mode if you don't need remote access: Startup aborted with "no paired devices exist and no alternative remote authentication scheme is configured." -Pair a device: +If this is a brand-new non-local install, check whether the daemon actually completed its first successful start. The one-shot bootstrap only happens before that first successful non-local startup. + +If the daemon is already running, pair from the daemon host: ```bash netclaw daemon pair ``` -Or re-run the init wizard, which generates a bootstrap token automatically when you select a tunnel mode: +If the device store was lost after the first successful non-local start and the daemon will not start anymore, temporarily switch `Daemon.ExposureMode` to `local` or restore `devices.json` and `secrets.json` from backup. Then start the daemon, run `netclaw daemon pair`, and switch back. + +You can also re-run the init wizard, which still cooperates with the runtime bootstrap path: ```bash netclaw init ``` +### Reverse proxy bound to loopback + +Startup aborted because `Daemon.ExposureMode` is `reverse-proxy` but `Daemon.Host` is `127.0.0.1`, `::1`, or `localhost`. + +Bind netclaw to a non-loopback internal IP and point the proxy at that address instead of loopback: + +```json +{ + "Daemon": { + "Host": "10.0.0.5", + "ExposureMode": "reverse-proxy", + "TrustedProxies": ["10.0.0.10"] + } +} +``` + ### Non-loopback bind in local mode `netclaw doctor` reports a warning: "ExposureMode is 'local' but bind address is not loopback." @@ -234,8 +288,6 @@ Or switch to the exposure mode that reflects how the daemon is actually reachabl } ``` - - ## Related pages - [Docker Deployment](/deployment/docker/) — containerized daemon with loopback port binding @@ -250,4 +302,6 @@ Or switch to the exposure mode that reflects how the daemon is actually reachabl - [Tailscale Funnel documentation](https://tailscale.com/kb/1223/funnel) — expose tailnet services to the public internet - [Cloudflare Tunnel documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — route traffic through Cloudflare to your origin - [Cloudflare Access documentation](https://developers.cloudflare.com/cloudflare-one/policies/access/) — identity-aware access policies for tunneled services +- [nginx reverse proxy guide](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) — baseline reverse-proxy behavior and forwarding model +- [Cloudflare Tunnel service docs](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/configure-tunnels/local-management/as-a-service/) — service startup and lifecycle details for cloudflared - [Tailscale download](https://tailscale.com/download) — install Tailscale on your platform diff --git a/src/content/docs/guides/pairing-remote-devices.md b/src/content/docs/guides/pairing-remote-devices.md index 1e5da70..f66a9c4 100644 --- a/src/content/docs/guides/pairing-remote-devices.md +++ b/src/content/docs/guides/pairing-remote-devices.md @@ -3,12 +3,12 @@ title: "Pairing Remote Devices" description: "Pair remote CLI clients with your Netclaw daemon." --- -Your netclaw daemon runs on one machine but you want to use the CLI from another -- a laptop, a second server, a container you can't shell into. Pairing is a two-command handshake: generate a time-limited code on the daemon, exchange it from the remote device, and the client gets a bearer token for all future requests. +Your netclaw daemon runs on one machine but you want to use the CLI from another -- a laptop, a second server, a container you can't shell into. Pairing takes two commands: generate a time-limited code on the daemon, exchange it from the remote device, and the client gets a bearer token for all future requests. ## Before You Begin - Netclaw installed on both machines ([`netclaw init`](/cli/init/) completed on the daemon host). The remote device only needs the `netclaw` binary -- you don't need to run `netclaw init` on it. Pairing replaces init for client-only machines. -- The daemon's exposure mode set to something other than `local` -- remote pairing doesn't work over loopback. The default daemon port is **5199**; make sure your firewall allows it. +- The daemon's exposure mode set to something other than `local` -- remote pairing doesn't work over loopback. The default daemon port is **5199**; make sure the chosen proxy or tunnel path can reach the daemon. - Network connectivity between the two machines (same tailnet, tunnel, or direct) ## 1. Set an Exposure Mode @@ -22,6 +22,7 @@ During `netclaw init`, Step 9 handles this: | Mode | Config value | Requires | Who can reach it | |------|-------------|----------|-----------------| | Local | `local` | Nothing | Loopback only (default) | +| Reverse Proxy | `reverse-proxy` | Reverse proxy + trusted proxy config | Whatever the proxy exposes | | Tailscale Serve | `tailscale-serve` | `tailscaled` running | Same [tailnet](https://tailscale.com/kb/1136/tailnet) | | Tailscale Funnel | `tailscale-funnel` | `tailscaled` running | [Public internet](https://tailscale.com/kb/1223/funnel) | | Cloudflare Tunnel | `cloudflare-tunnel` | `cloudflared` running | Internet via [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) | @@ -41,7 +42,7 @@ For tunnel setup details -- Tailscale Serve commands, Cloudflare Tunnel configur Restart the daemon after changing exposure mode (`netclaw daemon stop && netclaw daemon start`, or `systemctl restart netclaw` if you're [using systemd](/deployment/systemd/)). :::caution[Non-local modes require at least one paired device] -If you set a non-local exposure mode, the daemon requires at least one paired device at startup (unless another remote auth scheme is configured). This is a chicken-and-egg situation: you need to pair your first device while still in `local` mode, then switch. The `netclaw init` wizard handles this automatically -- manual setup requires pairing first from the daemon host. +If you set a non-local exposure mode, the daemon still requires at least one paired device at startup unless another remote auth scheme is configured. On fresh setup-owned installs, netclaw creates a one-time bootstrap credential before the first successful non-local start. After that first successful start, normal CLI use requires pairing. ::: ## 2. Generate a Pairing Code (Daemon Side) @@ -68,13 +69,19 @@ The character set (`23456789ABCDEFGHJKLMNPQRSTUVWXYZ`) deliberately excludes `0` ### Docker -If the daemon runs in a container, the pairing code is logged at `Information` level: +If the daemon runs in a container, `docker exec` is still the simplest daemon-host path: + +```bash +docker exec -it netclaw daemon pair +``` + +The daemon also logs pairing codes at `Information` level: ```bash docker logs | grep "Pairing code" ``` -No need to exec into the container. +Use `docker logs` when you only need the code. Use `docker exec` when you want to pair or manage devices from inside the container. ## 3. Pair the Remote Device (Client Side) @@ -84,7 +91,7 @@ On the remote machine: netclaw pair http://my-server:5199 ``` -For Tailscale and Cloudflare Tunnel modes, the endpoint URL differs -- check [Exposure Modes](/deployment/exposure-modes/) for the correct format for each mode. +For Tailscale, Cloudflare Tunnel, and reverse-proxy modes, the endpoint URL differs -- check [Exposure Modes](/deployment/exposure-modes/) for the correct format for each mode. The CLI prompts for two things: @@ -103,7 +110,7 @@ On success: If a device with the same name already exists on the daemon, the exchange returns HTTP 409. Revoke the old device first (see below). -There's no limit to how many devices you can pair -- add as many as you need. +You can pair as many devices as you need. ## 4. Verify the Connection @@ -139,7 +146,8 @@ After revocation, the device gets 401 on its next request. Use this when a devic - Raw tokens never hit disk on the daemon side -- it stores a SHA256 hash with a per-device salt in `~/.netclaw/config/devices.json` (file permissions `600` on Linux) - Clients keep the raw token in `~/.netclaw/config/secrets.json` -- Loopback connections skip bearer auth entirely -- if you're on the daemon host, you don't need to pair +- In `local` mode, loopback connections still work without bearer auth +- In remote-auth-required modes, bearer auth may still be required even on a loopback control-plane endpoint ### Endpoint Resolution @@ -147,7 +155,10 @@ When the CLI connects, it checks these in order: 1. `NETCLAW_DAEMON_ENDPOINT` environment variable 2. `~/.netclaw/client/config.json` (written by `netclaw pair`) -3. Default: `http://127.0.0.1:5199` +3. Daemon bind config (`Daemon.Host` + `Daemon.Port`) if available +4. Default: `http://127.0.0.1:5199` + +If the daemon bind host is a wildcard like `0.0.0.0`, the CLI normalizes it to a connectable local endpoint instead of trying to connect to the wildcard address. Override with the env var when you need to switch between multiple daemons without re-pairing. @@ -161,7 +172,7 @@ The pairing endpoint has three layers of brute-force protection: | Fail2ban-style guard | 10 failures in 15 min blocks the IP for 15 min | | No-code-pending gate | Returns 404 when no code is active | -With a 32-character alphabet, 8-character codes, 5-minute expiry, and 5 attempts/minute -- brute force isn't happening. +With a 32-character alphabet, 8-character codes, 5-minute expiry, and 5 attempts/minute, that makes brute-force guessing impractical. ## Troubleshooting @@ -173,7 +184,9 @@ The token was revoked or the daemon's device store was reset. Re-pair: netclaw pair ``` -Run `netclaw daemon pair` on the daemon host to get a fresh code. +If the daemon is already running, run `netclaw daemon pair` on the daemon host to get a fresh code. + +If the device store was lost and a non-local daemon now fails startup, temporarily switch `Daemon.ExposureMode` to `local` or restore `devices.json` and `secrets.json` from backup. Then start the daemon, run `netclaw daemon pair`, and switch back. ### Pairing code expired @@ -193,14 +206,16 @@ Check the basics: 1. Is the daemon running? (`netclaw daemon start` on the host) 2. Is the exposure mode set to something other than `local`? -3. Can you reach the endpoint from the remote machine? (`curl http://my-server:5199/api/health/ready`) +3. Can you reach the daemon through the intended endpoint or proxy path? 4. Is a firewall blocking port 5199? +If the daemon uses `reverse-proxy`, also confirm the daemon itself is not still bound to loopback and that `TrustedProxies` includes the proxy's source IP or CIDR. + Run [`netclaw doctor`](/cli/doctor/) on the daemon host -- it includes exposure-mode health checks. ### IP blocked after too many failed attempts -Wait 15 minutes, or fix the issue from a different IP. The fail2ban guard auto-expires after 15 minutes. +Wait 15 minutes, or fix the issue from a different IP. Too many wrong codes temporarily block your IP for 15 minutes. ## Next Steps @@ -215,4 +230,5 @@ Wait 15 minutes, or fix the issue from a different IP. The fail2ban guard auto-e - [Tailscale Serve](https://tailscale.com/kb/1242/tailscale-serve) -- expose local services to your tailnet - [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) -- expose local services to the public internet via Tailscale - [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) -- route traffic to your daemon through Cloudflare's network +- [Caddy reverse_proxy](https://caddyserver.com/docs/caddyfile/directives/reverse_proxy) -- example reverse proxy if you're exposing netclaw behind Caddy - [Tailscale: What is a tailnet?](https://tailscale.com/kb/1136/tailnet) -- networking concepts for the Tailscale Serve mode diff --git a/src/content/docs/security/hardening.md b/src/content/docs/security/hardening.md index 0aa4d79..9c3e751 100644 --- a/src/content/docs/security/hardening.md +++ b/src/content/docs/security/hardening.md @@ -119,6 +119,7 @@ The daemon binds to `127.0.0.1:5199` by default. Keep it that way unless you hav | Mode | Scope | Risk | |------|-------|------| | `local` | Loopback only | Minimal — only local processes can connect | +| `reverse-proxy` | Whatever your proxy exposes | Medium to high — depends on final hop and trusted-proxy config | | `tailscale-serve` | Your tailnet | Low — [Tailscale identity](https://tailscale.com/kb/1312/serve) gates access | | `tailscale-funnel` | Public internet | High — anyone on the internet can reach it via [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) | | `cloudflare-tunnel` | Public internet | High — requires a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) policy | @@ -129,6 +130,8 @@ The exposure selection during `netclaw init`. Internet-facing modes force explic If you need remote access, prefer `tailscale-serve`. It limits access to your tailnet. Funnel exposes you to the public internet — a completely different threat model. +If you need a conventional reverse proxy, bind netclaw to a non-loopback internal IP and explicitly set `Daemon.TrustedProxies`. `reverse-proxy` mode rejects loopback final-hop proxying on purpose. + For Docker deployments, bind to loopback explicitly: ```bash @@ -143,7 +146,7 @@ Changing exposure mode requires a daemon restart. It's excluded from hot-reload netclaw daemon stop && netclaw daemon start ``` - +In `reverse-proxy` mode, daemon-host CLI access is no longer "loopback means trusted." Local control-plane requests may still work, but they do so through explicit paired-device auth when the exposure mode requires it. ## Slack Channel Restrictions diff --git a/src/content/docs/security/security-model.md b/src/content/docs/security/security-model.md index c4a303c..6d65085 100644 --- a/src/content/docs/security/security-model.md +++ b/src/content/docs/security/security-model.md @@ -172,12 +172,15 @@ Exposure mode controls network reachability. It's separate from audience and pos | Mode | Scope | Requires | |------|-------|----------| | `local` | Loopback only | Nothing (default) | +| `reverse-proxy` | Whatever your proxy exposes | Trusted proxy config + non-loopback final hop | | `tailscale-serve` | Your tailnet | [`tailscaled`](https://tailscale.com/kb/) | | `tailscale-funnel` | Public internet | [`tailscaled`](https://tailscale.com/kb/1223/funnel/) | | `cloudflare-tunnel` | Public internet | [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) | Internet-reachable modes force explicit confirmation during [`netclaw init`](/cli/init/) and trigger high-risk diagnostic warnings. Changing exposure mode requires a daemon restart — not hot-reloaded. +`reverse-proxy` has an extra trust-boundary rule: loopback auto-auth is not inherited through the proxy path. A reverse proxy can front netclaw, but the final hop into the daemon must be a non-loopback internal address and the proxy source must be explicitly trusted. + A Personal-posture deployment exposed via Tailscale Funnel is reachable from the internet but still applies Personal audience rules to TUI sessions and Team rules to Slack. Exposure and audience are orthogonal. ## Fail-Closed Guarantees From 44bafc13d46f9a0f1b3fdb09c2d08ad9f0b4c157 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Wed, 6 May 2026 15:39:07 +0000 Subject: [PATCH 2/2] Update homepage hero copy to match brand positioning Sub-tagline and descriptor now align with the locked positioning hierarchy: "Simple, secure, reliable agents." replaces the old "Your always-on chief of staff" tagline, and the descriptor uses channel-agnostic language consistent with the OG/Twitter metadata. --- src/pages/index.astro | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/index.astro b/src/pages/index.astro index d94e3fa..d614376 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1540,11 +1540,10 @@ Open source · Apache 2.0

Run your own
agent.

-

Your always-on chief of staff.

+

Simple, secure, reliable agents.

- An AI agent that runs on your hardware, connects to Slack and Discord, - and does whatever you delegate to it — research, triage, reporting, the stuff - you never get to. Not a coding assistant. Built with .NET. Open source. + An AI agent that runs on your hardware, connects to the communications + tools you and your team use — Slack, Discord, and more. Built with .NET. Open source.