Repository navigation
Security and Access
Nautelle can start agents on machines that hold your source code, GitHub credentials and paid model subscriptions — so those machines are the asset it protects. This page explains who can do what, how accounts work, what the audit log guarantees, why a compromised server still cannot do whatever it wants on a runner, and exactly what data leaves your machines.
flowchart TB
user["Person<br/>(cookie session, CSRF)"] -->|"role × project membership"| api["API"]
api -->|"typed command allowlist<br/>(ADR-0010)"| runner["Runner"]
runner -->|"local policy check<br/>(ADR-0047)"| exec["tmux · git · gh · claude"]
api --> audit[("Audit log<br/>hash-chained, append-only")]
subgraph machine["Your machine"]
runner
exec
creds["Claude / Codex / gh credentials<br/>never leave the machine"]
end
- A stolen user session or runner token turning into code execution on runners.
- A malicious skill from a public registry.
- A forged inbound webhook triggering runs.
- A teammate spending the owner's subscription.
Nautelle uses local accounts: email and password (argon2id), with a server-side cookie session and a CSRF token on every state-changing request.
-
The first admin is created from the command line — the seed creates no users:
bun run admin:create
It prompts, or reads
ADMIN_EMAILandADMIN_PASSWORDfrom the environment (set both, run, then remove them). In a container, see Self-Hosting. -
Registration is closed by default. An admin opens it under Admin › Users.
-
A new account waits as pending until an admin approves it and assigns a role in one action — or rejects it.
Changing your password revokes your other sessions.
Failed sign-ins slow the source down; they never lock the account. Failures are counted per (email, IP): after 5 in a row that source waits 1 second, doubling with each further failure. Anyone signing in from another address gets through at once, so a stranger cannot lock you out. When an account first crosses the threshold, every admin is notified, and refused sign-ins are written to the audit log.
Every request and every runner command is checked against the person's global role and their project membership.
| Role | Can |
|---|---|
| Admin | everything: users and registration, runners, integrations, prices, budgets, the audit log, terminal attach, profile-wide skill installs, AI providers |
| Operator | steer the projects they are a member of: run cards and skills, control the orchestrator, approve pull requests and action requests, create issues and schedules |
| Viewer | read the projects they are a member of, including session content and action-request input |
A project you are not a member of answers 404, as if it did not exist. Members are managed under Project › Settings › Members.
Every privileged action — sign-ins, user and role changes, runner commands, approvals, merges, budget overrides, schedule and webhook changes, terminal attaches, AI provider key changes — is written to an append-only audit log with the state before and after.
-
Hash-chained. Each record stores
sha256(previous hash ‖ record), so altering or removing a record breaks the chain. -
Append-only in the database. A trigger rejects
UPDATE,DELETEandTRUNCATEon the table. -
Verified nightly (03:00 server time by default,
AUDIT_VERIFY_CRON), and on demand. - Secrets are redacted before hashing: passwords, tokens, codes and secrets never enter a record.
- Admin › Audit lists, filters and verifies records and exports them as CSV.
The runner is the only party that can refuse what the server asks, and it does so in two layers.
- A typed command allowlist. The runner accepts only named commands with strict argument schemas — never a shell string. Paths are confined to registered project roots and their worktrees. Runners advertise which commands they support.
-
A local security policy the server cannot change. It lives in the runner's own config, and only
nautelle-runner policyon the machine writes it. It decides which directories may hold projects, the highest permission mode any launch may use (autoby default), and whether terminal typing, profile-wide skill installs and messages into running sessions are allowed (all off by default). A refusal names the rule and the exact command that lifts it. See Installing the Runner.
Other protections:
-
Runner tokens are 256-bit, issued once from a pairing code, stored hashed, revocable and never logged. Plain
http://is refused except for loopback, unless you explicitly pair with--insecure. -
Local hook and MCP calls reach the daemon only through a
0600unix socket and carry a per-session capability token. - Releases are signed with Ed25519. Homebrew and the npm shim verify the signature before installing or running the binary, and release secrets reach only protected tags.
- Text the runner sends — pane frames, logs, reports, PR bodies, hook content — passes one redactor for tokens, keys and credentials. It is pattern-based and best-effort.
Important
The local policy bounds the blast radius; it is not a sandbox. Below the permission cap, an agent in auto still does what its prompt asks within Claude Code's own permission checks.
With away mode on, Claude Code permission prompts become action requests in the web app and Telegram. Each one carries a redacted preview of the tool input (at most 1,000 characters, with a hash of the full input) so you know what you approve. Deciding needs the operator role; every decision is audited. An issue a session files for the fleet waits until a person approves queuing it.
| Data | Where it goes | When |
|---|---|---|
| Session metadata (names, ids, timing, token counts) | your Nautelle API | always |
| Session content (prompts, replies, tool I/O) | your Nautelle API, redacted on the runner | only for projects where an admin turned it on |
Agent credentials (Claude, Codex, gh) |
nowhere | never |
| Stored text people save (titles, bodies, feedback) | DeepL | only when DEEPL_API_KEY is set; otherwise a small model on the runner normalizes it |
| Assistant conversations and the data its tools read | the AI provider an admin configured (Anthropic, OpenAI, xAI, OpenRouter) | only when the assistant is used |
| Telegram voice messages | the configured speech-to-text service | only when it is configured; audio is never stored |
The control plane never reads a project's filesystem; the assistant's provider sees only what the API already stores or what you typed. Provider keys are sealed with AES-256-GCM under LLM_SECRETS_KEY; webhook secrets, the Telegram bot token and the GitHub App's secrets under APP_ENCRYPTION_KEY. Keep both keys out of database backups — either alone is useless.
Pull-request pipelines are untrusted: anyone who can push a branch, a card worker included, can open one. Steps that run on a pull request get no Docker socket, no host volume, no secret and no privileged container. See Contributing.
Decisions: ADR-0008 · ADR-0010 · ADR-0021 · ADR-0033 · ADR-0047 · ADR-0048 · ADR-0049 · ADR-0052 · ADR-0053 · ADR-0059 · Spec: #3, #8
Nautelle · Website · Discord · Engineering docs · MIT
Start
Work
Cost and health
Surfaces
Operate
Reference