AnsibleXLVII · Controller SecurityController security
Every collection you install runs as root everywhere
What you'll learn
- State why installing a collection is equivalent to granting root on every managed node
- Describe precisely what ansible-galaxy collection verify checks, and what it leaves unchecked
- Review a third-party role for the constructs that would execute arbitrary code
- Design an install path that does not fetch unreviewed content at run time
- Decide when a dependency is worth taking and when writing the task yourself is cheaper
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
Part XXVII owns collections as a mechanism: namespaces, FQCNs,
requirements.yml, version pinning, resolution paths and signature
verification. This lesson takes the same file and asks one different
question.
What does installing this grant?
The answer is uncomfortable and it follows directly from lesson 1. Your automation runs as root on every managed node. A collection contributes code to that automation. Therefore installing a collection grants its authors — and anyone who compromises them — the ability to execute code as root on every host you manage.
Not “introduces a risk of”. That is what it does, and it is what it is for. A module exists to run privileged code on your hosts.
Why this is not the same as a library dependency
Application developers have a well-developed intuition about dependency risk, and it slightly under-estimates this case.
A compromised library in an application runs with the application’s privileges, inside whatever sandbox the application has, on the hosts the application runs on. That is often significant and it is bounded.
A compromised collection runs with your automation’s privileges, which are root, on every host in your inventory, with no sandbox, and with a network path to all of them already established. There is no intermediate step to exploit. The privilege is the feature.
The second difference is timing. An application dependency runs when the
application runs. A collection runs at the next ansible-playbook
invocation, which on most estates is scheduled, unattended, and
privileged — so a compromise reaches production without anyone being
present.
What verify actually checks
ansible-galaxy collection verify is frequently cited as the answer and
it is worth being precise about its scope, because the gap matters.
What it checks: that the files on disk match the checksums in the
collection’s manifest, and — without --offline — that the manifest
matches the canonical hash the Galaxy server holds. That is an
integrity check. It tells you the bytes you have are the bytes that
were published.
What it does not check:
Whether the published content is safe. A malicious collection published deliberately verifies perfectly. Integrity is not trustworthiness.
Dependencies. Verification covers the named collection. A collection
that pulls in three others does not cause those to be verified, and the
dependency is where an attacker would prefer to be — it is installed by
your requirements.yml without appearing in it.
Anything about the author. Whether the namespace changed hands, whether the maintainer account was taken over, whether the new maintainer is the same person as last year.
Signature verification with --keyring and --signature is a stronger
control where the content is signed, and Part XXVII covers it. It
raises the bar from “these bytes were published” to “these bytes were
published by a holder of this key” — which is genuinely better, and
still says nothing about intent.
Reading a role before its first run
Reviewing third-party content in full is not realistic. Reviewing it for the constructs that would execute arbitrary code is, and it takes minutes.
Look for:
shell and command with templated content. A task that builds a
command line from a variable is a place where controlling the variable
controls the command. Part XXIII covers command injection from variables
in detail; here it is a review signal.
get_url, uri and pip/npm pointing outside the collection. A
role that downloads a script at run time is a role whose actual content
is decided later, by whoever controls that URL. This is the single most
important thing to look for, because it moves the trust decision from
install time to run time, where you are not looking.
raw and script. Legitimate for bootstrapping, and an efficient
way to run arbitrary code with no module semantics to inspect.
Lookups and plugins. A collection can ship lookup, filter and action plugins that execute on the controller — the machine with the vault password and the SSH keys. Content that ships plugins deserves more attention than content that ships only modules, because the blast radius includes the controller itself and not merely the fleet.
Vendored binaries or archives. Anything not human-readable in a content repository is a question that needs an answer.
$ grep -rn --include=*.yml --include=*.yaml \
-e 'ansible.builtin.shell' -e 'ansible.builtin.raw' \
-e 'get_url' -e 'ansible.builtin.script' \
~/.ansible/collections/ansible_collections/<namespace>/<name>/roles/install/tasks/main.yml:23: - name: Fetch the installer
roles/install/tasks/main.yml:24: ansible.builtin.get_url:
roles/install/tasks/main.yml:31: ansible.builtin.shell: /tmp/installer.sh --unattendedIllustrative output
Three lines that describe the pattern exactly: download something from elsewhere, then execute it as root. Whatever that URL serves at the moment your scheduled run fires is what runs on your fleet. This is not necessarily malicious — a great many legitimate roles install software this way — but it is a decision you should make knowingly rather than inherit.
An install path worth having
The properties to aim for, in the order they pay off:
Pinned versions in requirements.yml. Part XXVII’s material. It is
the difference between “we run community.general” and “we run this
content”, and it makes an upgrade a reviewable event.
Installed at build time, not at run time. A controller that runs
ansible-galaxy install as part of every play is fetching unreviewed
content on a schedule. Install during a controller build or a deliberate
update, then verify.
From an internal mirror, --offline where possible. A vetted mirror
means content enters your estate through one reviewed path. It also
removes a run-time dependency on a public service, which is an
availability benefit that will sell the change to people unmoved by the
security argument.
With a review step for new entries and for version bumps. The review does not have to be deep. Reading the diff of a version bump for the constructs above catches the realistic attack, which is a compromise introduced into a later version of something you already trust.
Knowledge check
Knowledge check · 4 questions
Q1. Why is a compromised Ansible collection more serious than a compromised application library?
Q2. What does ansible-galaxy collection verify leave unchecked? Select all that apply.
Q3. Which finding in a third-party role should concern you most on first review?
Q4. Lookup, filter and action plugins shipped in a collection execute on the controller rather than on the managed node.
Passing score: 75%. Answers are checked in this browser.