Blog

Terraform and Ansible security: from an IaC misconfiguration to a proven attack path

Static IaC scanners flag Terraform and Ansible misconfigurations. Darkmoon's terraform and ansible agents prove which ones chain into an obtained credential.

· 9 min read

Infrastructure as code security usually stops at the linter. A pipeline runs a static scanner over the Terraform and Ansible, collects a wall of findings, and someone triages them by severity label. The trouble is that a severity label is a guess about impact, not a demonstration of it. A resource marked high because a policy string looks broad may sit behind three other controls that make it unreachable. A finding marked low may be the exact hinge an attacker needs. Static analysis cannot tell you which is which, because it never leaves the file.

Darkmoon approaches the same code from the other direction. Its terraform and ansible agents are two of the fifty specialist agents the platform can dispatch, and they do not grade configuration in the abstract. They read the code, then try to reach the thing the code protects, and they report only what they could actually stand up. This is a methodology piece on how that works. For a full run against a real lab, see our exposed terraform.tfstate write-up, which this article deliberately does not repeat.

Static IaC scanners tell you what is wrong, not what is exploitable

Tools like tfsec, Checkov and Ansible Lint are pattern matchers over source. They are fast, they run in CI, and they are genuinely useful for hygiene. What they cannot do is reason about the environment the code produces. They flag an IAM policy with a wildcard action, but they do not know whether an identity carrying that policy is reachable, whether a key for it is sitting in a state file, or whether the account has logging that would catch its abuse. They report a task that fetches a script over plain HTTP, but they cannot show that the fetched script runs as root on every host on every play.

The gap matters because remediation budget is finite. A list of two hundred flagged lines with no proof of impact gets triaged by heuristic, and heuristics are wrong at exactly the moments that count. The OWASP Infrastructure as Code Security guidance and the CIS Benchmarks give you the rules to check against, which is the right starting point. Darkmoon adds the missing half: which of those broken rules an attacker can actually chain, and how far the chain goes.

The two agents, and how they reason

The terraform agent treats infrastructure code and its by products as an intelligence source. It parses HCL and, where it can reach them, state files and plan output, because state is a plaintext inventory of everything Terraform had to know to build the environment. HashiCorp documents that state can contain sensitive values in clear regardless of the sensitive flag, and the agent reads it as an attacker would: resource graph, provider credentials, generated passwords, private keys, backend configuration. It uses ordinary tools, one jq query per resource type, so every claim is independently reproducible rather than a model assertion.

The ansible agent reads playbooks, roles, inventories and group variables. It looks for the two things that turn a configuration repository into an access path: credentials written in the clear, and tasks that execute untrusted content with elevated privilege. It knows what ansible-vault looks like when it is used and, more usefully, when it is not.

Both agents run inside the same controlled architecture as the rest of the platform. Operations go through a controlled MCP layer whose executor refuses unbounded commands, and the reasoning runs on a local model behind the Privacy Gateway. That gateway tokenizes sensitive values into deterministic placeholders before anything leaves the local scope, so the model only ever sees markers like IP_PRIVATE_001 or HOST_INTERNAL_001, never your real IPs, hosts or credentials. The placeholders are rehydrated locally only at the moment a tool runs and masked back out of every result. This is a real reduction in exposure, not a magic one: deterministic placeholders leak structure, cardinality and co occurrence by design, and the command gateway is a policy layer rather than a sandbox. We treat that honestly in the privacy design write-up, and the same caveats apply here. The point that survives is direct: your Terraform state and Ansible vault contents do not get shipped to a third party model to be analysed.

Misconfiguration classes that become attack paths

Not every misconfiguration is a foothold. The agents concentrate on the classes that reliably chain into something, because those are the ones worth a human's attention. Three recur across almost every environment we test.

Misconfiguration classWhy a scanner flags itWhy it is exploitable
Overly broad IAM in HCLWildcard actions or AdministratorAccess on a role or userA leaked or mintable key for that identity turns a finding into full account control
Plaintext secrets in inventory or group_varsA credential-looking string outside ansible-vaultansible_ssh_pass and become_pass are direct remote access plus root on the target host
Untrusted execution in a taskA shell task fetching a script over plain HTTPcurl piped to bash as root on every host is fleet-wide remote code execution by design
Readable state or backendState served without auth, or a backend with listing onOne unauthenticated GET yields passwords, access keys and private keys in clear

The pattern across all four rows is the same. The scanner sees a shape in the text. The agent sees whether that shape connects to a reachable asset. An AdministratorAccess policy attached to a CI user is a shrug on its own. Attach it to a user whose access key is sitting in a world readable state file, in an account with no CloudTrail, and it is the whole game.

Walking the path, from a config finding to an obtained credential

The methodology is a cascade. Read the code, extract a lead, test the lead against the live surface, and only then decide what the finding is worth. A typical Ansible chain reads the inventory the way any anonymous client could, then confirms the secret is genuinely in the clear rather than templated in from a vault at runtime.

# read the exposed inventory over HTTP
curl -s http://<host>/inventory.ini

[db]
db01 ansible_host=<internal ip> ansible_user=root ansible_become_pass=<redacted, in the report>

