Infrastructure

Hidden Credentials in Docker, Terraform, and YAML

Secrets rarely stay in application code. Infrastructure files are the blind spot most teams forget to scan.

5 min.

Cube glyph on a dark navy grid, illustrating secrets in Docker, Terraform and YAML

Most teams add secret scanning to their application code and stop there. Meanwhile the credentials with the widest access usually live somewhere else entirely: in the files that describe how the application is built, deployed, and connected.

Infrastructure files are the blind spot because they do not feel like code. Nobody reviews a Helm values file the way they review a pull request that touches billing logic, and the values inside it are frequently the keys to the account.

Dockerfiles and compose files

ENV and ARG values are baked into image layers. A value passed as a build argument stays visible in the image history even if a later instruction unsets the variable, which means the secret ships inside the artifact rather than living only in the build.

Compose files are worse in practice, because they are written for local development and then quietly promoted to a deployment mechanism. Database passwords sit in plain text next to the service definition, and the file gets committed on the first day of the project.

What to look for

Any ENV or ARG whose name contains key, token, password, or secret. Any compose environment block with a literal instead of a variable reference. Any entrypoint script that echoes configuration for debugging.

Terraform state and variables

Terraform is one of the most reliable sources of high-severity findings. Variable defaults get filled in for convenience during a spike and never removed. Provider blocks receive credentials directly when someone is debugging an auth failure.

State files are the sharper edge. Terraform records resolved values in state, including generated passwords and access keys, and a committed tfstate is effectively a plain-text inventory of everything the stack knows. Remote state with encryption exists precisely because of this, and it is still common to find state in the repository.

Treat any committed tfstate as a rotation event, not a cleanup task. Assume every credential inside it is exposed and work through them by blast radius.

YAML, everywhere

Kubernetes manifests, CI pipelines, Ansible playbooks, and Helm values all end up holding secrets, usually because YAML is where configuration goes when nobody has decided where configuration should go.

Kubernetes Secret objects deserve a specific warning: base64 is encoding, not encryption. A base64 blob in a committed manifest is a plain-text secret with an extra step, and it reads as safe to anyone who has not thought about it for a moment.

A leaked frontend analytics token is annoying. A cloud provider key in a Terraform variable file is an incident, because it usually grants access to everything else.

The files worth scanning first

  • Dockerfile, docker-compose.yml, and any entrypoint or init scripts.

  • *.tf, *.tfvars, and any committed *.tfstate or state backup.

  • Kubernetes manifests, Helm values files, and kustomize overlays.

  • CI pipeline definitions, reusable workflows, and composite actions.

  • Shell scripts used for deploy, migrate, seed, or backup steps.

  • Ansible playbooks, inventories, and any vault file committed unencrypted.

Why severity should default upward here

A credential found in an infrastructure file deserves a higher default severity than the same string in application code, for three reasons. It usually has broader permissions, it is usually still in use, and it is usually shared across environments rather than scoped to one service.

The practical rule: if the file describes how something is deployed, assume the credential inside it can deploy something.

Fixing the pattern, not just the finding

Point the deployment at a secret store and inject at runtime. Use build secrets rather than build arguments. Move Terraform state to an encrypted remote backend. Add the state and vars patterns to .gitignore before the first apply, not after the first scan.

None of these are new ideas. They get skipped because the insecure path works on the first try and the secure path needs ten minutes of setup.

Why these files are reviewed least

Infrastructure changes tend to arrive as one-line diffs under time pressure, usually while something is broken. A reviewer looking at a Helm values change has no way to tell a placeholder from a live password, and the pull request that fixes production gets approved quickly by design.

That is not a discipline problem, it is a visibility problem. Human review is the wrong control for values that all look like base64. Automated scanning across infrastructure files is the right one, precisely because it does not get tired at six in the evening.

A five-minute audit

Search the repository for the words password, secret, token, and key, restricted to yml, yaml, tf, tfvars, and Dockerfile paths. Read every hit. Most teams doing this for the first time find at least one value they assumed was templated.

Scan them in the same pass as your source

Splitting scanning across tools guarantees that the infrastructure half gets skipped, because it is nobody in particular job. One scan over the whole repository, with severity that accounts for file type, keeps the highest-risk findings in front of the person who can fix them.

KAIKI scans Docker, Terraform, YAML, shell, and .env files in the same pass as application code, so infrastructure findings appear in the same report rather than being discovered during an audit six months later.

Ship With Confidence

Zero config, 50+ secret patterns, AI-powered analysis. Start scanning in seconds.