Files
mcp-gateway-nexus/docs/modules/07-secrets-manager.md
drjones 8d3ffef920 docs: initial architecture and design for MCP Nexus
MCP Nexus is a self-hosted control plane for MCP servers — "Kubernetes for
MCP." Agents connect to one endpoint; Nexus discovers services, installs the
right MCP servers, aggregates them behind a namespaced router, secures access
with auth/RBAC, and heals/updates them via a reconcile loop.

This first commit is design-phase only (no runnable code yet):
- README.md            project front door + module map
- docs/ARCHITECTURE.md target design: tenets, two-plane split, reconcile
                       loop, domain types, storage, security, deployment
- docs/ROADMAP.md      phased delivery (foundations -> walking skeleton ->
                       discover+install -> secure -> operate -> extend/scale)
- docs/modules/01-20   one design doc per module, all cross-linked

Backend stack decision: Go (single static binary, embedded SQLite + embedded
React dashboard). Repo initialized in /root with a whitelist .gitignore so
only project files are tracked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-07 04:38:27 +00:00

128 lines
10 KiB
Markdown

# Secrets Manager
> Module 07 · Plane: Cross-cutting · Roadmap phase: 3
> Part of [MCP Nexus architecture](../ARCHITECTURE.md).
## Purpose
The Secrets Manager is Nexus's encrypted vault for the credentials that MCP servers need to talk to real services: API keys, passwords, SSH keys, tokens, and certificates. It stores every `Secret` (Architecture §7) **encrypted at rest** via envelope encryption, resolves **secret references** carried by [Recipes](15-smart-recipes.md)/config templates at install time, and hands the plaintext to the [Auto Installer](03-auto-installer.md) for **runtime injection** into the target MCP container (env var or file mount). It embodies tenet #5 and Architecture §9: secrets are **never returned to agents, never written into config sent to agents, and never logged** — unless an explicit policy permits it.
## Responsibilities
- Store `Secret` objects encrypted at rest in the **Secrets** store (Architecture §8), keyed by a stable name/ref.
- Perform **envelope encryption**: a per-secret data-encryption key (DEK) encrypts the payload; the DEK is wrapped by a master **key-encryption key (KEK)**. The KEK provider is pluggable (tenet #3): local master key (from file/env/OS keyring) first, with cloud KMS / HashiCorp Vault adapters later.
- Resolve **secret refs** (e.g. `secret://homeassistant/token`) to plaintext **only** at install/inject time, for the Installer, in memory.
- Provide **runtime injection** material to the Installer: env pairs or tmpfs-backed file mounts, so the plaintext lands inside the sandboxed container and nowhere else.
- **Rotate** secrets and re-wrap DEKs on KEK rotation without re-encrypting every payload; version secrets so an in-flight instance keeps working during rotation.
- **Audit every access** — who/what resolved which secret ref, when, and for which instance — to the append-only log (Architecture §9).
- Enforce a **redaction/leak policy**: refuse to serialize plaintext into any agent-visible payload, dashboard response, or log line; return refs or masked values instead.
## Non-goals
- **Authenticating callers to Nexus.** That is [Authentication](06-authentication.md); this module secures the credentials Nexus *uses*, not access *to* Nexus.
- **Deciding which tools a principal may use.** That is [RBAC](08-rbac.md).
- **Creating containers or templating config.** The [Auto Installer](03-auto-installer.md) injects; [Recipes](15-smart-recipes.md) declare refs. This module only stores, encrypts, and resolves.
- **Being a general end-user password manager or a public KMS.** It is scoped to MCP-server credentials.
- **Discovering credentials.** Discovery methods reference secrets by ref (Module 01); they do not populate the vault automatically.
## Interfaces
```go
// A stored secret. Payload is always encrypted at rest; plaintext exists only in memory.
type Secret struct {
Ref string // "secret://homeassistant/token" — the reference key
Type SecretType // apikey | password | ssh-key | token | certificate
Version int // incremented on rotation
CreatedAt time.Time
Policy AccessPolicy // who/what may resolve; may plaintext ever be revealed?
}
type SecretType string
// The store never returns Secret.plaintext except via Resolve, and never logs it.
type Store interface {
Put(ctx context.Context, ref string, plaintext []byte, t SecretType, p AccessPolicy) error
// Resolve returns plaintext IN MEMORY for injection; every call is audited.
Resolve(ctx context.Context, ref string, requester Principal) ([]byte, error)
Rotate(ctx context.Context, ref string, newPlaintext []byte) (version int, err error)
Delete(ctx context.Context, ref string) error
List(ctx context.Context) ([]SecretMeta, error) // metadata only — never plaintext
}
// Envelope encryption: a KEK provider wraps/unwraps per-secret DEKs (tenet #3).
type KEKProvider interface {
Name() string // "local", "aws-kms", "vault", ...
WrapDEK(ctx context.Context, dek []byte) (wrapped []byte, keyID string, err error)
UnwrapDEK(ctx context.Context, wrapped []byte, keyID string) (dek []byte, err error)
}
// What the Installer receives — injection material, not persisted plaintext.
type Injection struct {
Env map[string]string // VAR -> value (in-memory, container env)
Files map[string][]byte // mountPath -> content (tmpfs file mount)
}
type Injector interface {
// Resolve all refs in a rendered config into injection material for one instance.
Materialize(ctx context.Context, refs []string, instanceID string) (Injection, error)
}
```
HTTP/API surface (control-plane API, behind Auth, RBAC-gated; Admin/operator only):
- `POST /api/v1/secrets` — create a secret (write-only; plaintext in, never echoed back).
- `GET /api/v1/secrets` · `GET /api/v1/secrets/{ref}` — **metadata only** (type, version, created, last-accessed); never plaintext.
- `POST /api/v1/secrets/{ref}/rotate` — rotate; returns new version, not the value.
- `DELETE /api/v1/secrets/{ref}` — delete (guarded against deleting in-use refs).
- `POST /api/v1/secrets/rekey` — rotate the KEK and re-wrap all DEKs.
There is **no** endpoint, MCP tool, or dashboard view that returns a stored plaintext to a caller by default.
## Data
- **Reads/writes** the **Secrets** store (Architecture §8): `{ref, type, version, ciphertext, wrapped_dek, kek_key_id, iv, policy, created_at, last_accessed}` plus envelope-key metadata. Only ciphertext + wrapped DEKs are persisted.
- The **master KEK** is *not* stored in the database; it comes from the configured `KEKProvider` (local key material outside the DB, or an external KMS handle).
- **Reads** secret refs declared by [Recipes](15-smart-recipes.md)/config templates ([Registry](02-package-registry.md)) at install time.
- **Writes** every access to the **Audit** store (append-only) — ref, requester, instance, outcome; never the value.
Secret-ref resolution + injection flow (install time, control plane):
```
1. Reconciler decides to install an MCPInstance; Recipe/config template
carries refs, e.g. env HASS_TOKEN = secret://homeassistant/token.
2. Installer calls Injector.Materialize(refs, instanceID).
3. For each ref: Store.Resolve(ref, requester)
a. load {ciphertext, wrapped_dek, kek_key_id} from the Secrets store
b. KEKProvider.UnwrapDEK(wrapped_dek, kek_key_id) → DEK (in memory)
c. decrypt ciphertext with DEK → plaintext (in memory only)
d. audit {ref, requester, instanceID, outcome} — never the value
4. Materialize returns Injection{Env, Files} (plaintext held transiently).
5. Installer creates the sandboxed container with env pairs / tmpfs mounts.
6. Plaintext buffers cleared; it now lives only inside the container.
```
## Dependencies
- Injected by the [Auto Installer](03-auto-installer.md) into sandboxed containers ([Runtime](19-security.md)) at create time.
- Referenced by [Recipes](15-smart-recipes.md) and [Package Registry](02-package-registry.md) config templates via secret refs.
- Access requests carry a `Principal` from [Authentication](06-authentication.md); resolution policy may consult [RBAC](08-rbac.md).
- KEK providers are plugin points via the [Plugin System](11-plugin-system.md) (KMS/Vault adapters, Phase 5).
- Rotation events surface through [Notifications](13-notifications.md); leak-prevention is part of [Security](19-security.md) defense-in-depth.
## Failure modes & handling
| Failure | Behavior |
|---|---|
| KEK provider (KMS) unavailable | Cannot unwrap DEKs → new installs needing secrets **fail closed** and are retried; already-running instances keep their injected material (data plane unaffected). |
| Secret ref not found at install | Installer receives a typed `ErrSecretNotFound`; the instance is not created with a blank/placeholder credential; reconciler flags it for review. |
| Attempt to log/serialize plaintext | Blocked by a redaction wrapper; the value is masked (`****`) and a leak-attempt warning is audited. |
| Rotation mid-use | Versioned secrets: the running instance keeps its injected version; new/restarted instances get the new version; old version retired after drain. |
| KEK compromise suspected | `rekey` re-wraps all DEKs under a new KEK; payloads need not be re-encrypted; event audited + alerted. |
| Corrupt ciphertext / failed decrypt | Fail closed; surface as an integrity error; never return partial/garbage plaintext. |
| Delete of an in-use ref | Rejected unless forced; forcing warns that bound instances will fail on next restart. |
## Security notes
Honors Architecture §9 and tenet #5 as its core contract. **Encryption at rest** is envelope-based: unique per-secret DEK, DEK wrapped by a pluggable KEK/KMS; the master key never lives in the database. Plaintext exists **only transiently in memory** during `Resolve`/`Materialize` and inside the target sandboxed container (preferably tmpfs file mounts over env vars, since env is visible to child processes and some inspection paths). Secrets are **never** returned to agents, never placed in config sent to agents, and never logged — a redaction layer enforces this at the serialization boundary, and any attempt is audited. Every resolution is audited with requester + target instance. Access is RBAC/policy-gated; reading a stored plaintext back out is not a supported operation by default (write-and-inject, not read-back).
## Open questions
- Default injection mechanism: tmpfs file mounts vs env vars — trade off broad MCP-server compatibility against env-visibility risk.
- Policy grammar for the rare "agent may read this secret" exception — how is it expressed and how loudly is it audited?
- KEK rotation cadence and whether re-keying should be automatic on a schedule.
- Do we support external secret *sources* (read-through to Vault/KMS at inject time) in addition to storing our own ciphertext?
- Certificate lifecycle: does the Secrets Manager track expiry and drive renewal, or is that the [Update Manager](10-update-manager.md)?
## Milestone
Delivered in **Phase 3** (Secure). Thin slice first: local-KEK envelope encryption, `Put`/`Resolve`/`Rotate`, secret-ref resolution, and **runtime injection into a sandboxed container** by the Installer, with full audit and hard redaction on logs/agent payloads. KMS/Vault KEK providers are Phase 5 plugins. Contributes directly to the Phase 3 exit proof: a secret-backed MCP works without the secret ever appearing in any agent-visible payload or log.