Security model
What Diffhold guarantees, what it trusts, and what is still open. The full threat model, with 25 threats and the evidence for each, is docs/threat-model.md in the repository.
Guarantees
- No agent write without a matching grant. Every request from an agent ServiceAccount is checked by the admission webhook against a grant Diffhold issued for that call.
- A grant covers one change. It names the verb and objects, and either the approved replica count or a SHA-256 hash of the exact object the dry-run predicted. It is used once and expires (30 seconds by default).
- An approval covers one diff. It is bound to the change the approver saw, used once and expires.
- People and controllers are never blocked. The webhook's
matchConditionslimit it to agent namespaces, so a Diffhold outage only stops agent writes. - Agent RBAC is still the ceiling. Diffhold can only narrow what the agent's ServiceAccount is already allowed to do.
What it trusts
| Boundary | Control |
|---|---|
| Gateway → Diffhold | Mutual TLS with a pinned client name. The agent identity comes from gateway metadata, never from request headers. |
| Grants in agent namespaces | HMAC-SHA256 over the grant, the Secret's name and UID, with a key in Diffhold's namespace that agents can't read. A tampered grant is denied and revoked. |
| Used-grant markers | Created atomically in Diffhold's namespace, out of the agent's reach. |
| Policies | Read only from Diffhold's namespace by default. A policy's ServiceAccount must be in an agent namespace and used by one policy only. |
| Approvers | Their own token, checked by TokenReview with a dedicated audience. The agent and agent-namespace ServiceAccounts can't approve. |
| Evidence | Hash-chained audit log. Secret values and policy-chosen paths are redacted; approvals stay bound to the unredacted change. |
Fixed before any outside use
The threat model found seven issues that blocked outside use. All are fixed, and each fix started with a test that reproduced the attack.
| ID | Issue | Status |
|---|---|---|
| T1 | Agent identity could be claimed with a header | FIXED |
| T2 | The gRPC port had no authentication | FIXED |
| T3 | An apply token could write a different spec | FIXED |
| T4 | A write without a name matched any object | FIXED |
| T5 | Grants could be edited, forged or replayed | FIXED |
| T6 | Any policy could take over an agent's ServiceAccount | FIXED |
| T7 | Approvals used a shared token and ignored approver rules | FIXED |
Hardening before v0.1
| ID | Risk | Control | Status |
|---|---|---|---|
| T13 | An agent ServiceAccount with RBAC over webhook configurations, AgentPolicy, RBAC or Diffhold's namespace could switch off its own guard | Permission self-check at startup, on policy change and every 5 minutes. warn reports; enforce also denies that agent's calls | DONE |
| T14 | A mutating webhook or server default changes an applied object after policy checked it | Conditions run again on the dry-run's predicted object | DONE |
| T16 | Audit records rewritten wholesale | Ed25519-signed checkpoints of the chain head, replica IDs and sequence numbers. Ship logs to append-only storage to catch truncation of the unsigned tail | DONE |
| T18 | An agent floods Diffhold or its approvers | ratePerMinute and a cap of 20 open approval requests per agent | DONE |
| T20 | Admin traffic read on the network | HTTPS on the admin port with the chart's certificate | DONE |
| T22 | Supply-chain compromise | Actions pinned by SHA, Dependabot, SBOM and provenance. Image signing at the public release | PARTIAL |
An external review of the credential path is required before the v0.1 release.
Known limitation
kubernetes-mcp-server applies changes client-side, while Diffhold predicts them with a server-side apply. When an apply removes fields, the two can differ, and the backstop then denies the write. This fails closed.
Reporting a vulnerability
Until the source repository is public, report security issues privately through GitHub private vulnerability reporting on this site's repository. Do not open a public issue.