Skip to main content
RunBook Academy

AnsibleXXVII · Collections, Galaxy and Dependency TrustCollections and dependency trust

Reading a collection before you run it

Advanced⏱ ~24 minansible-galaxyansible-docgrep

What you'll learn

  • Run a repeatable evaluation of a third-party collection before it touches a host
  • Read a collection manifest, changelog and source tree for the signals that predict maintenance
  • Determine what privilege a collection needs and what it does with the credentials it is given
  • Produce a written verdict a colleague can disagree with

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.

You would not install a .deb from a stranger’s website onto a production server. You would ask who built it, whether the repository is signed, what it depends on, and what it does when it runs.

ansible-galaxy collection install is that install, with a wider blast radius. The modules go to every host in the play and run under become. The lookup, filter, action and inventory plugins run on the controller — the machine holding SSH keys to the entire fleet and the vault password that decrypts your secrets. There is no sandbox at either end.

The good news is that a collection is a small, readable, fully local artefact. Everything below is answerable in about twenty minutes from files already on your disk.

The seven questions

  1. Who publishes it, and does the namespace match?
  2. Is it maintained, and by how many people?
  3. Where is the source, and can you reach it?
  4. What does it drag in?
  5. What privilege does it need, and what does it do with credentials?
  6. Does it support check mode and diff, honestly?
  7. What is the licence, and does it fit?

Take them in order against a real collection.

1. Who publishes it

Read-only / Safethe manifest, which travels with the artefact
$ python3 -c "
import json
m = json.load(open('collections/ansible_collections/community/docker/MANIFEST.json'))
ci = m['collection_info']
for k in ('namespace','name','version','license','authors','repository','issues','dependencies','tags'):
  print(k, '=', ci.get(k))
"
namespace = community
name = docker
version = 4.7.0
license = ['GPL-3.0-or-later', 'Apache-2.0']
authors = ['Ansible Docker Working Group']
repository = https://github.com/ansible-collections/community.docker
issues = https://github.com/ansible-collections/community.docker/issues
dependencies = {'community.library_inventory_filtering_v1': '>=1.0.0'}
tags = ['docker']

That block is the publisher’s own statement about itself, and it is where a typosquat gives itself away. The check is not “does it look plausible” but does the repository URL belong to the organisation the namespace claims? community.* collections live under github.com/ansible-collections/. A community.something whose repository points at a personal account is either a mistake or a name nobody should have been able to claim.

2. Is it maintained

Every well-run collection ships its changelog inside the artefact, so this is a local file:

Read-only / Saferelease history without leaving the controller
$ grep '^## v' collections/ansible_collections/community/docker/CHANGELOG.md | head -16
## v4\.7\.0
## v4\.6\.2
## v4\.6\.1
## v4\.6\.0
## v4\.5\.2
## v4\.5\.1
## v4\.5\.0
## v4\.4\.0
## v4\.3\.1
## v4\.3\.0
## v4\.2\.0
## v4\.1\.0
## v4\.0\.1
## v4\.0\.0
## v3\.13\.1
## v3\.13\.0

The backslashes are real. This changelog is generated from reStructuredText, where the dots in a version number are escaped; the file on disk contains them.

What you are reading for:

  • Cadence. Regular minor releases with patch releases between them is a project responding to reports. Four years of silence followed by one release is a project somebody dusted off.
  • Do patch releases exist at all? A collection that only ever publishes x.y.0 is not fixing bugs between features, which tells you what happens when you report one.
  • What the entries say. Bug fixes, security fixes and deprecation notices in the text are the signal. Nothing but “add new module” is a project that is growing rather than being maintained.
  • How breaking changes are handled. Look at a major bump and see whether the removals were deprecated first, with a removal_version, in an earlier release.

The one thing the artefact cannot tell you is how many humans are behind it, so this is the point at which you open the repository.

3. Where is the source

The repository and issues URLs from the manifest are the ones to follow, not a search result. What to look at, in ten minutes:

  • Open issues and their ages. Not the count — the distribution. Fifty open issues with recent maintainer replies is healthy. Twelve open issues, all unanswered, oldest three years, is not.
  • Commits in the last year, and by how many distinct people. A collection with one contributor is one bus away from unmaintained, regardless of how good that contributor is.
  • Is CI green, and does it test what you use? Collections usually publish a test matrix. If it tests against ansible-core 2.14–2.17 and you run 2.21, you are the test matrix.
  • Security contact. A SECURITY.md, or membership of a programme with one. Its absence tells you how a vulnerability report would go.

4. What does it drag in

You have already seen this one:

Read-only / Safeeverything that landed
$ ansible-galaxy collection list -p ./collections
# /home/opsuser/project/collections/ansible_collections
Collection                               Version
---------------------------------------- -------
community.docker                         4.7.0
community.library_inventory_filtering_v1 1.1.5

Every collection in that list needs its own answers to questions 1 through 7. A transitive dependency has exactly the same access as a direct one; the only difference is that you did not choose it and probably have not read it.

The requires_ansible line is worth pulling out at the same time, because it bounds compatibility:

Read-only / Safewhat core version it claims to support
$ grep requires_ansible collections/ansible_collections/community/docker/meta/runtime.yml
requires_ansible: '>=2.15.0'

That is a floor, not a ceiling, and it is a claim rather than a guarantee — >=2.15.0 does not mean anyone has tested it against 2.21. Cross-check against the repository’s CI matrix.