# confirm the secret is static, not vaulted at runtime
curl -s http://<host>/group_vars_all.yml | grep -iE 'pass|secret|key'
vault_aws_secret_key: <redacted, in the report>

From there the Terraform side of the same environment often yields a second, independent path to the same asset. A state file read with one request gives up the resource graph and the credentials baked into it, and a leaked access key is then tested against the live account rather than assumed to work. The escalation from a scoped key to account control, and the way an autonomous agent enumerates and proves it, is the subject of our autonomous cloud penetration testing work. The important discipline is that the credential is demonstrated, not inferred: the agent uses it, shows the caller identity it maps to, and records the raw response. That is the difference between a lead and a finding.

Authorized testing only

The techniques above read live infrastructure and use credentials found along the way. Run them only against systems you own or are explicitly authorized in writing to test, and keep destructive actions out of scope unless the engagement says otherwise. Reading a state file you were not asked to read is not a demonstration, it is an incident.

Where the agent stops

Every agent carries a status qualification block that it applies adversarially to its own claims. A finding is only EXPLOITED when the impact was executed end to end, meaning data was extracted, an action was completed or access was gained. It is CONFIRMED when the impact was demonstrated with the exact request, the raw response and the extracted data or execution trace, but the final destructive step was not taken. It is UNCONFIRMED when there is a real lead but no demonstrated impact, and those are held at low severity.

This is where the Ansible curl piped to bash task lands as CONFIRMED rather than EXPLOITED. The agent reads the playbook, proves that it runs untrusted content as root on every host, and stops. It does not stand up a server to answer for the fetched hostname and it does not connect to the managed hosts with the credentials it recovered. The finding is that the credentials exist, are readable by anyone, and grant what they grant. Walking into a host is a separate, destructive decision that a non destructive run does not make on its own. The agent also demotes the flattering non findings: a route that returns a bare 200, an echoed payload, a secret that is public by design. Holding that line is what makes the numbers usable.

Continuous IaC testing instead of a yearly review

Infrastructure code changes every day. A yearly review is a snapshot of a repository that no longer exists by the time the report is written. The value of an autonomous agent is that the walk from a misconfiguration to a credential is short, mechanical and repeatable, which means it can run on every merge rather than every year. A pull request that adds a wildcard IAM action, or drops a secret into group variables, or introduces a task that fetches over plain HTTP, can be met not with a lint warning but with a proof that the change is reachable and what it reaches. Because the whole run stays on a local, self hosted model, that testing can sit inside a private pipeline without your infrastructure code ever leaving it.

Remediation

  • Keep state out of reach. Use a backend with authentication, encryption at rest and locking, restrict who can read it, and never serve it from an unauthenticated file server.
  • Treat every secret that has ever touched a state file or an unvaulted inventory as compromised, and rotate it rather than just relocating the file.
  • Move inventory and group variable secrets into ansible-vault or a secret store, and turn off directory listing anywhere that serves infrastructure code.
  • Replace curl piped to bash with a pinned, checksum verified artefact fetched over TLS, and drop become: true from tasks that do not need it.
  • Scope CI identities down from AdministratorAccess to the actions they use, and enable account level logging so credential abuse is at least visible.
  • Keep the static scanner in CI for hygiene, and add an exploit oriented pass on top so that severity reflects reachability, not just pattern shape.

FAQ

How is this different from tfsec or Checkov? Those scanners match patterns in source and are excellent at hygiene. They tell you a line looks wrong. The Darkmoon agents read the same code, then try to reach and use the asset behind it, so they tell you whether that line is actually exploitable in your environment and how far the chain runs. Use both: the scanner for coverage, the agent for proof.

What makes an IaC misconfiguration exploitable? Reachability plus a usable secret or an executable path. An overly broad policy matters when there is a live key for the identity that carries it. A plaintext inventory password matters because it is direct access to a host. The agent establishes the connection rather than assuming it.

Does the agent modify my infrastructure? A non destructive run reads code and state, and demonstrates access without taking the final destructive step. Where a run does prove a persistence primitive, for example minting a second key, it cleans up after itself and records both the proof and the cleanup in the same finding. Destructive actions stay out of scope unless the engagement explicitly authorizes them.

Can it test Ansible playbooks and inventories? Yes. The ansible agent reads playbooks, roles, inventories and group variables, checks whether secrets are vaulted or in the clear, and identifies tasks that execute untrusted content with elevated privilege.

Where does it draw the line on destructive actions? At CONFIRMED. When proving impact would require running an untrusted playbook or logging into a production host, the agent stops at a demonstrated, evidence backed finding rather than executing the damage, and it labels the finding accordingly.

What this proves about IaC security

Static analysis answers what is wrong. It cannot answer what is exploitable, because it never leaves the file. Darkmoon's terraform and ansible agents close that gap by reading the code and then reaching for the asset it protects, reporting only what they could stand up and labelling the rest honestly. The result is a severity list ordered by demonstrated impact rather than by pattern shape, produced continuously and without your infrastructure code leaving your own pipeline.

Darkmoon is GPL-3.0 and self hosted: source, docs.

Run it against your own lab

Darkmoon is open source (GPL-3.0) and self hosted. Clone it, point it at a target you own, and read every line.