Skip to main content
RunBook Academy

AnsibleXXIX · Dynamic InventoryOperating a dynamic source

Authenticating an inventory source

Advanced⏱ ~23 minbash

What you'll learn

  • Trace where an inventory plugin reads its credentials from, in order
  • Scope an inventory credential to read-only and justify the scope to a reviewer
  • Describe what an attacker gains from an inventory credential specifically
  • Separate the identity that lists the fleet from the identity that changes it

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

An inventory plugin talks to somebody else’s API, so it needs to authenticate. That fact gets three lines in most vendor documentation and deserves rather more, because an inventory credential is a distinct thing from the credential Ansible uses to connect to hosts, it has a distinct blast radius, and confusing the two is common.

The short version, which the rest of the lesson argues for:

The identity that lists your fleet should be able to list your fleet and nothing else, and it should not be the identity that logs in to the machines.

Where the credentials come from

Every plugin resolves credentials through a chain, and the chain is almost always the same four links in the same order.

OrderSourceTypical form
1The plugin config fileAn explicit option in the YAML
2Ansible environment variablesANSIBLE_<PLUGIN>_<OPTION>, named in ansible-doc
3The provider SDK’s own chainAWS_PROFILE, AZURE_*, OS_CLOUD, a ~/.config file
4Ambient instance identityAn instance role, a workload identity, a metadata service

ansible-doc is authoritative for links 1 and 2 for any given plugin. Every option that can be set by environment variable declares that variable in its documentation, which is why reading the plugin documentation is step one of the bring-up order rather than an afterthought:

Read-only / Safeoptions declare their own environment variables
$ ansible-doc -t inventory ansible.builtin.script
OPTIONS (red indicates it is required):

 always_show_stderr  Toggle display of stderr even when script was
                     successful
      set_via:
        env:
        - name: ANSIBLE_INVENTORY_PLUGIN_SCRIPT_STDERR
        ini:
        - key: always_show_stderr
          section: inventory_plugin_script
      default: true
      type: boolean

For a provider plugin the same block lists the credential options and the environment variables that set them, which is how you find out whether a token can be supplied without writing it into a file. Read it before you decide where the secret lives.

The plugin config is a file. It lives in a repository. It gets committed, reviewed in a browser, copied into a ticket, pasted into chat, and pulled onto every machine that clones the repo.

Read-only / Safedo not do this
plugin: example.provider.source
api_endpoint: https://api.example.com
api_token: REPLACE_ME_THIS_IS_THE_MISTAKE
regions:
- eu-west-1

Some plugins accept a vault-encrypted string here, which is better than plaintext and still means the ciphertext is in the repository and the vault password is on every controller. It is a legitimate choice for a small team and it is not the default recommendation.

Read-only / Safethe shape to prefer
plugin: example.provider.source
api_endpoint: https://api.example.com
regions:
- eu-west-1
# credentials come from the environment or the provider profile;
# see docs/runbooks/inventory-credentials.md

Why read-only is not a formality

The natural instinct is that an inventory credential is low-risk because it only reads. That instinct is correct about the credential and wrong about the consequence, and it is worth separating the two.

What the credential can do: list resources. That is genuinely limited.

What listing resources gives an attacker: a complete, structured, current map of your infrastructure. Every hostname. Every private address. Every tag, which in most estates encodes the environment, the owning team, the application and often the data classification. Which machines are databases. Which are in which region. Which were created this week.

That is reconnaissance that would otherwise take weeks, delivered as JSON. It is the difference between an attacker who has to scan and guess and an attacker who knows exactly which three hosts are the production payment databases.

So the argument for read-only is not that the credential is unimportant. It is that a credential this useful to an attacker must not also be able to change anything.

Two identities, not one

Here is the separation that matters most, and it is the one most estates get wrong by default.

Inventory identityConnection identity
Talks toThe provider APIThe managed hosts
NeedsList and describeSSH or WinRM, and usually privilege escalation
Blast radius if stolenA complete map of the estateChange or destroy the estate
RotationCheap: nothing depends on it but a queryExpensive: touches every host

They are different in every row, so they should be different accounts.

The reason they end up as one account is convenience: an instance role on the controller that already has cloud permissions is right there, and adding “describe instances” to it is one line. Now the identity that can log in to every host is also the identity that can enumerate the estate, and a compromise of the controller yields both at once.

Read-only / Safethe separation, concretely
ansible-inventory-ro    provider API: describe/list only, scoped to one project
                      no access to secret stores
                      no SSH key, no host access at all

ansible-automation      SSH key or certificate to managed hosts
                      sudo via a documented, restricted rule
                      no provider API permissions whatsoever

Neither account can do the other’s job. That is the point: an attacker who obtains one has to obtain the other separately, and each has a different theft path.

Rotation, and the reason it matters here

An inventory credential is the cheapest credential in your estate to rotate, because nothing depends on it except a read query. There is no coordination, no restart, no window. Rotating it is a config change and a re-run of ansible-inventory --list.

Which means there is no excuse for it to be three years old, and yet it usually is — because it works, and nothing prompts anyone to look at it.

  1. Give the inventory credential its own name in your identity provider, so that "who is ansible-inventory-ro" has an obvious answer.
  2. Scope it to list and describe on the resource types you query, in the projects you query, and nothing else.
  3. Deny it access to secret stores explicitly, rather than relying on it not being granted.
  4. Store it where your other automation credentials live - a secret manager, an instance role, an environment injected by the scheduler - and never in the plugin config.
  5. Rotate it on a schedule and verify the rotation with ansible-inventory --list --flush-cache, which is read-only and proves the new credential works before anything depends on it.
  6. Alert on its use from anywhere other than the controller. An inventory credential has exactly one legitimate caller, which makes anomalous use unusually easy to detect.

Step six is worth more than it looks. Most credentials are used from many places and their access logs are noise. This one is used by one process, on a schedule, from one address range. A call from anywhere else is a signal with almost no false positives, and it is one of the few high-quality detections available for free.

Knowledge check

Knowledge check · 4 questions

  1. Q1. What does a stolen read-only inventory credential give an attacker that a port scan does not? Select all that apply.

  2. Q2. An inventory plugin fails to authenticate. Which check most quickly separates an Ansible problem from a credential problem?

  3. Q3. Why is an inventory credential unusually good material for an anomalous-use alert?

  4. Q4. Encrypting the API token with Ansible Vault inside the inventory plugin config removes the objection to storing credentials in the repository.

Passing score: 75%. Answers are checked in this browser.