feat(secrets): opt-in injected secret delivery, scoped per server
Completes the path that stops mcpd writing secret VALUES into MCP server
pod specs. With `secretDelivery: injector`, the pod fetches its own
secrets from OpenBao through the agent injector, under a ServiceAccount
and role scoped to just that server's secrets — so the value never enters
etcd, and gitea-mcp cannot read the Grafana token.
Opt-in per server, defaulting to `env`. Every existing server is
bit-for-bit unchanged, and migrating is one reversible decision at a time
rather than a flag day.
The two invariants under most risk, both tested:
- **Opted-out servers produce an identical manifest.** No annotations, no
serviceAccountName, automountServiceAccountToken still false.
- **Opted-in servers still fail LOUDLY on a bad ref.** Once mcpd stops
reading a server's secrets, the check e6cd735 added no longer fires for
it, and a typo'd secretRef would degrade into a vault-agent-init
crashloop that mcpd reports as a generic pod failure — the same class of
bug that had gitea-mcp running for weeks on an empty token while
reporting healthy. `validateServerEnvRefs` resolves every ref and throws
the value away, purely to keep that error. After the value cache it is a
cache hit and costs nothing.
Shell quoting is the other silent-failure trap and is treated as part of
the contract: the agent renders `export NAME='value'` and the container
command sources it, so a value containing a space, `$`, a quote or a
newline would truncate and yield an empty token. `shellSingleQuote` is
tested by executing a real /bin/sh over nine adversarial values including
`'; export PWNED=1; '` — and those tests fail against naive quoting,
confirmed before keeping them.
`sh -c <script> arg0 arg1 …` preserves argv via $0/$@, and `exec` keeps
PID 1 as the real process, which matters because mcpd attaches to PID 1's
stdin/stdout for STDIO servers.
Docker/Podman declare `capabilities.secretRefs: false` and fall back to
inline resolution, so local development is untouched. Deleting a server
revokes its identity, after its pods are gone and best-effort — a role no
pod can authenticate as grants nothing, and failing the delete over it
would strand the row.
Per the CLI rules, `secretDelivery`/`entrypoint` are `create` flags,
round-trip through apply -f, and show in `describe server` — which now
also flags servers still inlining secrets into their pod spec.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018vybEitX4FykeMatKe5Xki
This commit is contained in:
@@ -71,9 +71,23 @@ model McpServer {
|
||||
env Json @default("[]")
|
||||
healthCheck Json?
|
||||
volumes Json @default("[]")
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// How secret-backed env reaches the container.
|
||||
/// env — mcpd resolves values and writes them into the pod spec.
|
||||
/// Cleartext in etcd, readable by anyone with `get pod`.
|
||||
/// injector — the pod fetches its own secrets from OpenBao via the agent
|
||||
/// injector, using a ServiceAccount + role scoped to just this
|
||||
/// server's secrets. The value never touches the pod spec.
|
||||
/// Defaults to `env` so existing servers are bit-for-bit unchanged.
|
||||
secretDelivery String @default("env")
|
||||
|
||||
/// argv the injector wrapper must exec after sourcing the rendered secrets.
|
||||
/// Only needed for dockerImage servers using `injector`, where the image's
|
||||
/// own ENTRYPOINT is what would otherwise run and mcpd cannot introspect it.
|
||||
entrypoint Json?
|
||||
version Int @default(1)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
templateName String?
|
||||
templateVersion String?
|
||||
@@ -109,8 +123,22 @@ model McpTemplate {
|
||||
env Json @default("[]")
|
||||
healthCheck Json?
|
||||
volumes Json @default("[]")
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
/// How secret-backed env reaches the container.
|
||||
/// env — mcpd resolves values and writes them into the pod spec.
|
||||
/// Cleartext in etcd, readable by anyone with `get pod`.
|
||||
/// injector — the pod fetches its own secrets from OpenBao via the agent
|
||||
/// injector, using a ServiceAccount + role scoped to just this
|
||||
/// server's secrets. The value never touches the pod spec.
|
||||
/// Defaults to `env` so existing servers are bit-for-bit unchanged.
|
||||
secretDelivery String @default("env")
|
||||
|
||||
/// argv the injector wrapper must exec after sourcing the rendered secrets.
|
||||
/// Only needed for dockerImage servers using `injector`, where the image's
|
||||
/// own ENTRYPOINT is what would otherwise run and mcpd cannot introspect it.
|
||||
entrypoint Json?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@index([name])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user