Skip to main content
RunBook Academy

AnsibleXIX · Privilege EscalationPrivilege escalation

Least privilege for the automation account

Advanced⏱ ~22 min🧪 Lab requiredansible-playbookvisudo

What you'll learn

  • State what a blanket automation sudo grant means for controller compromise
  • Identify which least-privilege levers remain when command scoping is unavailable
  • Split one automation identity into several with different grants
  • Design a Runas restriction that keeps service work out of root
  • Plan a sudo rule change so it cannot break every run at once

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.

Almost every Ansible estate starts with the same line, pushed to every host by the bootstrap play:

ansible ALL=(ALL) NOPASSWD: ALL

It works immediately, it never needs revisiting, and it makes a statement that nobody in the room intended to make:

whoever holds the controller’s SSH key is root on every host, without a password, without a second factor, and with the same audit trail as a legitimate run.

That is not a hypothetical. It is the arithmetic of what you deployed. The security question is not whether the automation is trustworthy; it is what a stolen key is worth, and with that line the answer is “the entire estate”.

The constraint that makes this harder than it should be

The become documentation is explicit, and lesson 1 of this part showed the mechanism:

Become must be general; it cannot be limited to specific commands.

What sudo evaluates is a shell invocation running a Python interpreter against a randomly-named temporary file. There is no module name in it. So the sudoers Cmnd_Spec — the field that would normally carry least privilege — cannot usefully enumerate what the automation does.

That constraint is real, and it is where most least-privilege efforts stop. They should not, because it removes exactly one of the five levers sudoers gives you.

sudoers fieldAvailable to scope Ansible?
Who — the user or group the rule applies toYes, and this is the strongest lever
Which hosts — the Host_ListYes, when sudoers is centrally distributed
Runas — which target identities may be assumedYes, and it is routinely left as ALL
TagsNOPASSWD, LOG_OUTPUTYes
Commands — the Cmnd_SpecMostly no, for module execution

Four out of five. The design work is using them.

Lever one: more than one automation identity

The single biggest improvement is also the least technical. One account with one key and one grant means every playbook has the maximum authority any playbook needs. Splitting by what the automation is for turns one blast radius into several.

Configuration change/etc/sudoers.d/50-automation on a database host
# Read-only inventory and compliance collection. No escalation at all -
# this account is not in any sudoers rule on this host, deliberately.
# ansible-audit  (no rule)

# Service operations: may become the database service account, and
# nothing else. Cannot become root on this host.
ansible-dbops   ALL=(postgres) NOPASSWD: ALL

# Platform operations: full root, on the hosts that need patching and
# package management, and only those.
ansible-platform ALL=(root) NOPASSWD: ALL

A compromised ansible-dbops key is a serious incident. It is not root on the database host, and it is nothing at all on the web tier where no rule mentions it.

Lever two: Runas is not ALL

(ALL) in a sudoers rule means “may become any user on this system”. For automation whose work is service-scoped, that is far more than needed, and narrowing it is a single-word change.

Configuration changenarrowing the Runas specification
# Before: any target user, including root.
ansible-appops  ALL=(ALL) NOPASSWD: ALL

# After: the application account and its deploy helper, nothing else.
ansible-appops  ALL=(appsvc, appdeploy) NOPASSWD: ALL

The playbook side is become_user: appsvc, which lesson 1 argued should be written explicitly anyway. The two changes together mean a task that tries to escalate to root on that host fails, loudly, in review or in CI, rather than succeeding because the grant permitted more than the design intended.

That is the property to aim for throughout: the grant is narrow enough that a mistake in a playbook produces a failed task rather than an unintended privileged operation.

Lever three: the host list is a real boundary

ALL= in the middle field means “on all hosts”, which matters only when sudoers is distributed centrally — via LDAP, SSSD, or a configuration-managed file that is identical everywhere.

If your estate distributes one sudoers file to every host, the host field can encode the same separation your inventory groups do — with the crucial difference that the managed node enforces it. An inventory group is a statement about what you intend to target. A sudoers host restriction is a statement the target itself checks.

Lever four: make the escalation auditable

NOPASSWD removes the authentication step. It does not have to remove the record.

Defaults:ansible-platform log_output, logfile=/var/log/sudo-ansible.log
Defaults:ansible-platform !syslog

log_output records what the escalated session did. For automation, where every session is a shell running a Python module, the value is not in reading it routinely — it is in having it during an incident, when the question is “what did this account do on this host between 02:00 and 02:40?”

Two costs to size deliberately: the log grows with every run, and it captures module output, which for a module handling secrets means the secret can land in the log. Scope the setting to the automation user rather than setting it globally, exclude accounts that handle credential material, and give the log a rotation policy before you enable it.

Part XLIII covers the automation-side audit trail — callbacks, run records, who triggered what. This is the managed-node side of the same question, and it is the one an attacker cannot edit from the controller.

Deploying the grant without locking yourself out

A sudoers change is a fleet-wide change to the mechanism your automation uses to make changes. Get it wrong and you cannot use Ansible to fix it.

Configuration changea sudoers deployment that refuses to install a broken file
- name: Deploy the automation sudo grant
ansible.builtin.template:
  src: automation-sudoers.j2
  dest: /etc/sudoers.d/50-automation
  owner: root
  group: root
  mode: '0440'
  validate: /usr/sbin/visudo -cf %s
become: true

validate catches a malformed file. It does not catch a well-formed file that grants less than your playbooks need, and that is the failure that takes the fleet out.

Read-only / Safethe pre-flight escalation check
ansible -i inventories/prod all --become --become-user root \
-m ansible.builtin.command -a "id -un" \
| grep -E 'UNREACHABLE|FAILED|root'

The review that keeps it honest

A privilege review for an automation account asks four questions, and the answers should be findable without logging into anything:

  1. Which accounts can escalate, on which hosts? Enumerate the sudoers rules mentioning any automation identity, per environment.
  2. To which target users? Anything reading (ALL) is a finding unless it is deliberately the platform account.
  3. What holds the keys? One controller, or six laptops? Part III asked this; the answer determines what the grant is actually exposed to.
  4. How would you notice a change? If the security team tightened a rule tonight, would anything tell you before tomorrow’s run failed?

Knowledge check

Knowledge check · 4 questions

  1. Q1. An estate deploys "ansible ALL=(ALL) NOPASSWD: ALL" to every host. What has that decided?

  2. Q2. Ansible cannot scope escalation by command. Which sudoers levers remain genuinely useful? Select all that apply.

  3. Q3. Running plays with --limit constrains what the automation account is permitted to change, so it is part of the least-privilege design.

  4. Q4. The security team will tighten the fleet sudo rules this week. What most reduces the chance of a fleet-wide run failure?

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