Your Ansible inventory lists every server you run, and the passwords to log into them

Ansible's job is to log into all your machines and configure them. To do that it keeps two things close: an inventory of every host it manages, and the credentials to reach them, the SSH passwords, the become (sudo) passwords, the private-key paths. Those live in files like group_vars/all.yml, host_vars/..., and vault.yml. They are, by design, a map of your fleet plus the keys to it.

When one of those files ends up under your web root, that map and those keys are a download. If the secrets were run through ansible-vault, an attacker gets the encrypted blob plus your full host topology, which is bad. If they weren't, and plenty of inventories keep passwords in cleartext because the team "meant to vault it later," the attacker gets live, working credentials to move laterally across every box Ansible touches. This is the file that turns one exposed web server into the whole estate.

The two arms of the leak

An exposed Ansible file fails in one of two ways, and the difference is the difference between "they have a head start" and "they're already in."

The first arm is the plaintext inventory. A group_vars/all.yml or host_vars/web01.yml with variables like ansible_password, ansible_become_password, or ansible_ssh_private_key_file set to literal values. Those are the credentials Ansible authenticates with, sitting in readable YAML.

request
GET /group_vars/all.yml HTTP/1.1 Host: yoursite.com
response
HTTP/1.1 200 OK
Content-Type: text/yaml
ansible_user: deploy
ansible_become_password: Sup3r_R00t_2026
ansible_ssh_private_key_file: /home/deploy/.ssh/id_rsa
db_root_password: prod_db_MasterKey
Not a hash, not a token. The actual become password Ansible uses to gain root, in cleartext, in the web root.

The second arm is the vaulted file. A vault.yml that opens with $ANSIBLE_VAULT;1.1;AES256 and a wall of hex. The values are encrypted, so they don't leak directly, but two things still escape: the host topology around it (which servers exist, how they're grouped, what roles they run) and the encrypted blob itself, which is now in the attacker's hands to brute-force offline against the vault password at their leisure. A weak vault password becomes a time-limited secret the moment the file is downloadable.

Why the inventory ends up served

Ansible files almost always live in the same repo as the thing being deployed, and that's the root of it.

A deploy clones the repo onto the server, the web root points at the repo top instead of an app subdirectory, and the group_vars/, host_vars/, and inventory/ folders come along for the ride, now reachable over HTTP. Or someone runs Ansible from the production box and leaves the playbook tree in a served path. Or a vault.yml gets copied into a static/ folder during a hurried deploy and never removed. The file was written for ansible-playbook to read locally, never for a browser, and the docroot pointing one level too high is all it takes to publish it.

Vault everything, and keep the tree out of the web root

The fix has a belt and a pair of suspenders: never serve the playbook tree, always vault the secrets, and rotate anything that leaked.

# repo cloned to server, web root = repo top
# /group_vars/all.yml is a public URL with live creds
ansible_become_password: Sup3r_R00t_2026   # plaintext
The playbook tree never belongs under a web root, and live secrets never belong in plaintext YAML even when they're not.

Point the web root at the application's own public directory so the playbook tree, group_vars, host_vars, inventory, and any vault.yml, is never in the served path. Encrypt every sensitive variable with ansible-vault so even a leaked file holds only ciphertext, and back that with a strong vault password, because a downloadable vault file becomes an offline cracking target the instant it's exposed. And if a plaintext inventory ever faced the web, rotate every credential it named: those passwords and keys should be considered known.

A scan catches this because both arms have a clean signature. SurfaceCheckr requests the common inventory and vault paths and flags one only on a real hit: the $ANSIBLE_VAULT;1.x;AES256 header followed by its hex continuation for the encrypted arm, or an Ansible-specific secret variable carrying a genuine inline literal for the plaintext arm, with !vault references, Jinja placeholders, empty values, and commented lines all rejected so a template doesn't fire. It reads enough to confirm the file is a real inventory or vault and stops; it never harvests the credentials or pulls the encrypted blob. For the closely related case of SSH keys themselves sitting in the web root, a downloadable id_rsa or .htpasswd as a direct server login is the neighbouring finding, and the wider set of infra config files that map your cloud for free is the pillar around it.

Find it before someone else does.

Paste your domain. The grade and issue count are free, and you'll see in a couple of minutes exactly what's reachable from outside.