Cairn is a personal, hobby project maintained by one person. Security work here is best-effort, but reports are genuinely welcome and taken seriously.
Cairn is self-hosted and single-user, and it ships with no authentication by default. It is designed to run on a network you already trust:
localhoston your own machine, or- a home LAN, or
- a private overlay network (Tailscale/MagicDNS, WireGuard, or another VPN).
If you want a thin extra layer, you can set a shared bearer token via the
CAIRN_AUTH_TOKEN environment variable to gate the REST API (/api/*), the
MCP surface (/mcp), and the in-app agent-login WebSocket. This is a convenience
guard, not a substitute for network isolation.
When you connect a coaching CLI from Settings → Agents → Connect, the login
runs in a real terminal on the server and is streamed to your browser over a
WebSocket (/api/agent-login/ws). That endpoint is gated by the same
CAIRN_AUTH_TOKEN (and the optional per-IP rate limit) as the rest of /api, the
login command is chosen server-side from an allowlist (never from the browser),
and the login subprocess has Cairn's own secrets (CAIRN_AUTH_TOKEN,
GARMIN_PASSWORD) stripped from its environment — it authenticates only to the
external provider, never to Cairn. Keep it on a trusted network like everything else.
The safest posture is to never expose Cairn's port to the public internet. Keep it behind your firewall / VPN / tailnet. Do not put it on a public IP or a public-facing reverse proxy without your own authentication in front of it. If you run Cairn on a cloud VM, bind the port to localhost or a private interface and reach it through Tailscale, WireGuard, an SSH tunnel, or an authenticated reverse proxy.
Cairn stores personal data, so an exposed instance is a real privacy risk:
- Personal health data — bodyweight, training logs, nutrition, uploaded lab
documents and extracted markers, family details, and free-text notes live in
the
data/volume (SQLite database plus uploaded files). - Live OAuth tokens for the coaching CLIs (Claude Code, Codex, etc.) live in
the
cairn-homevolume. Anyone who can reach those credentials could act as you against those services.
Treat both volumes as sensitive. Back them up privately and never commit them.
The Family roster inside Cairn is coaching context, not an account or access
boundary. Do not share one Cairn instance, bearer token, database, or provider
home between people who need private profiles. Run one isolated Compose project
per person with distinct cairn-data, cairn-home, cairn-tools,
CAIRN_AUTH_TOKEN, and CAIRN_SETTINGS_SECRET_KEY values. Bind each host port
to loopback and expose a distinct private HTTPS origin through Tailscale Serve.
Each person should also use their own Tailscale identity and, unless a provider's
terms explicitly permit otherwise, their own coaching-provider login. See the
full deployment and removal procedure in docs/HOUSEHOLDS.md.
Cairn can install or refresh third-party coaching CLIs inside the container. Treat that as executing vendor code on the Cairn host:
- Claude Code and Codex are installed from npm at explicit versions by default,
not from
@latest. BumpCLAUDE_CODE_VERSIONorCODEX_CLI_VERSIONdeliberately when rebuilding or runningcairn-update-agent-clis. - Moving npm tags such as
latestare refused unlessAGENT_CLI_ALLOW_MOVING_TAGS=1is set for a one-off update. - Antigravity and Grok use vendor shell installers. Cairn skips those installers
unless a matching
ANTIGRAVITY_INSTALL_SHA256/GROK_INSTALL_SHA256is provided, or you explicitly setAGENT_INSTALL_ALLOW_UNVERIFIED=1. - The release workflows pin external GitHub Actions to commit SHAs. When updating action versions, resolve the new tag to a SHA and update the adjacent version comment in the workflow at the same time.
Only run automatic CLI updates on a trusted local or tailnet deployment. Do not enable them on an internet-exposed instance.
There are two very different ways to put Cairn on your phone, and they have very different security properties:
- Tailscale Serve (recommended) — private. HTTPS on your tailnet's MagicDNS
name. Nothing touches the public internet; only your own signed-in devices can
reach it. Your phone needs Tailscale installed and signed in. This is the
supported, low-risk path (see
docs/DEPLOYMENT.md). - Tailscale Funnel / any public reverse proxy — public. Reachable from any
browser with no client install, which is convenient — but it places a personal
health app on the public internet. Do not do this without a token. If
you choose this path,
CAIRN_AUTH_TOKENis mandatory, not optional, and you should setCAIRN_REQUIRE_AUTH=1(below) so the instance cannot boot without one.
If anything other than your own machine can reach the port (a LAN, a tailnet, a public proxy, a "public" cloud-sandbox port), do all of these:
- Set a strong token.
CAIRN_AUTH_TOKEN=$(openssl rand -hex 32). - Fail closed. Set
CAIRN_REQUIRE_AUTH=1so Cairn refuses to start without a token — it can't silently come up unauthenticated after a config slip. - Keep the rate limit on (it activates automatically when a token is set;
leave
CAIRN_RATE_LIMITat its default to blunt token-guessing). - Serve HTTPS (Tailscale Serve or a private reverse proxy) — never plain HTTP over an untrusted network.
- Prefer private reach (tailnet / VPN / SSH tunnel) over a public URL whenever
you can. Bind the published port to loopback (
127.0.0.1:8787:8787) and let the tunnel terminate TLS.
Only the latest released image is supported. Fixes land on the newest release; there are no back-ported patches for older tags.
Please report security issues privately by email:
- Do not open a public GitHub issue or pull request for a security report.
- Include enough detail to reproduce (affected version/tag, steps, impact).
- Expect an initial response within a few days. As a solo hobby project, triage and fixes are best-effort, but I'll do my best to address real issues.
Thank you for helping keep Cairn and its users safe.