5. Privilege and credentials

Now open the source. This is the part people skip and it is the part that matters.

Read-only / Safewhich modules shell out
$ grep -rl 'run_command' collections/ansible_collections/community/docker/plugins/modules/
collections/ansible_collections/community/docker/plugins/modules/docker_stack_task_info.py
collections/ansible_collections/community/docker/plugins/modules/docker_stack_info.py

Two out of thirty-eight modules shell out; the rest talk to the Docker API. That is a good ratio and a meaningful one, because a module that shells out inherits every quoting and injection hazard the Linux course’s shell material covers, on the managed host, as root.

Then look at how credentials are handled. The single most informative grep in a collection is for no_log:

Read-only / Safeis the secret marked as a secret
$ grep -n -A 6 'argument_spec = dict' collections/ansible_collections/community/docker/plugins/modules/docker_login.py
    argument_spec = dict(
      registry_url=dict(type='str', default=DEFAULT_DOCKER_REGISTRY, aliases=['registry', 'url']),
      username=dict(type='str'),
      password=dict(type='str', no_log=True),
      reauthorize=dict(type='bool', default=False, aliases=['reauth']),
      state=dict(type='str', default='present', choices=['present', 'absent']),
      config_path=dict(type='path', default='~/.docker/config.json', aliases=['dockercfg_path']),
  )

no_log=True on password is what you want to see. Without it, the value appears in -vvv output, in the JSON the module returns, in callback plugin output and in any CI log that captured the run — the leak paths Part XXII catalogues.

The questions to answer for any module you will hand a credential to:

  • Is every secret parameter marked no_log=True? Grep the argument spec. One unmarked parameter is a finding.
  • Where does the credential end up? Read the code path. docker_login writes to ~/.docker/config.json on the target — base64-encoded, not encrypted, persisting after the play ends. That may be exactly what you want, and you should know it either way.
  • Does it phone anywhere you did not name? Grep for urlopen, requests, http in the plugin tree and check every hit resolves to an endpoint you configured.
  • Does it need root, or does it need root because nobody tried otherwise? Many modules work fine as an unprivileged user with the right group membership. Part XXI’s least-privilege material applies unchanged.

6. Check mode and diff, honestly

Read-only / Safethe attributes table is the honest answer
$ ansible-doc -t module community.docker.docker_container | sed -n '/ATTRIBUTES/,/NOTES/p'
ATTRIBUTES:

      check_mode:
      description: Can run in `check_mode' and return changed status prediction without
        modifying target.
      details:
      - When trying to pull an image, the module assumes this is never changed in check
        mode except when the image is not present on the Docker daemon.
      - This behavior can be configured with `pull_check_mode_behavior'.
      support: partial

      diff_mode:
      description: Will return details on what has changed (or possibly needs changing in
        `check_mode'), when in diff mode.
      support: full

      idempotent:
      description:
      - When run twice in a row outside check mode, with the same arguments, the second
        invocation indicates no change.
      details:
      - If `recreate=true' or `restart=true' the module is not idempotent.
      support: partial

support: partial with the specific caveat spelled out is a well-documented module. support: none is honest. A missing attributes block entirely is the finding — it means nobody stated a position, and a --check run that includes that module proves less than it appears to.

Part IX owns what check mode does and does not prove overall. Here the point is narrower: this is intake evidence. A collection whose modules all claim check_mode: full with no caveats, for operations that obviously cannot be predicted without performing them, is making a claim you should test rather than accept.

7. Licence

license = ['GPL-3.0-or-later', 'Apache-2.0'] above. Two things to check: that the licence exists at all, and that it is compatible with whatever you are doing. For internal infrastructure automation, almost anything is fine. For a collection you intend to vendor into a product you ship, GPL-3.0 is a decision for somebody other than you.

A collection with license_file: '' and an empty license list is a collection whose author did not think about it, which is a small signal about everything else.

The verdict

Write it down. Four lines is enough:

Read-only / Safedocs/collections/community.docker.md
community.docker 4.7.0 - approved for the container estate, 2026-08-11

Publisher:   Ansible Docker Working Group, github.com/ansible-collections
Maintenance: regular minor + patch releases; changelog current; CI matrix
           covers 2.21
Drags in:    community.library_inventory_filtering_v1 (pinned 1.1.5 by us,
           declared >=1.0.0 upstream)
Privilege:   docker group on target; 2 of 38 modules shell out; password
           parameters carry no_log
Check mode:  partial on docker_container, documented caveat on image pull
Licence:     GPL-3.0-or-later / Apache-2.0 - fine, internal use only
Reviewed by: <name>   Re-review on major version bump.

The point of writing it is not the document. It is that a verdict somebody can disagree with is a verdict, and “we’ve always used it” is not. When the next major version lands, this file tells the reviewer what changed since somebody last thought about it.

Knowledge check

Knowledge check · 4 questions

  1. Q1. Reviewing a collection, which part of the plugins/ tree deserves the closest reading and typically gets the least?

  2. Q2. Which of these are genuine signals when judging whether a collection is maintained? Select all that apply.

  3. Q3. A module takes a password parameter and its argument spec reads password=dict(type='str'). What is the finding?

  4. Q4. A collection installed as a transitive dependency needs the same intake review as one you named in requirements.yml.

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