Overview
This maintainer-facing overview describes the bao-kms-provider components, data flows, trust boundaries, and deployment shape. For the upstream protocol and Transit concepts, see Background
. For the Vault Transit KMS plugin work that informed the design, see Related Work
.
Purpose
The provider adapts Kubernetes KMS v2 to OpenBao Transit. Kubernetes talks to the local KMS provider plugin over gRPC. bao-kms-provider talks to OpenBao Transit over HTTPS.
kube-apiserver
-> Unix domain socket
-> bao-kms-provider
-> OpenBao Transit
-> bao-kms-provider
-> kube-apiserver
-> encrypted Kubernetes API resource data in etcd
The provider participates in Kubernetes envelope encryption for selected API resources. It does not encrypt raw etcd disk blocks or any workload storage outside the Kubernetes API resource persistence path.
Component Diagram
flowchart LR
API["kube-apiserver<br/>EncryptionConfiguration<br/>etcd storage path"]
Etcd["etcd<br/>encrypted API resources"]
StateFile["local registry state<br/>non-secret JSON"]
subgraph Plugin["bao-kms-provider"]
KMS["KMS v2 server"]
Registry["key registry"]
AAD["additional authenticated data (AAD)<br/>builder / validator"]
AuthManager["auth manager"]
TransitClient["Transit client"]
StatusCache["status cache"]
Observability["metrics / logging / health"]
end
subgraph Bao["OpenBao"]
BaoAuth["JSON Web Token (JWT)<br/>or cert auth method"]
Transit["Transit secrets engine"]
Audit["audit devices"]
end
API <-->|gRPC KMS v2<br/>Status / Encrypt / Decrypt<br/>Unix domain socket| KMS
API -->|stores ciphertext<br/>key_id / annotations| Etcd
KMS --> Registry
KMS --> AAD
KMS --> StatusCache
KMS --> TransitClient
Registry <--> StateFile
TransitClient --> AuthManager
AuthManager -->|login| BaoAuth
TransitClient -->|HTTPS<br/>TLS verify| Transit
Transit --> Audit
Observability -.->|observes| KMS
Observability -.->|observes| TransitClient
Components
kube-apiserver
kube-apiserver is configured with an EncryptionConfiguration that contains a KMS v2 provider entry. The provider name and endpoint participate in the encryption format and remain stable after encryption begins.
bao-kms-provider
The provider process:
- serves the Kubernetes KMS v2 gRPC API over a Unix domain socket,
- maintains the active key snapshot,
- persists non-secret registry state,
- returns cached KMS Status,
- validates decrypt
key_idand annotations, - constructs Transit associated data when enabled,
- authenticates to OpenBao,
- calls Transit encrypt and decrypt,
- exposes local health and metrics endpoints,
- produces structured redacted logs.
OpenBao
OpenBao provides:
- JWT or certificate authentication,
- short-lived OpenBao tokens,
- Transit key metadata,
- Transit encrypt and decrypt operations,
- audit records for cryptographic operations.
OpenBao must be available independently of the protected Kubernetes API server. Running OpenBao inside the protected cluster creates a bootstrap dependency during API server recovery.
Data Flow
Encrypt
sequenceDiagram
participant API as kube-apiserver
participant Plugin as bao-kms-provider
participant Registry as key registry
participant Transit as OpenBao Transit
participant Etcd as etcd
API->>Plugin: Encrypt(plaintext, uid)
Plugin->>Registry: select active KeySnapshot
Registry-->>Plugin: TransitVersion, KubernetesKeyID
Plugin->>Plugin: build annotations and AAD
Plugin->>Transit: encrypt(plaintext, key_version, associated_data)
Transit-->>Plugin: ciphertext
Plugin-->>API: ciphertext, key_id, annotations
API->>Etcd: store encrypted resource data
Encrypt does not use implicit Transit latest-version behavior. The provider passes the explicit Transit key_version from the active snapshot.
Decrypt
sequenceDiagram
participant API as kube-apiserver
participant Plugin as bao-kms-provider
participant Registry as key registry
participant Transit as OpenBao Transit
API->>Plugin: Decrypt(ciphertext, key_id, annotations, uid)
Plugin->>Plugin: validate key_id syntax
Plugin->>Registry: lookup historical KeySnapshot
Registry-->>Plugin: snapshot or reject
Plugin->>Plugin: validate annotations
Plugin->>Plugin: reconstruct AAD
Plugin->>Transit: decrypt(ciphertext, associated_data)
Transit-->>Plugin: plaintext
Plugin-->>API: plaintext
Decrypt does not brute-force unknown keys or try every historical key. Unknown key_id values fail before Transit is called.
Status
sequenceDiagram
participant API as kube-apiserver
participant Plugin as bao-kms-provider
participant Cache as status cache
API->>Plugin: Status()
Plugin->>Cache: read cached health/version/key_id
Cache-->>Plugin: cached status
Plugin-->>API: health, version, active key_id
Status reads from cached state populated by background probes; it does not perform a live Transit encrypt or decrypt on every call. The cache becomes healthy only after the metadata probe and the Transit encrypt/decrypt deep probe succeed. A successful probe does not clear a failure from the other probe type. Kubernetes polls Status regularly, and the Status key_id drives rotation behavior.
Trust Boundaries
The provider sits across these boundaries:
- Kubernetes API server to the local provider socket.
- Provider host process to OpenBao HTTPS endpoint.
- OpenBao policy boundary for Transit operations.
- Local host filesystem boundary for configuration, auth material, CA bundle, socket, and registry state.
- etcd persistence boundary for ciphertext and KMS annotations.
The provider sees plaintext material passing through KMS calls. Treat it as a control-plane critical component. For the full asset and threat catalog, see Threat Model .
Internal Active Key Model
type KeySnapshot struct {
ProviderName string
ClusterID string
OpenBaoInstanceID string
TransitMountID string
TransitKeyLineageID string
TransitVersion int
TransitVersionCreatedAt time.Time
CreatedAt time.Time
KubernetesKeyID string
State SnapshotState // active, pending, retired, rejected
AADMode AADMode // aad.required
}
The fields are non-secret identity and Transit metadata. The provider uses them
to derive Kubernetes key_id values and reconstruct AAD. A background key
watcher computes the active snapshot outside hot-path Status calls. The
implementation derives historical key_id values from stable configuration
and Transit metadata when possible.
A small local key registry state file with strict permissions persists rotation decisions across restarts. See Reference: Key ID And AAD: Local Registry State .
Implementation Guardrails
The implementation encodes design boundaries as local and CI checks before feature work begins.
ast-grep owns structural Go and architecture rules:
- no broad dynamic types in production code,
- no runtime panics,
- no root contexts in runtime packages,
- no Viper imports outside the configuration boundary,
- no environment reads outside the configuration boundary,
- no concrete OpenBao or Transit client imports from
internal/kmsv2.
Semgrep owns security and dangerous-API rules:
- no disabled TLS verification,
- no default HTTP client or package-level HTTP helpers,
- no
http.NewRequestwithout context, - no runtime subprocess execution,
- no sensitive log field names.
For the supporting policy see Development: Code Quality .
Startup Sequence
bao-kms-provider completes one successful metadata probe and one successful deep probe before it binds the Unix socket. Startup fails closed rather than exposing a socket without a verified active data path.
Recommended systemd sequence:
flowchart TD
A["host boot"]
B["network and DNS available"]
C["bao-kms-provider starts"]
D["provider reads config / auth material / CA"]
E["provider authenticates to OpenBao"]
F["provider reads Transit metadata"]
G["provider creates Unix socket"]
H["provider reports ready"]
I["kubelet starts kube-apiserver static pod"]
J["kube-apiserver connects to KMS socket"]
A --> B --> C --> D --> E --> F --> G --> H --> I --> J
Recommended static-pod sequence:
flowchart TD
A["host boot"]
B["kubelet starts"]
C["kubelet starts bao-kms-provider static pod"]
D["kubelet starts kube-apiserver static pod"]
E["provider creates socket"]
F["kube-apiserver connects or retries"]
A --> B
B --> C --> E --> F
B --> D --> F
Static-pod ordering must be tested because kubelet does not provide a strong dependency graph between static pods. The API server may start before the provider socket exists. It must retry while the provider completes bootstrap. See Deployment: Choosing A Model for the model selection rationale and Deployment: Static Pod Deployment for the manifest and bootstrap risks.
Multi-Control-Plane Operation
Each control-plane node runs its own local provider instance.
All instances share:
- the same provider name,
- the same cluster ID,
- the same OpenBao instance ID,
- the same Transit mount ID,
- the same Transit key lineage ID,
- the same Transit key,
- the same AAD policy,
- the same
key_idderivation algorithm.
Instances may have different auth credentials and OpenBao client tokens.
Each instance also owns a local registry state file. The active key_id converges across the files. Pending or recovered snapshots can differ temporarily during failover or rotation recovery.
Promotion of a new Transit key version is stable across all control-plane nodes. If one node promotes early and another does not, API server behavior can become inconsistent. The activation delay and stable observation count reduce this risk; operational monitoring still checks for key_id convergence. See Architecture: Rotation Model
.
OpenBao Placement
Recommended placement is an external management plane or otherwise independent OpenBao deployment that does not depend on the protected Kubernetes API server.
Avoid running OpenBao inside the same protected cluster for this use case. If the API server requires the KMS provider plugin to start and the provider requires OpenBao, then OpenBao must be reachable before the protected API server is healthy. A same-cluster OpenBao deployment introduces a circular dependency.