Education › DevOps › Stage 4: Operate & secure

Secrets & DevSecOps

Secret managers, least privilege, OIDC instead of long-lived keys, policy as code.

Advanced ~30 min read Module 15 of 17

Most real-world compromises of delivery systems do not involve clever exploits. They involve a credential that was somewhere it should not have been: an access key in a Git history, a token in a CI log, a password shared in chat. Pipelines and clusters concentrate powerful credentials, which makes them targets. This module covers how to take secrets out of code, replace long-lived keys with short-lived identity, and build security checks into the pipeline so that they run on every change instead of once a year.

After this module you can
  • Identify where secrets leak, and respond correctly when one does
  • Store and deliver secrets with a secret manager instead of code, images or plain environment files
  • Replace long-lived cloud keys in CI with OIDC federation and short-lived credentials
  • Apply least privilege across IAM, Kubernetes RBAC and pipeline permissions
  • Add automated security gates to a pipeline: secret scanning, SAST, dependency, IaC and policy-as-code checks

Where secrets leak, and what to do when they do

A secret is anything that grants access: passwords, API tokens, private keys, database URLs with credentials, signing keys, webhook secrets. They escape through a predictable set of routes.

  • Source control. A .env file or a hard-coded key is committed. Deleting it in a later commit does not help, because it remains in the history and in every clone and fork.
  • Container images. ENV, ARG and COPY persist in image layers, as the Docker module showed.
  • Logs and error messages. A debug line that prints the whole configuration, or a stack trace containing a connection string.
  • CI configuration. Secrets echoed by a script, or exposed to workflows triggered by untrusted pull requests.
  • Terraform state and Kubernetes Secrets, which hold values in plain text or in base64, which is encoding and not encryption.
  • People. Credentials pasted into chat, tickets, wikis and screenshots.

When a secret leaks, the order of operations matters, and most teams get it wrong under pressure.

  1. Revoke or rotate the credential immediately. Assume it has been copied. Public repositories are scraped for keys within minutes.
  2. Check the audit logs for use of that credential from the moment of exposure.
  3. Then clean up the history, if it is worth doing at all. A rotated secret in the history is harmless; an unrotated one that was deleted is still live.
  4. Fix the cause: add scanning, move the secret to a manager, and write it up.
bash
gitleaks detect --source . --verbose          # scan the working tree and full Git history
gitleaks protect --staged                     # scan only what is about to be committed

# run it on every commit with the pre-commit framework
pre-commit install
Tip

Turn on your Git host's built-in secret scanning and push protection, which rejects a push that contains a recognisable credential. Blocking the secret before it lands is far cheaper than responding afterwards.

Secret managers

A secret manager is a service built for this one job: AWS Secrets Manager, Azure Key Vault, Google Secret Manager, or HashiCorp Vault if you run your own. It provides what files and environment files cannot.

CapabilityWhy it matters
Encryption at rest, with keys held in a KMSA stolen disk or backup reveals nothing
Access controlled by IAM, per secretThe orders service can read its own database password and nothing else
An audit log of every readAfter an incident you know exactly which secrets were accessed, and by whom
Versioning and rotationChange a credential in one place, on a schedule, without redeploying every consumer
Dynamic secrets (Vault, some cloud databases)Credentials created on demand for one client and expired automatically
bash
aws secretsmanager create-secret --name shop/prod/orders/db \
  --secret-string '{"username":"orders","password":"REPLACE_ME"}'

aws secretsmanager get-secret-value --secret-id shop/prod/orders/db \
  --query SecretString --output text | jq -r .password

In Kubernetes, the External Secrets Operator bridges the gap. You commit a manifest that names the entry in the secret manager; the operator, which has a role allowing it to read that entry, creates and refreshes an ordinary Kubernetes Secret. Git holds only a reference, which fits the GitOps module exactly.

yaml
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: orders-db
  namespace: shop
spec:
  refreshInterval: 1h
  secretStoreRef:
    name: aws-secrets
    kind: ClusterSecretStore
  target:
    name: orders-secrets            # the Kubernetes Secret to create
  data:
    - secretKey: DATABASE_PASSWORD
      remoteRef:
        key: shop/prod/orders/db
        property: password

Prefer mounting secrets as files over environment variables where your application allows it. Environment variables are inherited by child processes and tend to appear in crash dumps and debug endpoints, and a mounted file can be refreshed without restarting the pod.

Note

The operator's API version changes as the project matures. Check the version installed in your cluster with kubectl api-resources | grep externalsecret before copying a manifest.

OIDC: deploying without stored keys

The traditional way to let CI deploy to the cloud is to create a user, generate an access key, and paste it into the CI system's secrets. That key is long-lived, powerful, held by a third party, and rarely rotated. OIDC federation removes it.

  1. At the start of a job, the CI platform issues a signed, short-lived identity token. Its claims describe the job: which repository, which branch, which environment.
  2. The job presents the token to the cloud provider and asks to assume a role.
  3. The cloud verifies the signature against the CI platform's public keys, and checks the claims against the role's trust policy.
  4. If they match, it returns temporary credentials that expire, typically within an hour.
