Skip to main content
RunBook Academy

AnsibleXLVII · Controller SecurityController security

Every collection you install runs as root everywhere

Advanced⏱ ~22 minansible-coreansible-galaxy

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

Not yet marked complete on this device.

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.

Read-only / Safe

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.

Read-only / Safea first pass over freshly installed content
$ 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 --unattended

Illustrative 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

  1. Q1. Why is a compromised Ansible collection more serious than a compromised application library?

  2. Q2. What does ansible-galaxy collection verify leave unchecked? Select all that apply.

  3. Q3. Which finding in a third-party role should concern you most on first review?

  4. 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.