Skip to content

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:

kubectl get secret psp-api-key -n boxoffice -o jsonpath='{.data.PSP_API_KEY}' | base64 -d

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)

  1. Revoke first, investigate second: create a new key, roll it out (sequence above), revoke the leaked one.
  2. Search the provider's dashboard for calls from unknown IPs (BoxOffice only calls from its NAT Elastic IPs).
  3. Find how it leaked (Git history, logs, a laptop) and close that path.
  4. 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.

.github/workflows/ci.yaml (image job: permissions, scan gate, attestations)
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 β€” payment alone may read PSP_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.yaml pins every action, and dependabot.yml opens weekly pull requests (supply-chain box).
  • Give CI jobs the minimum token permissions β€” contents: read by default, id-token: write only in the job that signs. In BoxOffice: the image job 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).