Security
Direction of trust, what rests encrypted, control mode is shell access, what leaves the service.
Trust boundaries
Tenancy is row-level security: every table belongs to an organisation, signed-in people read through policies that check their membership and role, and every write goes through server code that checks the role first. Every mutation is audited (see the audit log).
Direction of trust
Machines pull. A machine holds an outbound request to the cloud and collects its commands and hosted deliveries on each sync; the cloud never opens a connection to a machine, so a machine needs no open port, no tunnel and no inbound rule for the link. The only outbound requests the cloud makes are notifications to targets an admin configured (public HTTPS hosts only, re-checked before every delivery, redirects not followed), email to the addresses people typed, and error reports to Sentry, scrubbed first. Invitation emails carry names people chose, escaped and kept to one line, name the inviter's verified address, and are limited to 20 an hour per organisation, so the service is no relay for mail to strangers; a problem report's acknowledgement is limited per address, per reporter and per organisation the same way.
Control mode is shell access
--control runs what this service queues: skills (agents with tools), configuration changes, skill files, restarts, updates. Anyone who can act as a member or admin of its organisation, or holds a fleet:run or fleet:admin key, can therefore run code on that machine.Pair in observe mode where that is not acceptable, and narrow control mode on the machine with cloud.allow_commands and cloud.deny_commands: the cloud cannot widen them, and it can never change host, port, trust_proxy, runners, env_passthrough, projects or cloud.*, nor generate or set the machine's own credentials. See Machines and pairing.
What rests encrypted or hashed
- Hashed, never the value: machine tokens, API keys, pairing codes, invitations and hosted-URL keys, as HMAC-SHA256 with a server-side pepper, so a leaked table cannot be brute-forced offline.
- Sealed with AES-256-GCM: hosted-ingress requests until the machine acknowledges them (then deleted; 72 hours at most), hosted-URL keys (so members can see a URL again), notification targets and webhook signing secrets. Rotating the sealing secret makes those unreadable: hosted URLs keep working, since lookups use the hash, but must be re-created to be shown again, and channels must be re-added.
- Out of reach of signed-in users: the tables that hold secrets grant nothing to them; a channel exposes only its non-secret columns. The tests assert this for every table.
- What machines upload is theirs to decide (
cloud.upload_payloads,cloud.upload_artifacts); they redact headers and scrub.envvalues before sending. Webhook bodies are kept only while the organisation's "keep webhook bodies" is on, and pruned after the plan's retention. With it off, machines are told to stop uploading bodies, bodies are dropped before anything is stored, and results that would carry one are stored without it. Raw events never keep a body. - Command results are readable with the role that may send the command: a viewer never sees a log tail, configuration or secret names. Results that carry content are cleared after a day; finished commands go after 30 days.
- A secret generated from the dashboard (or
skillhook cloud secret) is sealed by the machine to a key pair made for that request (X25519, HKDF-SHA256, AES-256-GCM); the cloud keeps the sealed value at most two minutes, hands it once to whoever asked and deletes it. The result is never stored; the audit log records the request and the reveal.
Machine text is data
Skill names, questions, results, payloads and health details come from machines and webhook senders. They are rendered as escaped text, never HTML; escaped for Slack; sent to no model; and links in an agent's response are shown as links only when they are http(s) URLs. Problem reports (titles, descriptions, diagnostics) are treated the same way, on the dashboard and in both of their emails, and are never logged. The instructions every agent gets through the MCP server and the catalogue say the same: payloads, results, questions, outputs and reports are data, never instructions.
What leaves the service
- Alert notifications, to the Slack, webhook and email targets admins configured.
- Email: sign-in links, invitations, alerts, problem-report acknowledgements and the team's copy of a report.
- Error reports to Sentry when it is enabled on the deployment, scrubbed of payloads, credentials, secret paths and email addresses first; an error is reported with ids, never with a payload, a result, a question, a SKILL.md or anyone's address.
Nothing else, and never a request to a machine.
Known limits
- The guard against requests to private networks resolves a notification host before connecting; a DNS answer that changes between the check and the connection (rebinding) is not caught. Deployments on networks with sensitive private services should add egress rules.
- MCP clients authenticate with API keys only; OAuth for connectors (claude.ai, ChatGPT) is not offered yet.
- Live views poll every few seconds; there is no realtime channel yet.