AnsibleXXIX · Dynamic InventoryOperating a dynamic source
Authenticating an inventory source
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
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.
| Order | Source | Typical form |
|---|---|---|
| 1 | The plugin config file | An explicit option in the YAML |
| 2 | Ansible environment variables | ANSIBLE_<PLUGIN>_<OPTION>, named in ansible-doc |
| 3 | The provider SDK’s own chain | AWS_PROFILE, AZURE_*, OS_CLOUD, a ~/.config file |
| 4 | Ambient instance identity | An 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:
$ ansible-doc -t inventory ansible.builtin.scriptOPTIONS (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: booleanFor 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.
Link 1 is a trap
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.
plugin: example.provider.source
api_endpoint: https://api.example.com
api_token: REPLACE_ME_THIS_IS_THE_MISTAKE
regions:
- eu-west-1Some 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.
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.mdWhy 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 identity | Connection identity | |
|---|---|---|
| Talks to | The provider API | The managed hosts |
| Needs | List and describe | SSH or WinRM, and usually privilege escalation |
| Blast radius if stolen | A complete map of the estate | Change or destroy the estate |
| Rotation | Cheap: nothing depends on it but a query | Expensive: 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.
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 whatsoeverNeither 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.
- Give the inventory credential its own name in your identity provider, so that "who is ansible-inventory-ro" has an obvious answer.
- Scope it to list and describe on the resource types you query, in the projects you query, and nothing else.
- Deny it access to secret stores explicitly, rather than relying on it not being granted.
- 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.
- 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.
- 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
Q1. What does a stolen read-only inventory credential give an attacker that a port scan does not? Select all that apply.
Q2. An inventory plugin fails to authenticate. Which check most quickly separates an Ansible problem from a credential problem?
Q3. Why is an inventory credential unusually good material for an anomalous-use alert?
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.