Zero Trust, Secrets & Supply Chain
A vulnerable image-parsing library lets an attacker run code inside one search pod. Now replay the default setup. From that pod, the attacker calls order with the header X-Account-Id: acc-42 and reads Ana's orders, because "only the gateway can reach order". They read the environment variables and find DATABASE_URL with the one password every service shares. They find PSP_API_KEY, because the payment key was put in a ConfigMap "temporarily". They open a connection to https://attacker.example and upload everything, because the private subnet has a NAT route and nothing restricts egress. Every step was possible because something trusted the network.
This page is about the architecture that removes that trust β Zero Trust β and about two places where trust hides in every system: secrets and the software supply chain.
From perimeter to Zero Trust
The perimeter model ("castle and moat") protects the edge β firewall, VPN β and trusts everything inside. Once an attacker is inside, through a stolen laptop, a phished password or a vulnerable dependency, they move from machine to machine: lateral movement.
Zero Trust removes the idea of a trusted network. Its motto is "never trust, always verify": every request β from the Internet, from the office, from the pod next door β is authenticated, authorized and encrypted on its own merits1. Google built its corporate network this way in the BeyondCorp project after 20112; NIST standardized the architecture in SP 800-2071.
flowchart LR
subgraph perimeter ["Perimeter model"]
fw["Firewall / VPN"] -->|"inside = trusted"| int["search, order, payment, DB<br/>(trust each other)"]
end
subgraph zt ["Zero Trust model"]
any["Any request<br/>(user or workload)"] -->|"identity + context"| pep["Policy Enforcement Point<br/>(gateway, mesh proxy)"]
pep -->|"ask"| pdp["Policy Decision Point<br/>(policies, identity, device, risk)"]
pdp -->|"allow, least privilege"| res["Resource"]
pdp -->|"deny"| rej["Rejected + logged"]
end NIST names the moving parts: a Policy Enforcement Point (PEP) sits in the path of every request and blocks or forwards it; a Policy Decision Point (PDP) decides from identity and context. In BoxOffice, the API gateway is the PEP for users, and the mesh proxy next to each service is the PEP for workloads (Service mesh).
Principles
| Principle | What it means | BoxOffice mechanisms |
|---|---|---|
| Verify explicitly | every user and every workload proves who it is on every request | OIDC tokens, mTLS with SPIFFE identities, signed webhooks |
| Least privilege | the minimum access, for the minimum time | narrow scopes, one DB user per service, short-lived credentials, egress allow-lists |
| Assume breach | design as if an attacker is already inside; limit the blast radius | default-deny NetworkPolicy, isolated subnets, per-service secrets, audit logs |
| Continuous evaluation | trust is re-checked, not granted once | short token lifetimes, re-authentication for sensitive actions, anomaly alerts |
The compromised search pod, again
| Attacker step | Perimeter model | Zero Trust control | Where |
|---|---|---|---|
Call order with a forged X-Account-Id | accepted β "only internal callers" | order accepts calls only from the gateway's mesh identity; header stripped at the edge | Service mesh, Identity |
| Connect to PostgreSQL with the shared password | works | search has no route/SG/NetworkPolicy to the DB, and no DB credential at all | Cloud networking |
Read PSP_API_KEY from the environment | it is there | the key exists only in payment pods, fetched from a secrets manager | Secrets |
| Exfiltrate to the Internet | NAT route, no egress rules | egress default-deny; only payment may reach the provider | Service mesh |
| Stay hidden | nobody looks at internal traffic | denied calls are logged and alerted | Observability |
The attack still started β Zero Trust does not prevent the first compromise. It makes the compromised pod a dead end.
Workload identity: no more long-lived keys
Zero Trust needs every workload to have an identity it can prove without a static secret. Three common forms:
| Workload | Identity mechanism | What it replaces |
|---|---|---|
| Pod β pod | SPIFFE ID in an mTLS certificate issued by the mesh or SPIRE, rotated hourly to daily | IP allow-lists, shared API tokens |
| Pod β cloud API (S3, Secrets Manager, KMS) | the Kubernetes service account federated to a cloud role: EKS Pod Identity / IRSA, GKE Workload Identity Federation, Azure Workload Identity | access keys in environment variables |
| CI pipeline β cloud | the CI system's OIDC token exchanged for short-lived cloud credentials | cloud access keys stored as CI secrets |
The third is a small change with a big effect: GitHub Actions can request an OIDC token that says "I am workflow release.yaml of this repository on branch main", and the cloud trusts that claim for a role valid only for the job's duration3:
permissions:
contents: read
id-token: write # allow the job to request an OIDC token
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/boxoffice-deploy
aws-region: sa-east-1
- run: aws eks update-kubeconfig --name boxoffice-prod
No AWS key is stored in GitHub; the role's trust policy accepts only tokens whose sub matches repo:<org>/<repo>:ref:refs/heads/main. A forked repository or another branch cannot assume it.
People are workloads too
For engineers, Zero Trust means: single sign-on with MFA or passkeys, device posture checks, and access through an identity-aware proxy or session manager instead of a VPN that puts the laptop "inside" (Cloud networking). Every access is per application and logged.
Secrets management
A secret is any value that grants access: the payment provider API key, database passwords, the signing key of the identity provider, webhook HMAC secrets, registry tokens. Secrets must not live where things get copied, versioned or logged: source code, container images, plain ConfigMaps, CI logs.
Kubernetes Secrets are base64, not encrypted
A Kubernetes Secret stores values base64-encoded. Anyone who can get Secrets in the namespace β or create a pod that mounts them β reads them in one command:
By default they are also stored unencrypted in etcd. Minimum hygiene: encryption at rest for etcd (with a KMS provider), tight RBAC on Secrets, and never committing Secret manifests to Git unencrypted4.
Where secrets should live
| Approach | Pros | Cons |
|---|---|---|
| Kubernetes Secrets only | simple, native | base64; rotation and audit are on you |
| Encrypted in Git (Sealed Secrets, SOPS) | GitOps-friendly, reviewed in PRs | rotation still manual; key management for the sealing key |
| Cloud secrets manager (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) | managed, versioned, audit log, rotation hooks, IAM per secret | tied to one cloud; per-secret and per-call cost |
| HashiCorp Vault / OpenBao | dynamic secrets, any cloud, fine-grained policies and audit | a critical, stateful system to operate |
The External Secrets Operator (ESO) bridges both worlds: the source of truth is the secrets manager; ESO syncs values into native Secrets that pods read as usual.
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
name: psp-api-key
namespace: boxoffice
spec:
refreshInterval: 1h
secretStoreRef:
kind: ClusterSecretStore
name: aws-secrets-manager
target:
name: psp-api-key # Kubernetes Secret created and kept in sync by ESO
data:
- secretKey: PSP_API_KEY
remoteRef:
key: prod/boxoffice/payment/psp-api-key
The life of the payment API key
The payment provider lets a merchant hold two active keys at once β the standard trick for rotation without downtime. The lifecycle of BoxOffice's most valuable secret:
sequenceDiagram
autonumber
actor Ops as Payments on-call
participant PSP as Payment provider
participant SM as Secrets Manager
participant ESO as External Secrets Operator
participant K8s as Secret psp-api-key
participant Pay as payment pods
Ops->>PSP: create key 2 (key 1 still active)
Ops->>SM: put key 2 as a new version of prod/boxoffice/payment/psp-api-key
ESO->>SM: refresh (every 1 h, or forced)
SM-->>ESO: version 2
ESO->>K8s: update Secret
K8s-->>Pay: mounted file changes β app reloads (or rolling restart)
Pay->>PSP: charges now signed with key 2
Ops->>Ops: confirm in metrics: zero calls with key 1 for 24 h
Ops->>PSP: revoke key 1 Notice what the design gives you: only the payment service account may read that secret (IAM); every read is in the audit log; the key never appears in Git, images or CI logs; and if it leaks, the same procedure runs in minutes instead of days.
BoxOffice β leaked key runbook (short version)
- Revoke first, investigate second: create a new key, roll it out (sequence above), revoke the leaked one.
- Search the provider's dashboard for calls from unknown IPs (BoxOffice only calls from its NAT Elastic IPs).
- Find how it leaked (Git history, logs, a laptop) and close that path.
- Write a blameless postmortem (Observability β incident response).
Dynamic secrets
Even better than rotating a shared secret is not having one. Dynamic secrets (Vault's database engine, cloud IAM database authentication) create a unique, short-lived credential per requester:
$ vault read database/creds/order
Key Value
lease_id database/creds/order/8sTt3lV0b2yL
lease_duration 1h
username v-k8s-order-x7Qa2
password A1a-4kd0s8f7GQpz
Each order pod gets its own database user that expires in an hour. A credential stolen from a pod stops working by itself, and the database logs show exactly which pod did what.
Encryption at rest with envelope encryption
Databases, object storage and backups are encrypted at rest with envelope encryption: each object is encrypted with a data key; the data key is itself encrypted with a key-encryption key that never leaves a KMS (Key Management Service) or HSM (hardware security module).
flowchart LR
D["order data"] -->|"AES-256-GCM with data key"| E["ciphertext"]
DK["data key (plaintext, in memory only)"] -->|"encrypt via KMS API"| EDK["encrypted data key<br/>(stored next to the ciphertext)"]
KMS["KMS master key<br/>(never leaves the KMS/HSM)"] -.-> EDK Rotating or revoking the master key in KMS controls access to all the data without re-encrypting terabytes; every decrypt call is authorized by IAM and logged.
Keep secrets out of Git
Scan before they land: a pre-commit hook and a CI job with a secret scanner (gitleaks, trufflehog, GitHub secret scanning with push protection) block commits that look like keys. If a secret is ever committed, rotate it β deleting the commit does not delete clones, forks or caches.
Supply-chain security
BoxOffice's order image contains a few hundred libraries written by strangers, built by a CI system, stored in a registry, and pulled by the cluster. Each step is an attack path: a malicious or compromised dependency (the 2024 xz-utils backdoor), a vulnerable one (Log4Shell, 2021), a compromised build system (SolarWinds, 2020), typosquatting (spring-boot-strater) and dependency confusion (a public package with the name of your internal one). Supply-chain security answers three questions: what is inside?, is it vulnerable?, did it come from our pipeline, unmodified?
flowchart LR
src["Source<br/>(reviewed PR)"] --> build["CI build<br/>(isolated runner)"]
build --> sbom["SBOM<br/>(syft β CycloneDX)"]
build --> scan["Scan<br/>(trivy / grype)"]
scan -->|"no critical CVEs"| sign["Sign + provenance<br/>(cosign keyless, SLSA)"]
sbom --> sign
sign --> reg[("Registry<br/>image@sha256 + attestations")]
reg --> adm{"Admission policy<br/>(Kyverno)"}
adm -->|"signature valid"| run["Pod runs"]
adm -->|"unsigned / wrong signer"| deny["Rejected"] What is inside: the SBOM
An SBOM (Software Bill of Materials) is the ingredient list of an artifact: every package, version and license, in a standard format β CycloneDX (OWASP) or SPDX (Linux Foundation, ISO/IEC 5962)5. When the next Log4Shell is announced, the question "which of our images contain this version?" becomes a query instead of a week of panic.
Is it vulnerable: scanning
Scanners (Trivy, Grype, Dependabot/Renovate for upgrades) match the SBOM against vulnerability databases (CVE identifiers, GitHub advisories). Gate the pipeline on a policy β e.g. "no critical vulnerability with a fix available" β not on zero findings, which never happens.
Did it come from us: signing and provenance
Sigstore cosign signs container images. In keyless mode, the CI job proves its identity with its OIDC token, a short-lived certificate is issued by Sigstore's CA (Fulcio), and the signature is recorded in a public transparency log (Rekor) β no long-lived signing key to protect6. SLSA (Supply-chain Levels for Software Artifacts) defines levels of build integrity; its provenance attestation records which source, workflow and builder produced the artifact7.
| SLSA Build level | Requirement | Protects against |
|---|---|---|
| L1 | provenance exists, describing how the artifact was built | "nobody knows how this binary was produced" |
| L2 | provenance is signed by a hosted build platform | tampering after the build |
| L3 | hardened build platform: isolated runs, signing keys not accessible to the build steps | a compromised build step forging provenance |
The BoxOffice release job, trimmed to the security steps (tool installation omitted):
permissions:
contents: read
packages: write
id-token: write # keyless signing uses the job's OIDC identity
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- id: build
uses: docker/build-push-action@v6
with:
push: true
tags: registry.example.com/boxoffice/order:${{ github.sha }}
- name: SBOM
run: syft registry.example.com/boxoffice/order@${{ steps.build.outputs.digest }} -o cyclonedx-json > sbom.cdx.json
- name: Scan (fail on fixable critical CVEs)
run: trivy image --severity CRITICAL --ignore-unfixed --exit-code 1 registry.example.com/boxoffice/order@${{ steps.build.outputs.digest }}
- name: Sign and attest
run: |
IMAGE=registry.example.com/boxoffice/order@${{ steps.build.outputs.digest }}
cosign sign --yes "$IMAGE"
cosign attest --yes --type cyclonedx --predicate sbom.cdx.json "$IMAGE"
In the BoxOffice code: the image job of the pipeline (lab 11)
Hands-on 11 β Image and supply chain answers the three questions for the catalog image, with one improvement over the job above: it builds the image locally (load: true), scans it, and pushes it only if the Trivy gate passes β a vulnerable image is never published. The gate is deliberately narrow, critical and fixable only: failing on every HIGH without a fix would block all work, and a gate that is always red gets ignored. The SBOM is SPDX (Syft), and instead of calling cosign the job uses GitHub's attestation actions, which sign the provenance and the SBOM keylessly with Sigstore (id-token: write) and bind them to the image digest. Every action is pinned to a commit SHA, and Dependabot proposes weekly upgrades of actions, Maven dependencies and base images as pull requests that the pipeline tests. What the lab does not do is the last link below: the provenance is checked by hand (gh attestation verify), not by an admission controller.
permissions:
contents: read
packages: write # push to ghcr.io
id-token: write # keyless signing of the attestations (Sigstore)
attestations: write # store the provenance and SBOM attestations
security-events: write # upload the Trivy report to the Security tab
- name: Gate - no CRITICAL vulnerability that has a fix
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: boxoffice-${{ matrix.service }}:ci
format: table
severity: CRITICAL
ignore-unfixed: true
exit-code: "1"
- name: Attest build provenance (who built it, from which commit, how)
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.push.outputs.digest }}
push-to-registry: true
- name: Attest the SBOM
uses: actions/attest-sbom@c604332985a26aa8cf1bdc465b92731239ec6b9e # v4.1.0
with:
subject-name: ${{ env.IMAGE }}
subject-digest: ${{ steps.push.outputs.digest }}
sbom-path: sbom.spdx.json
push-to-registry: true
The last link is verification at deploy time. An admission controller (Kyverno, Sigstore policy-controller, OPA Gatekeeper) rejects any pod whose image was not signed by our release workflow:
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-boxoffice-images
spec:
validationFailureAction: Enforce
rules:
- name: signed-by-release-workflow
match:
any:
- resources:
kinds: ["Pod"]
namespaces: ["boxoffice"]
verifyImages:
- imageReferences: ["registry.example.com/boxoffice/*"]
mutateDigest: true # pin the tag to the verified digest
attestors:
- entries:
- keyless:
issuer: "https://token.actions.githubusercontent.com"
subject: "https://github.com/repo-classes/pma271/.github/workflows/release.yaml@refs/heads/main"
Other habits that close common gaps: pin by digest (image@sha256:β¦, actions by commit SHA), use minimal or distroless base images, run containers as non-root with a read-only filesystem, use a private package proxy with scoped names to prevent dependency confusion, and follow the NIST Secure Software Development Framework (SSDF) as the checklist for the process8.
Try it. Open the ai-assistant scenario. The assistant calls tools (catalog, order) on the user's behalf: list every identity in the path (user, assistant, tool registry, services) and what each one should be allowed to do. Where would you put the PEP for a tool call? Which secrets does the LLM gateway hold? (The AI systems class goes deeper into the safety mesh.)
A defense checklist for BoxOffice
| Area | Control | Done when⦠|
|---|---|---|
| Network | isolated data subnets, per-AZ NAT, egress default-deny | search cannot open a TCP connection to the DB or the Internet |
| Workload identity | mesh mTLS STRICT + authorization policies | curl payment/charges from search returns 403 |
| User identity | OIDC, PKCE, 15-minute access tokens, rotating refresh tokens | no password ever touches the app or our services |
| Authorization | ownership checks; admin API separated | test "user A reads user B's order" fails |
| Secrets | secrets manager + ESO, per-service IAM, dual-key rotation | PSP_API_KEY readable only by payment; rotation runbook tested |
| Data | KMS encryption at rest, TLS in transit | backups and snapshots are encrypted |
| Supply chain | SBOM, scan gate, keyless signing, admission verification | an unsigned image cannot start in boxoffice |
| Detection | audit logs, denied-request alerts | a denied call from search pages someone |
In the interview
"Zero Trust" is a buzzword unless you make it concrete. Say three things: every call carries an identity (tokens for users, mTLS for services), every resource checks a policy (least privilege, default deny, including egress), and credentials are short-lived (workload identity, dynamic secrets, no static keys in CI). If the design has a payment provider, add: "the API key lives in a secrets manager, only the payment service can read it, and we rotate with two active keys." For supply chain, one line is enough: "CI produces an SBOM, scans, and signs images; the cluster only runs signed images."
Best practices
- Give every workload its own identity and a least-privilege role β
paymentalone may readPSP_API_KEY; a leak then points at one service and rotation touches one service. - Rotate every secret on a schedule and rehearse the leak runbook β dual-key rotation (e.g. every 90 days) and a drill each quarter, timed: how many minutes from "key leaked" to "old key revoked"?
- Prefer dynamic, short-lived credentials β database users issued per pod with a 1-hour TTL by a secrets engine instead of one password that lives for years.
- Scan every push for secrets β a secret scanner with push protection (in the repository and in CI) blocks the commit before the key is public.
- Pin third-party actions and base images to a commit SHA or digest, and let a bot propose the updates β in BoxOffice: lab 11's
ci.yamlpins every action, anddependabot.ymlopens weekly pull requests (supply-chain box). - Give CI jobs the minimum token permissions β
contents: readby default,id-token: writeonly in the job that signs. In BoxOffice: theimagejob of lab 11.
Common pitfalls
- "It's internal, so it's safe" β the definition of the perimeter model.
- One shared credential for all services β when it leaks, you cannot tell who used it, and rotation breaks everyone at once.
- Secrets in Git, images, ConfigMaps or CI logs β and "deleting the commit" instead of rotating.
- Static cloud keys in CI β use the CI's OIDC token and short-lived roles.
- Signing without verifying β a signature nobody checks at deploy time protects nothing.
- Scanning without a policy β 400 findings and no gate means nothing changes.
How it connects
flowchart LR
PER["perimeter model:<br/>inside = trusted"] -->|"replaced by"| ZT["Zero Trust:<br/>never trust, always verify"]
ZT --> PEP["PEP: gateway for users,<br/>mesh proxy for workloads"]
PEP --> MESH["mTLS + authorization policy<br/>β Service mesh"]
ZT --> WI["workload identity:<br/>SPIFFE, cloud roles, CI OIDC"]
WI --> SEC["secrets manager, dynamic secrets,<br/>dual-key rotation"]
SEC --> KMS["envelope encryption<br/>with a KMS"]
ZT --> SC["supply chain: SBOM, scan,<br/>cosign, SLSA"]
SC --> ADM["admission policy at deploy<br/>β Cloud-native"]
ZT --> DET["denied calls logged and alerted<br/>β Observability"]
DET --> IR["leaked-key runbook<br/>β incident response"] Palette components: apigw and mesh (the enforcement points) Β· payment (the key only payment may read) Β· logs Β· alerting (audit and denied-call alerts) Β· llmgw Β· toolreg (identities in an agent's tool calls). Scenarios: ai-assistant, payment-system. Continues in: Service mesh, Identity, Cloud-native β delivery, Observability β incident response, AI systems β safety & evals, Hands-on 11 β supply chain, Hands-on 13 β security.
Key takeaways
- Zero Trust: no trusted network; every request is authenticated, authorized and encrypted at a PEP using a PDP's decision. It turns a compromised pod into a dead end.
- Workload identity (SPIFFE/mTLS, cloud roles for service accounts, CI OIDC) replaces static keys with short-lived, verifiable credentials.
- Secrets belong in a secrets manager or Vault, scoped per service, audited, rotated (dual keys) β ideally dynamic; Kubernetes Secrets are base64, not encryption.
- Envelope encryption with a KMS protects data at rest; secret scanning keeps keys out of Git.
- Supply chain: SBOM (what is inside), scanning (is it vulnerable), signing + provenance + admission verification (did it come from us, unmodified).
-
ROSE, S. et al. NIST SP 800-207 β Zero Trust Architecture. NIST, 2020.Β β©β©
-
WARD, R.; BEYER, B. BeyondCorp: A New Approach to Enterprise Security. ;login:, v. 39, n. 6, 2014.Β β©
-
KUBERNETES. Good practices for Kubernetes Secrets and Encrypting Confidential Data at Rest.Β β©
-
SIGSTORE. Sigstore documentation β cosign, Fulcio, Rekor.Β β©
-
OPENSSF. SLSA β Supply-chain Levels for Software Artifacts, v1.0.Β β©
-
SOUPPAYA, M.; SCARFONE, K.; DODSON, D. NIST SP 800-218 β Secure Software Development Framework (SSDF). NIST, 2022.Β β©