1. short-lived token2. token: who I am3. verify the signature4. claims allowed?5. temporary credentials6. call; expires in 1 hCI identitysigns job tokensCI jobrepo acme/shop, mainToken serviceAssumeRoleWith...Trust policysub = repo:acme/...Cloud APIdeploy
Deploying with no stored key: the CI job presents a short-lived identity token, the cloud checks its claims against the role's trust policy, and hands back temporary credentials that expire within the hour.
GitHub Actions job using OIDC to assume an AWS role
yaml
jobs:
  deploy:
    runs-on: ubuntu-latest
    environment: production
    permissions:
      id-token: write      # allow this job to request an OIDC token
      contents: read
    steps:
      - uses: actions/checkout@v4
      - uses: aws-actions/configure-aws-credentials@v4
        with:
          role-to-assume: arn:aws:iam::123456789012:role/shop-deploy
          aws-region: eu-west-1
      - run: aws sts get-caller-identity
The role's trust policy: only the main branch of one repository may assume it
json
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com"
      },
      "Action": "sts:AssumeRoleWithWebIdentity",
      "Condition": {
        "StringEquals": {
          "token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
          "token.actions.githubusercontent.com:sub": "repo:acme/shop:ref:refs/heads/main"
        }
      }
    }
  ]
}
Watch out

