How Sallyport protects your keys
Where a key is kept and when it opens, how accounts stay apart, what the network guard refuses and how the journal is signed.
On this page
- The key travels one segment: from Sallyport to the service
- Every stored record has its own key
- Accounts never see each other's data
- An agent cannot aim the gateway at the gateway
- Agents sign in with their own tokens
- A person decides where you ask, and on two actions always
- Every call is written down before it runs
- What happens to the answers your calls bring back
- Restarts and crashes never lose or repeat a call
The key travels one segment: from Sallyport to the service
One process holds the vault and the journal and makes the outbound call itself. The agent names the action. Sallyport puts the key on the request and sends it. The agent never receives the key.
- No route reads a key back. Not for an owner, not for support, not over the API. You can replace or delete a key; nobody can display it.
- A key goes only to the addresses it was saved with. A new connection, a new address or a new token endpoint for a saved key is refused: another address takes a new key, typed again. A catalog key also reaches the API hosts of its own service.
- A key opens for three things only. One is an agent's call that is cleared to go. Another is the check you start by saving a key or pressing "Check again". The third is reading the tool list of an MCP server you connected. Every opening is written to the journal before the key is used.
- A key stays bytes in memory and is wiped after the call. A Go string cannot be wiped, so a test fails the build if a key is ever converted to one.
Every stored record has its own key
The root key opens one key per account. From the account key, HKDF-SHA256 derives a key per kind of data: service keys, OAuth renewals, the requests agents send, webhook secrets. From that comes a key per record.
Each record is sealed with ChaCha20-Poly1305 and bound to its account, kind, record and version. Change any of the four and the record stops opening.
- The root key never sits in the database. It is a file on the gateway's host. Any two of three recovery shares rebuild it.
- The root key is tested every day. A daily job opens a real account key with it, and a failure reaches the operators at once.
Accounts never see each other's data
The gateway connects to the database as a role that owns no table and cannot bypass row-level security. Every query runs in a transaction that names the account and the workspaces it may see.
- A test fails the build if any table or journal partition lacks its policy.
- Your traffic rides your own connections. Sallyport keeps one HTTP transport per account and service host, so another account's request never reuses your connection.
An agent cannot aim the gateway at the gateway
Sallyport calls public internet services only. Before a connection opens, the gateway resolves the name and checks every address against a deny list compiled into the binary. Configuration can only add to that list.
- Refused whatever a connection says: private ranges, loopback, link-local and cloud metadata addresses, carrier-grade NAT, multicast, and every Sallyport hostname and network.
- The name is resolved again when the call runs, including a call that waited for a person. The address is checked and pinned at the moment of sending.
- Your key never follows a redirect. A 3xx comes back to the agent as the result. If the agent wants the new address, it makes a new call and every check runs again.
Agents sign in with their own tokens
Each agent has its own tokens, shown once when created. The database stores only an HMAC-SHA-256 of each token under a pepper kept outside the database. A copy of the database holds no working token.
- Cut an agent off in one press. "Pause agent" on its page refuses its next call, whatever token or sign-in it arrives with. Revoking a token under Tokens stops that one token. "Delete agent" at the bottom of its page retires it for good.
- MCP clients that sign in with OAuth get a Sallyport token after a person approves the client on a consent screen. The client never sees a service key.
- A platform that runs agents on its own machines connects as an access to manage agents, not as an agent. Its own token calls nothing: the MCP endpoint and the proxy refuse it. It gets a one-hour token per machine, after a person’s consent. Each such agent starts with no access, and only a person gives it any. Each token acts as that machine’s own agent and ends with the connection.
- The tool list differs per agent, so it is marked private. A client may not cache one agent's tools and show them to another.
A person decides where you ask, and on two actions always
By default every call runs without asking. Turn on "Ask every call" for an agent and a connection when you want to see each call first.
- The card shows the request Sallyport will send: method, host and path. The agent's own words sit in a separate block marked unverified.
- "Approve" covers one exact request. It is bound to a digest of that request, and the stored request is checked against it before sending. The card's grant button covers more: the same agent, connection, class and path, for a set time and number of calls.
- A DELETE cannot pass as a GET. Method-override headers and parameters are removed before the call is classed and before the digest.
- A rate or concurrency limit answers with a time to retry. It never raises a card, because nobody has a decision to make.
Every call is written down before it runs
The call's intent and its journal entry share one transaction with the decision to send. Sequence numbers come from a counter row that a rollback cannot skip. The numbering has no gaps, so a missing entry reads as a deletion.
- Each entry commits to the one before it. Its hash covers the scheme, the account, the sequence number, the previous hash and the event. The scheme is spc-chain/2.
- Each entry is signed with Ed25519. The public keys, active and retired, are published at
https://mcp.sallyport.cloud/.well-known/sallyport-audit-keys.json, and a retired key stays on that list after a key change. - The gateway cannot edit its own journal. Its database role may add entries and read them, and holds no right to update one.
- Events are stored as readable canonical JSON, and the chain is computed over those exact bytes. Anyone you hand the file to reads what you read.
- "Download journal" on the Activity screen saves the whole journal as JSON Lines, every entry with its hash and signature. Only a person who audits every workspace gets it. That is an owner, or an organization-wide admin or auditor with no narrower workspace role.
- Each journal entry belongs to the workspace of what it is about. The Journal tab and a webhook show each person the entries of workspaces they audit, plus the organization’s own. An organization admin who is only a member of one client’s workspace does not read that client’s entries.
What happens to the answers your calls bring back
An answer that fits goes straight to the agent. A longer one is stored so the agent can read the rest in pieces. Sallyport keeps what each call sent and got, so you read the whole call in Activity.
- A stored answer is readable by the agent whose call produced it and by people of your account who may read the journal. Every read is written to the journal.
- Stored answers, the answer each call got and request bodies over 1 MiB are deleted after 30 days. A large body is stored sealed under a key of its own.
- Call history, the request each agent sent and the journal are kept 180 days by default. Change it in Settings, Organization, "Journal and call history (days)".
- A browser notification names the agent, the kind of action and the service, with ids in the path replaced. It never carries argument values, a body or a key.
Restarts and crashes never lose or repeat a call
- A call waiting for a person lives in the database. A deploy does not drop it, and the agent continues with the same call id.
- If the process stops after a request left for the service, the call is marked as having an unknown outcome. Sallyport never retries it by itself, so a payment is never sent twice.