Software Securityhome
Fix a findingSearch
Phase 5 · Release

IaC and containers

Infrastructure as code (IaC) describes cloud resources, networks and permissions in files such as Terraform; Dockerfiles and Kubernetes manifests describe how the software is packaged and run. This practice reviews and tests those files like code, and makes the platform refuse configurations that break the rules.

One line in a Terraform plan can open a database to 0.0.0.0/0 or let a staging pipeline assume a production role, and an image can carry a registry token in its build history or run as root. Application tests do not look at these files. Policy checks in CI catch the change before it is applied, and admission control catches what skips CI, such as a kubectl apply from a laptop.

Who
Platform and DevOps engineers who write the Terraform, Dockerfiles and manifests, with AppSec owning the shared policies
When
On every pull request that changes infrastructure, images or manifests, and again when the cluster admits each workload
How often
Every change in CI; base images rebuilt on a schedule; drift checked on a schedule; policies reviewed on platform upgrades
You end up with
Policy-as-code rules that gate CI, hardened image and pod baselines, and admission policies the cluster enforces

Do this

  1. Review the plan, not only the diff

    Run terraform show -json on the exact plan you will apply and review it as four questions: who can obtain each identity, what each identity can do, what becomes reachable from the network, and where secrets end up. Treat trust and permission as separate findings: a narrow permission policy does not help if the wrong workload can assume the role. Then apply the plan file that was reviewed, not a fresh one.

  2. Turn recurring rules into policy as code

    Write the rules every change must meet, such as no wildcard IAM actions, nothing but ports 80 and 443 open to the internet and no privileged pods, as policies that run in CI and fail the pipeline on a violation. conftest runs OPA Rego policies against Terraform plans, Dockerfiles and Kubernetes YAML; scanners such as Checkov and Trivy ship ready-made checks. Review the judgment calls by hand.

  3. Build minimal images that run as non-root

    Use a multi-stage build that ships a minimal runtime stage without a shell, package manager or compiler, pin base images by digest (name:tag@sha256:...) and let a bot refresh the digest, and set USER to a numeric non-root UID. Pass build credentials only through BuildKit secret mounts (RUN --mount=type=secret): ARG and ENV values stay in the image history. Scan the pushed digest in CI.

  4. Run every pod with the fewest privileges

    Set runAsNonRoot: true, allowPrivilegeEscalation: false, capabilities.drop: ["ALL"] and seccompProfile.type: RuntimeDefault, which the Restricted Pod Security Standard requires, and add readOnlyRootFilesystem: true with a size-limited emptyDir for scratch files. Give each workload its own service account bound to a Role with only the resources and verbs it uses.

  5. Enforce the rules at admission

    Label each application namespace pod-security.kubernetes.io/enforce: restricted with enforce-version pinned, after a --dry-run=server of the label change lists the pods that would fail. For rules Pod Security cannot express, such as allowed registries or signed images, use a ValidatingAdmissionPolicy (built in, written in CEL, stable since Kubernetes 1.30) or an engine such as Kyverno or OPA Gatekeeper.

  6. Keep secrets out of state and images

    Terraform writes secret values into plan and state files even when a variable is marked sensitive, so use ephemeral values (Terraform 1.10 or later) and write-only arguments (1.11 or later) where providers support them, and store state encrypted with few readers. Allowlist the Docker build context with .dockerignore so .env, .git and .npmrc never reach a layer.

  7. Check what was actually deployed

    After apply, test that the intended identity can do its job and an unrelated one cannot. Check the service account with kubectl auth can-i <verb> <resource> --as=system:serviceaccount:<ns>:<sa>, and the pod settings from inside the container (id, CapEff in /proc/self/status). Run terraform plan -refresh-only -detailed-exitcode on a schedule: exit code 2 means live infrastructure has drifted from state.

Done when

Policy checks run on IaC and manifests in CI, and the cluster rejects workloads that break them.

Mark it in the gap check

Common mistakes

  • Running policies only in CI. A terraform apply or kubectl apply from a laptop skips them; admission control and drift detection catch what the pipeline never saw.
  • Marking a Terraform variable sensitive and treating the secret as protected. It is hidden in terminal output but still readable in the plan and state files.
  • Deleting a secret in a later Dockerfile step. It remains in the earlier layer and in the build history, readable by anyone who can pull the image.
  • Setting USER to a name instead of a number. The kubelet cannot verify runAsNonRoot against a user name and refuses to start the container.
  • Fixing a startup error by going back to root or a writable root filesystem, instead of finding the one path the process needs to write and mounting a volume there.
  • Enforcing pod rules only on application containers in a custom policy. Pod Security checks init and ephemeral containers too; a custom policy that does not match them lets kubectl debug attach a container the policy would reject.

Pod Security Standards: what each level blocks

Kubernetes defines three levels, and the built-in Pod Security Admission controller enforces one per namespace. Restricted is a common target for application namespaces; Baseline suits workloads that cannot meet it yet.

Pod Security Standards: what each level blocks: table
LevelKubernetes descriptionMain controls
PrivilegedUnrestricted; allows known privilege escalationsNone; reserve it for system workloads you fully trust
BaselinePrevents known privilege escalations; allows the default pod configurationNo privileged containers, host namespaces, hostPath volumes or host ports; only a short list of added capabilities; seccomp not Unconfined
RestrictedFollows current pod hardening best practicesBaseline plus runAsNonRoot, allowPrivilegeEscalation: false, drop ALL capabilities (add back only NET_BIND_SERVICE), seccomp RuntimeDefault or Localhost, and a short list of volume types
Your own policyNot part of any levelreadOnlyRootFilesystem: true, resource limits, image digests and signatures, allowed registries

Tools and guides

Standards

NIST SSDF 1.1
PW.9 Configure Software to Have Secure Settings by Default

Sources

  1. Kubernetes Pod Security Standards. Kubernetes v1.37.
  2. Kubernetes Pod Security Admission. Kubernetes v1.37.
  3. Kubernetes ValidatingAdmissionPolicy. Kubernetes v1.37.
  4. OWASP Docker Security Cheat Sheet
  5. Docker: building best practices
  6. Docker: build secrets
  7. conftest documentation
  8. HashiCorp: managing sensitive data in Terraform
  9. Terraform: plan -refresh-only and detailed exit codes
  10. NIST SP 800-218: Secure Software Development Framework (SSDF) Version 1.1, practice PW.9. February 2022.
  11. OWASP SAMM: Secure Deployment

Find your next answer

Type to search. Search runs in your browser.

Searches run in your browser, but the host can infer them from the index files they load. The address shows your search so you can bookmark or share it.