How Greentic changed the way digital workers get their passwords and API keys — so the secret values never get copied into Kubernetes.
A digital worker often needs secrets — a Telegram bot token, a Slack key, an LLM API key — to do its job.
The old way: when we deployed a worker to a Kubernetes cluster, we took the operator's local secrets file and copied the actual secret values into the cluster (as a Kubernetes "Secret" object). It worked, but it meant the real passwords now sat inside the cluster, where anyone with enough access could read them.
In the new model — now live — no secret values are shipped into the cluster. Instead:
Built as a "vertical slice": one backend (HashiCorp Vault) wired all the way through, then proven live in a real cluster. More backends can follow the same seam.
Added Kubernetes login to the Vault client: it takes the pod's ServiceAccount token and swaps it for a short-lived Vault token. No long-lived passwords baked in.
greentic-secrets · PR #94 · mergedThe worker runtime can now read secrets from Vault at run-time (read-only). Added a "vault" backend option and scoped each worker to its own tenant/team so it can't read other teams' secrets.
greentic-start · PR #303 · mergedWhen you deploy a Vault-backed environment, the deployer renders the worker with its ID badge (ServiceAccount), the Vault address and role, opens a network path to Vault — and skips creating the secret object entirely. The runtime then picks the Vault backend from a single pod env var at boot. The traffic router gets no secret access (it never needs one).
gtc-dev-secrets object (E.3a).GREENTIC_SECRETS_BACKEND on the worker pod (E.3b).greentic-deployer · PR #392 (E.3a) · repo greentic-start · PR #305 (E.3b) · both mergedAn idempotent setup script that prepares Vault: turn on Kubernetes login, create a read-only policy scoped to one environment + tenant, and bind the gtc-worker badge to it (plus KV-v2 + transit). The permissions side of the handshake.
my_demos/k8s-vault-demo/vault-bootstrap.sh + vault.yamlA live demo in a local Kubernetes (kind) cluster with Vault running inside it. The real webchat-bot bundle deploys, the worker pulls its secret straight from Vault under its badge, and an inbound webhook returns HTTP 200 — while listing the cluster's secrets reveals nothing sensitive (no gtc-dev-secrets).
op secrets put, built on a raw-write primitive in the core).auth/kubernetes/login + KV read + transit-decrypt all show up in the Vault audit log.greentic-secrets · PR #97 (raw write) · repo greentic-deployer · PR #394 (seed Vault) + PR #395 (ref scoping) · all merged · demo my_demos/k8s-vault-demo/deploy.shThe first live run looked like a runtime bug: the worker made zero calls to Vault and the inbound webhook returned 401. It turned out none of the code was at fault — every layer was proven correct in isolation.
Fix: a one-line allow-vault-ingress-from-workers NetworkPolicy in the demo's vault.yaml. With it applied, the webhook returns HTTP 200 and the Vault audit shows the full login → read → decrypt chain. (A standalone in-cluster secret store like Vault sits outside the deployer's policy model — whoever deploys it must open its ingress.)
Secret values never sit in the cluster or its database. A leak of cluster access no longer means a leak of every password.
Vault adds rotation, leases and an audit trail — who fetched what, when. The dev-store had none of that.
It reuses the worker's existing secret:// lookups — we just point them at a real backend instead of a local file.
Only the worker that uses secrets gets the badge. The traffic router stays untouched.
| Mechanism | Workload identity + runtime resolution (not External Secrets Operator, not CSI driver). |
| First backend | HashiCorp Vault with Kubernetes auth — chosen because it runs fully in a local kind cluster end-to-end. |
| The badge | A Kubernetes ServiceAccount (gtc-worker); its projected token is exchanged at auth/kubernetes/login for a Vault token. A pod under the default SA is refused. |
| Deployer → runtime contract | The worker pod env carries GREENTIC_SECRETS_BACKEND=vault plus VAULT_ADDR / VAULT_K8S_ROLE (and KV mount/prefix when non-default). Non-secret values only. |
| Selection | Driven by the environment's bound Secrets pack: greentic.secrets.dev-store vs greentic.secrets.vault. Unknown kinds fail closed. |
| Seeding | Admin-side op secrets put writes a transit-wrapped envelope (AES-256-GCM data key wrapped by Vault transit) — a raw vault kv put would leak plaintext, so it isn't used. |
| Read-only at runtime | The worker resolves under a read-only Vault role; writes/deletes are rejected. |
| KV path | {mount}/data/{prefix}/<env>/<tenant>/<team>/<category>/<name>; the webhook ref's tenant is the environment owner. |