Static Pod Deployment
Static pod deployment is appropriate for kubeadm-style environments and image-based control-plane management. It keeps the provider under kubelet management alongside the API server. Kubelet, the container runtime, and local image availability are therefore part of the KMS provider boot path.
For the model selection rationale see Deployment: Choosing A Model . For the user, group, and file ownership model see Deployment: Linux Identity Model .
Constraints
Kubelet reads a static pod manifest from the host filesystem. Static pods cannot depend on Kubernetes API objects such as ConfigMaps, Secrets, or ServiceAccounts. The protected API server may need the KMS provider before those API objects are reachable.
The provider static pod mounts everything it needs from the host:
- configuration file,
- certificate authority (CA) bundle,
- configured auth material such as a JSON Web Token (JWT) file, certificate chain, or PKCS#11 personal identification number (PIN) file,
- runtime socket directory,
- optional local state directory.
Example Manifest
The maintained sample manifest lives at
deploy/static-pod/bao-kms-provider.yaml. Replace the placeholder image digest
with the verified digest from the selected release. Replace the supplemental
group ID (GID) before deploying. Do not deploy a tag-only image reference.
apiVersion: v1
kind: Pod
metadata:
name: bao-kms-provider
namespace: kube-system
labels:
app.kubernetes.io/name: bao-kms-provider
app.kubernetes.io/component: kms-provider
spec:
# Required during early control-plane boot because the Container Network
# Interface (CNI) may be unavailable when the provider must reach OpenBao.
hostNetwork: true
priorityClassName: system-node-critical
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
supplementalGroups:
# Replace with the host openbao-kms-socket GID from:
# getent group openbao-kms-socket
- 1234
seccompProfile:
type: RuntimeDefault
containers:
- name: bao-kms-provider
# Replace with the verified image digest from the selected release.
image: ghcr.io/dc-tec/bao-kms-provider@sha256:0000000000000000000000000000000000000000000000000000000000000000
imagePullPolicy: IfNotPresent
args:
- serve
- --config=/etc/openbao-kms/config.yaml
ports:
- name: metrics
containerPort: 8081
protocol: TCP
- name: health
containerPort: 8082
protocol: TCP
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: config
mountPath: /etc/openbao-kms/config.yaml
readOnly: true
- name: tls
mountPath: /etc/openbao-kms/tls
readOnly: true
- name: jwt
mountPath: /var/lib/openbao-kms/identity.jwt
readOnly: true
- name: run
mountPath: /run/openbao-kms
- name: state
mountPath: /var/lib/openbao-kms/state
livenessProbe:
httpGet:
host: 127.0.0.1
path: /live
port: 8082
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
host: 127.0.0.1
path: /ready
port: 8082
initialDelaySeconds: 5
periodSeconds: 10
volumes:
- name: config
hostPath:
path: /etc/openbao-kms/config.yaml
type: File
- name: tls
hostPath:
path: /etc/openbao-kms/tls
type: Directory
- name: jwt
hostPath:
path: /var/lib/openbao-kms/identity.jwt
type: File
- name: run
hostPath:
path: /run/openbao-kms
type: Directory
- name: state
hostPath:
path: /var/lib/openbao-kms/state
type: Directory
The final manifest depends on the host socket GID and the released
image digest recorded for the selected release. The sample uses user ID (UID)
and GID 65532:65532, matching the distroless non-root image user.
Pod Hardening
| Setting | Purpose |
|---|---|
hostNetwork: true | Avoids CNI availability as an early-boot dependency. |
automountServiceAccountToken: false | Prevents accidental dependency on protected-cluster ServiceAccount tokens. |
runAsNonRoot: true and runAsUser: 65532 | Runs as the distroless non-root image user. |
supplementalGroups | Gives the container access to the host socket group without exposing provider auth material to the API server. |
seccompProfile: RuntimeDefault | Uses the runtime default syscall filter. |
allowPrivilegeEscalation: false | Blocks privilege escalation inside the container. |
readOnlyRootFilesystem: true | Forces writes into explicit hostPath mounts. |
capabilities.drop: [ALL] | Runs without Linux capabilities. |
| immutable image digest | Prevents image drift during recovery. |
| liveness and readiness probes | Lets kubelet report provider process and dependency health. |
Image Availability
For air-gapped or bootstrap-sensitive control planes, preload the image on every control-plane node:
- use immutable image digests from the selected release,
- avoid
Alwayspulls in recovery-sensitive deployments, - keep the previous image available for rollback,
- document image import steps for node replacement.
imagePullPolicy: IfNotPresent is appropriate only when the exact digest has already been imported or is reliably pullable during node recovery. Do not rely on tag movement for upgrade or rollback.
Host Preparation
Every control-plane node must have:
/etc/openbao-kms/config.yaml
/etc/openbao-kms/tls/ca.crt
/var/lib/openbao-kms/identity.jwt
/etc/openbao-kms/client/client-chain.pem
/etc/openbao-kms/pkcs11/pin
/var/lib/openbao-kms/state
/run/openbao-kms
The JWT path is needed only for auth.method: jwt. PKCS#11 certificate-auth
deployments must instead mount the configured certificate chain, PKCS#11 PIN
file, and PKCS#11 module path.
The API server must be able to access the socket under /run/openbao-kms. The
container user must own the socket directory, or an equally narrow
provider-only identity must be the only writer. The API server socket group
needs execute permission on the directory and write permission on kms.sock.
It must not have write permission on the directory.
The provider configuration used by the static pod sets server.socketGroup to the same numeric host GID listed in supplementalGroups. See deploy/config/provider-static-pod.yaml for the matching configuration sample.
Every provider in a multi-control-plane cluster must use the same identity-bearing configuration: provider name, cluster ID, OpenBao instance ID, Transit mount ID, key lineage ID, Transit mount path, and Transit key name. Multi-control-plane validation exercises this model with one node-local provider per API server.
kubeadm Placement
Typical kubeadm static pod path:
/etc/kubernetes/manifests/bao-kms-provider.yaml
The kubelet watches this directory and starts the static pod.
Bootstrap Risks
Static pod mode depends on:
- kubelet,
- the container runtime,
- local image availability,
- hostPath mounts,
- container networking and DNS,
- file permissions inside the container.
If kubelet or the container runtime is broken, the provider may not start and the API server may be unable to decrypt existing resources.
The provider retries its initial metadata and deep probes for
bootstrap.graceTimeout before exiting. Keep this setting enabled for static
pod deployments because auth material, container networking, DNS, OpenBao
availability, and clock synchronization can settle after the container starts.
For single-node control planes, systemd is usually safer. See Deployment: Choosing A Model .
Verification
Before enabling API server encryption:
- Place the static pod manifest under
/etc/kubernetes/manifests/. - Confirm the pod is running through kubelet or container runtime tooling.
- Confirm
/run/openbao-kms/kms.sockexists on the host. - Run
bao-kms-provider doctoron the host or in an equivalent debug container. - Confirm
kube-apiservercan connect to the socket.
After the Kubernetes EncryptionConfiguration is staged, include it in doctor:
bao-kms-provider doctor \
--config /etc/openbao-kms/config.yaml \
--encryption-config /etc/kubernetes/encryption-config.yaml
Before enabling API server encryption, the pod is running, the socket exists
with the configured owner and mode, and the API server can connect to it. The
doctor command must exit with status 0 and must not report a [fail] check.