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 |
| Has per-secret grants | yes | no (scope-based) |
| Has per-project allowlist | implicit (project memberships) | explicit |
| 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 scopes. It does not give the attacker any way to authenticate as a machine and read stored secrets.
Authorization
Every AI agent holds a flat set of scopes granted at provisioning time. Scopes are validated against an allowlist on the server. Each route declares the scope it requires, and the agent's request is rejected with HTTP 403 if the scope isn't present.
Vault-level scopes
| 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. Render CI/CD templates. |
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. |
team.read | List team members and pending invites. |
team.write | Invite, remove, configure permissions. |
support.write | Open and reply to support tickets (read access included). |
Project-context scopes
These scopes' routes carry a {projectId} path parameter and additionally consult the agent's project allowlist.
| Scope | Unlocks |
|---|---|
projects.read | List projects, read permissions. |
projects.write | Create / update / delete projects. |
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. |
Project allowlist
In addition to scopes, an agent can be configured with an explicit project allowlist. If the list is non-empty, project-context routes are filtered to those project ids. The agent gets HTTP 403 on project-context operations against any project not in the allowlist.
If the list is empty, project-context routes apply to every project the vault owner has, including projects created in the future.
When an agent with a non-empty allowlist creates a new project, the new project is automatically added to its allowlist so it can manage what it just created.
Privilege escalation guard
The MCP surface deliberately omits two operations that the dashboard exposes:
PUT /ai-agents/{id}/scopes(replace an agent's scope set)PUT /ai-agents/{id}/allowlist(replace an agent's project allowlist)
Exposing either would let an agent grant itself, or a peer, scopes or projects it doesn't already hold. Both remain dashboard-only.
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 server-side with envelope encryption (a per-secret AES-256-GCM data key wrapped by a per-project master key, which is itself encrypted by the in-memory server unseal 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 allowlist are preserved; the agent can be re-enabled without re-provisioning.
- Revoke: agent record is deleted. Scopes, allowlist, 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.
- Bounded blast radius. Scopes are explicit and narrow. The project allowlist constrains where the scopes apply. Both are configured at provisioning time by the vault owner and cannot be self-modified.
- 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 a human admin with the same scope set: 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 and the dashboard-side revocation primitives.