First injected pod
Audience: Application developer
Annotate your pod
Apply this manifest. It creates a ServiceAccount and a Pod in the team-myapp namespace using the myapp-prod Vault role configured in Vault policies and roles.
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: team-myapp
---
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: team-myapp
annotations:
db-creds-injector.numberly.io/cluster: database
db-creds-injector.numberly.io/myapp.role: myapp-prod
db-creds-injector.numberly.io/myapp.mode: classic
db-creds-injector.numberly.io/myapp.env-key-dbuser: DB_USER
db-creds-injector.numberly.io/myapp.env-key-dbpassword: DB_PASS
labels:
vault-db-injector: "true"
spec:
serviceAccountName: myapp
containers:
- name: app
image: postgres:16
command: ["sleep", "infinity"]
The vault-db-injector: "true" label is what triggers admission. The webhook sees the label, places opaque placeholders in the DB_USER and DB_PASS env vars, and the NRI plugin on the node substitutes the real credentials before the container starts.
Apply it:
If your application reads a single connection string from one environment variable, use URI mode instead. Replace the annotations with:
apiVersion: v1
kind: ServiceAccount
metadata:
name: myapp
namespace: team-myapp
---
apiVersion: v1
kind: Pod
metadata:
name: myapp
namespace: team-myapp
annotations:
db-creds-injector.numberly.io/cluster: database
db-creds-injector.numberly.io/myapp.role: myapp-prod
db-creds-injector.numberly.io/myapp.mode: uri
db-creds-injector.numberly.io/myapp.template: postgresql://@db.team-myapp.svc:5432/myapp?sslmode=require
db-creds-injector.numberly.io/myapp.env-key-uri: DATABASE_URL
labels:
vault-db-injector: "true"
spec:
serviceAccountName: myapp
containers:
- name: app
image: postgres:16
command: ["sleep", "infinity"]
Verify the credentials work
Expected output shows the real username and password, not a placeholder:
For URI mode, the env var is DATABASE_URL instead of DB_USER/DB_PASS.
Test the database connection:
kubectl -n team-myapp exec myapp -- bash -c \
'PGPASSWORD=$DB_PASS psql -h db -U $DB_USER -d myapp -c "SELECT 1"'
Expected: SELECT 1 returns a row. If the connection fails, check that the db hostname resolves from inside the pod and that the PostgreSQL server allows connections from the pod's IP.
What just happened
- The webhook admitted the pod and replaced
DB_USERandDB_PASSwith 64-hex placeholders. The PodSpec stored in etcd contains only the placeholders. - The NRI plugin on the node intercepted
CreateContainer, read the pod's annotations, and used the pod's ServiceAccount to log into Vault asmyappinteam-myapp. - Vault authenticated the TokenRequest JWT, verified the ServiceAccount against
auth/kubernetes/role/myapp-prod, and issued a short-lived database credential. - The plugin substituted the placeholders with the real username and password in the container env before
runcexecuted. - The renewer now holds the pod-token and will renew it periodically. When the pod dies, the revoker revokes the token and the DB credential.
For a diagram of the full data flow, see operators/architecture.
Next steps
- Read annotations reference to learn URI mode and multi-database injection.
- Read monitoring to wire Prometheus dashboards and alerts.
- Read security to harden the NRI DaemonSet.