The sub condition is the security boundary. A trust policy that checks only the audience, or uses a wildcard such as repo:acme/*, lets any repository or any branch in your organisation, including an unreviewed pull request branch, assume the role.

The same pattern gives Kubernetes pods cloud access without keys: workload identity on GKE and AKS, IAM roles for service accounts or Pod Identity on EKS. Each pod's service account is mapped to a cloud role, so the orders pod and the payments pod get different permissions.

Least privilege everywhere

Least privilege means every identity can do exactly what its job requires and nothing more, so that a compromise of one component is contained there. It applies at every layer you have met in this track.

  • Cloud IAM. One role per workload, with specific actions on specific resources. Start from zero and add what fails, instead of starting from administrator and meaning to trim later.
  • Pipelines. A permissions: block on every workflow, and separate roles for plan and apply, and for staging and production.
  • Kubernetes RBAC. Humans get read access by default, and changes go through Git. Each workload gets its own ServiceAccount.
  • Network. Security groups and Kubernetes NetworkPolicies that allow only the flows a service needs.
A Role that can read pods and their logs in one namespace, bound to a group
yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: pod-reader
  namespace: shop
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: shop-developers-read
  namespace: shop
subjects:
  - kind: Group
    name: shop-developers
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io
bash
kubectl auth can-i delete pods -n shop --as jane@example.com
kubectl auth can-i --list -n shop --as system:serviceaccount:shop:orders

Be careful with permissions that look harmless. In Kubernetes, the right to read Secrets in a namespace is the right to every credential there, and the right to create pods is, in effect, the right to use any ServiceAccount in that namespace.

DevSecOps: security gates in the pipeline

DevSecOps means security checks are automated, run on every change, and give feedback in the pull request, while the author still has the context to fix the problem. This is often called shifting left. It does not replace security review or penetration testing; it removes the routine findings so that experts can spend their time on the hard ones.

CheckFindsExample tools
Secret scanningCredentials in code and historygitleaks, host push protection
SAST (static analysis)Injection, unsafe deserialisation, weak cryptography in your codeSemgrep, CodeQL
SCA (dependency scanning)Known CVEs in librariesTrivy, Grype, Dependabot alerts
Image scanningCVEs in OS packages, running as rootTrivy, Grype
IaC scanningPublic buckets, open security groups, unencrypted disksCheckov, Trivy config, tfsec
DAST (dynamic analysis)Flaws visible in a running applicationOWASP ZAP

Policy as code takes this further: organisational rules become versioned, testable code that is enforced automatically. Open Policy Agent with its Rego language, and Kyverno with YAML policies, are the common tools. The same policy can run in CI against manifests, where it advises, and as a Kubernetes admission controller, where it blocks.

A Kyverno policy that rejects pods which may run as root
yaml
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: require-run-as-non-root
spec:
  validationFailureAction: Enforce
  rules:
    - name: check-run-as-non-root
      match:
        any:
          - resources:
              kinds: [Pod]
      validate:
        message: "Containers must set securityContext.runAsNonRoot to true."
        pattern:
          spec:
            containers:
              - securityContext:
                  runAsNonRoot: true

Introduce gates gradually. Start every new check in report-only or Audit mode, fix the existing findings, and only then switch it to blocking for new violations. A gate that fails every build on day one will be disabled on day two, and a disabled check protects nothing.

Hands-on practice

Remove a secret, remove a key, add a gate

  1. In a scratch repository, commit a fake API key, then delete it in a second commit. Run gitleaks detect and confirm that it still finds the key in history. Write down the correct response order for a real leak.
  2. Install gitleaks as a pre-commit hook and confirm that it now blocks the commit. Enable secret scanning and push protection in your Git host's settings.
  3. Store a database password in your cloud's secret manager, or run Vault in dev mode locally. Grant one role read access to that single secret and read it from the CLI.
  4. Replace a stored cloud access key in a GitHub Actions workflow with OIDC: create the identity provider and a role whose trust policy pins your repository and the main branch, then prove aws sts get-caller-identity works with no stored secret.
  5. Try to assume the same role from a different branch and confirm that it is denied. Then delete the old access key.
  6. Create a namespaced read-only Role and RoleBinding in your cluster, and test it with kubectl auth can-i.
  7. Add Trivy config scanning or Checkov to your Terraform pipeline in report-only mode. Fix one finding, then make the job fail on HIGH and CRITICAL findings.
Cheat sheet

Secrets & DevSecOps — at a glance

Main things to focus on

  • A leaked secret is compromised. Rotate first, investigate second, clean history last.
  • Secrets never belong in Git, images, logs or Terraform variables committed to a repository. They belong in a secret manager.
  • base64 is encoding, not encryption. Kubernetes Secrets need RBAC and encryption at rest.
  • Replace long-lived CI keys with OIDC federation, and pin the trust policy to a repository and a branch or environment.
  • Least privilege at every layer: cloud IAM, pipeline token, Kubernetes RBAC, network policy.
  • Shift left: automated security checks on every pull request, while the author still has context.
  • Roll out gates in audit mode first, then block new violations.

Leak response, in order

1. revoke / rotateImmediately; assume the secret has been copied
2. auditSearch access logs for use since the exposure
3. containRotate anything the credential could reach
4. clean upRemove from history only if worthwhile; rotation made it harmless
5. preventAdd scanning, move to a manager, record what was learned

Finding secrets

gitleaks detect --source .Scan the working tree and full history
gitleaks protect --stagedScan staged changes; use in a pre-commit hook
pre-commit installActivate the hooks in .pre-commit-config.yaml
git log -p -S 'AKIA' --allFind commits that added or removed a string
push protection (Git host setting)Reject pushes that contain known credential formats

Secret managers and Kubernetes

aws secretsmanager get-secret-value --secret-id NAMERead a secret (every read is audited)
vault kv put secret/PATH key=valueWrite to Vault's key-value engine
vault kv get -field=key secret/PATHRead a single field
kind: ExternalSecretSync a manager entry into a Kubernetes Secret
kubectl get secret NAME -o jsonpath='{.data.KEY}' | base64 -dDecode a Secret, which shows how little base64 protects
volumeMounts + secret volumeDeliver secrets as files, refreshable without a restart

OIDC federation

permissions: { id-token: write }Let a GitHub Actions job request an OIDC token
sts:AssumeRoleWithWebIdentityThe AWS action that exchanges the token for credentials
sub = repo:ORG/REPO:ref:refs/heads/mainClaim to pin in the trust policy: repository and branch
sub = repo:ORG/REPO:environment:productionAlternative claim: pin to a protected environment
aud = sts.amazonaws.comAudience claim expected by AWS

Least privilege checks

kubectl auth can-i VERB RESOURCE -n NS --as USERTest a permission as another identity
kubectl auth can-i --list -n NSEverything the current identity may do
Role / RoleBindingPermissions within one namespace
ClusterRole / ClusterRoleBindingCluster-wide permissions; grant sparingly
automountServiceAccountToken: falseDo not give a pod an API token it does not need

Pipeline gates

semgrep scan --config autoStatic analysis of your code
trivy fs --scanners vuln,secret .Dependencies and secrets in a repository
trivy config .Misconfigurations in Terraform, Kubernetes, Dockerfiles
checkov -d .IaC policy checks
conftest test FILEEvaluate OPA/Rego policies against config files
validationFailureAction: Audit | EnforceKyverno: report only, or block

Common pitfalls

  • Deleting a leaked key from the repository and considering the matter closed, without rotating it.
  • An OIDC trust policy with a wildcard subject, so that any branch or repository can assume the deploy role.
  • Believing that Kubernetes Secrets are encrypted because the values look scrambled.
  • Granting cluster-admin or cloud administrator to a pipeline because working out the real permissions was tedious.
  • Printing the environment or full configuration in debug logs.
  • Switching on every scanner in blocking mode at once, then switching them all off a week later.
Quiz

Check your understanding

5 questions · 4 to pass · answers are explained as you go. Your best score is saved on this device only.

Progress and quiz scores are saved in this browser only. Back up or restore on the hub.

Was this lesson useful? Tell me what to improve →