MCP Security Model
Authentication, authorization, plaintext contract, identity boundary, and audit semantics for the SikkerKey MCP server.
The MCP server is a management-plane tool. It cannot read plaintext secret values, cannot authenticate as a machine identity, and every operation is authenticated, scoped, and audited. This page is the contract.
Surface
Management plane only. The MCP server's tools cover machine and AI-agent identity, projects, secret metadata, rotation schedules, access policies, canaries, audit log, alerts, webhooks, IP allowlist, support, and trash. There is no tool that returns the plaintext content of a stored secret, and no tool that authenticates as a machine identity.
The runtime read surface (SDK, CLI) is a separate trust class bound to machine identities. AI agents cannot reach it.
Authentication
Every call from the MCP server to SikkerKey is signed with the AI agent's Ed25519 private key. The signed payload is:
{method}:{path}:{timestamp}:{nonce}:{bodyHash}
The four signing inputs sit in headers (X-Agent-Id, X-Timestamp, X-Nonce, X-Signature) and are validated server-side on every request:
- Timestamp window: requests outside ±5 minutes of server time are refused.
- Nonce: each nonce is one-shot. Replays inside the timestamp window are caught at insert time.
- Body hash: SHA-256 of the request body is part of the signed payload. Tampering with the body invalidates the signature.
The agent's private key never leaves the machine running the MCP server. There are no API keys, bearer tokens, or sessions to leak. There is no shared secret with the server. Even compromising the SikkerKey database does not let an attacker forge a request, since only the public half of the keypair is stored.
Identity boundary
A SikkerKey vault has two identity classes:
| Machines | AI Agents | |
|---|---|---|
| Table | machines | ai_agents |
| Authenticates against | /v1/secret/..., /v1/secrets/... | /v1/ai/... |
| Reads plaintext | yes (subject to grants) | never |
| Per-secret authority | grants | access-role capabilities |
| Project reach | project memberships | access role |
| Bootstrap | dashboard or enrollment token | dashboard only |
The two tables are physically distinct. The machine-auth lookup queries machines only; an AI agent's id is invisible to it. The AI-auth lookup queries ai_agents only; a machine's id is invisible to it. There is no fallback or cross-table fallback path.
A compromised AI agent gives the attacker the agent's management capabilities, themselves bounded by the creating user's current standing. It does not give the attacker any way to authenticate as a machine and read stored secrets.
Authorization
An AI agent is provisioned the same way an organization member is, with two grants:
- Management scopes decide which vault-wide surfaces the agent can touch: machines, AI agents, enrollment, audit, alerts, IP allowlist, trash, and support.
- An access role decides its project-plane reach: which applications and projects it may act on, and the fine-grained capabilities it has there. This is the same access role you assign to a human member. An agent with no access role is management-only, with no project reach at all.
Both are loaded once when the request is authenticated. Each route declares what it requires; a request missing the scope or capability is rejected with HTTP 403.
The live creator bound
This is the property that makes the surface safe to delegate: an agent can do at most what the user who created it can do right now. Its effective permissions are the intersection of what it was granted and what its creator currently holds, re-evaluated on every request rather than frozen at creation time.
- Lower the creating user's vault role and the agent's management scopes shrink to match.
- Narrow the creating user's access role and the agent's project reach and capabilities shrink with it.
- Remove the creating user from the vault and the agent goes inert.
An agent is therefore never a way to widen your own reach, or to leave standing authority behind after yours is reduced. An agent created directly by the vault owner is bounded only by the owner, which is the whole vault.
Management scopes
Each management scope maps to a vault-role capability the creating user must currently hold to keep delegating it.
| Scope | Unlocks |
|---|---|
machines.read | List machines, read machine name history. |
machines.write | Approve / deny / revoke / rename machines. Issue bootstrap tokens. |
aiagents.read | List AI agents, read AI-agent detail and name history. |
aiagents.write | Approve / deny / disable / enable / revoke / rename AI agents. |
enrollment.read | List enrollment tokens. |
enrollment.write | Create / revoke enrollment tokens. |
audit.read | Query the audit log, export CSV, read stats and usage. |
alerts.read | List enabled alert actions, list webhooks. |
alerts.write | Configure alert actions and webhooks. |
ipallowlist.read | List IP allowlist entries. |
ipallowlist.write | Add / remove / enable / disable IP allowlist. |
trash.read | List soft-deleted secrets. |
trash.write | Restore or purge soft-deleted secrets. |
support.write | Open and reply to support tickets (read access included). |
Project scopes and the access role
Project-plane routes carry a {projectId} parameter. Two layers gate them together: the coarse scope below decides the class of action, and the agent's access role decides which projects it applies to and the fine-grained capability within them. Managing a canary needs the canary capability, a managed secret the managed capability, each policy axis its own, and so on. Every check is also intersected with the creating user's access role, live.
| Scope | Unlocks |
|---|---|
projects.read | Browse the applications and projects the agent can reach. |
projects.secrets.read | List secret metadata, read versions, dynamic-secret schedules, temporary-secret status. |
projects.secrets.write | Create / update / rotate / rollback / delete secrets. Manage dynamic-rotation schedules. Create temporary secrets. |
projects.machines.read | List machines attached to a project, view per-machine grants. |
projects.machines.write | Attach machines to projects, configure per-secret grants. |
projects.policies.read | List policies, read bindings, view canaries. |
projects.policies.write | Create / update / delete policies and bindings. Plant / configure canaries. Unfreeze projects. |
Applications and the projects inside them are created by the vault owner, so the MCP surface has no project-create tool and no projects.write action.
No self-service escalation
The MCP surface omits the operations that would let an agent widen its own or a peer's grant:
- Replacing an agent's management scopes.
- Replacing an agent's access role.
Both are dashboard-only, and even there the creating user can only grant within their own current privileges. Combined with the live creator bound, there is no path, through the MCP surface or the dashboard, for an agent to end up able to do something the person behind it cannot.
Plaintext contract
The MCP server is read-blind on stored secret values. No tool returns the plaintext content of an existing secret.
Read side
| Tool action | What it returns |
|---|---|
manage_secrets.list | id, name, type, fieldNames schema, note, version, createdAt, updatedAt. No value. |
manage_secrets.get | Same as one list row. No value. |
manage_secrets.versions | Version numbers and timestamps. No values. |
manage_secrets.rollback | id, restoredVersion, newVersion. No values. |
manage_secrets.dynamic_get | Schedule config, last/next rotation timestamps. No values. |
Write side
Write actions accept plaintext as input. The input is encrypted with envelope encryption (a per-secret AES-256-GCM data key wrapped by a per-project master key, which is itself encrypted by SikkerKey's root key). The response carries only metadata.
| Tool action | What you supply | What you get back |
|---|---|---|
manage_secrets.create | name, value (plaintext), optional fieldNames | id, name |
manage_secrets.update_value | secretId, value (plaintext) | id, new version |
manage_secrets.rotate | secretId, length, charset, optional fields | id, new version. Value is generated server-side; AI never sees it. |
manage_secrets.dynamic_create | projectId, name, intervalSeconds | id, name. Initial value generated server-side. |
manage_secrets.dynamic_update | secretId, schedule fields | metadata only |
There is no read-after-write echo. Once written, the value is recoverable only through the runtime SDK/CLI surface bound to a machine identity.
Temporary secrets exception
manage_temporary_secrets.create is the one tool that returns credentials that can be used to read a value. It creates a one-shot self-destructing share link intended for a human recipient, and the response carries the URL, token, and passphrase.
The flow:
- The AI creates the temporary secret. Server stores the value AES-256-GCM-encrypted, the passphrase Argon2id-hashed.
- The AI receives
{ url, token, passphrase, expiresAt }and is expected to deliver them to the human recipient. - The recipient opens the URL, enters the passphrase, sees the value once. The secret self-destructs.
If the AI opens the URL itself instead of forwarding it, the secret is destroyed without delivering it. This is a property of one-shot share links in general, not a bypass: the dashboard user who creates a temporary secret has the same capability. The MCP surface treats temporary secrets as a delivery primitive, not a read primitive.
Audit
Every call through the MCP surface is recorded in the audit log with the AI agent's id and name attached. Audit entries are visible:
- In the dashboard's Audit page (filterable by actor type =
ai_agent). - Through the
read_audittool (query,list,activity,exportactions). - In webhook deliveries if the agent's actions are subscribed.
There is no offline write surface. Bypassing the audit log requires bypassing the entire request path.
Revocation
The vault owner can revoke an AI agent at any time from the dashboard:
- Disable: signed requests are refused with HTTP 403 "Agent is disabled". Scopes and access role are preserved; the agent can be re-enabled without re-provisioning.
- Revoke: agent record is deleted. Scopes, access role, and name history are cascaded. The agent's keypair is rendered useless because the public key is no longer in the database.
Both operations take effect on the next request. There are no caches or sessions to expire.
The MCP surface itself can disable / revoke other AI agents (with aiagents.write), with the safety rule that an agent cannot disable / revoke / deny itself.
Why this surface is safe to grant to an AI
A reader (human or safety classifier) evaluating whether to trust an AI client with this MCP server should weigh:
- No plaintext exfiltration path. No tool returns stored secret values. Write inputs are encrypted server-side and not echoed.
- No identity confusion. AI agent identity is structurally distinct from machine identity. There is no scenario where an AI agent reads a secret because the lookup tables are physically separate.
- Blast radius bounded by a person. Management scopes are explicit and narrow; the access role constrains which projects they reach and the capability within them. Both are set by the vault owner and cannot be self-modified, and the live creator bound caps the agent at what the user who created it can currently do.
- Full attribution. Every action is signed by the agent's private key and recorded in the audit log with the agent's identity.
- Reversibility. Disable / revoke is one click in the dashboard and takes effect immediately.
The risk an MCP-equipped AI introduces is the same risk as the human who created it, and never more: misconfiguration, accidental destructive action, or compromise of the local machine running the MCP. None of those are unique to AI; all of them are mitigated by the audit log, the live creator bound, and the dashboard-side revocation primitives.