Skip to content

Privilege Escatation Best Practices and Gotchas


This closing chapter covers one more genuinely confusable boundary, the principle that should guide every become decision in practice, and an honest look back at what several earlier chapters were quietly relying on all along.

become And Lookups: Two Completely Separate Boundaries

Recall the entire lookups section’s central theme: a lookup runs on the control node, always — before a task’s own execution on the remote host even begins. become: true on a task has zero effect on the privileges a lookup inside that task’s arguments runs with, because the lookup already resolved, locally, as your own control-node user, before the remote task’s escalation was ever relevant.

- name: Read a control-node file that needs root to read there
  ansible.builtin.debug:
    msg: "{{ lookup('file', '/etc/shadow') }}"
  become: true

become: true here does nothing to help read /etc/shadow on the control node — that lookup either succeeds or fails based on your own local user’s permissions, resolved before this task’s remote execution even starts. become: true only ever affects the remote task itself, which in this example is just debug, trivially unaffected by any file permission at all. If a lookup genuinely needs elevated local privileges, that’s an entirely different problem — running ansible-playbook itself with different local privileges — completely unrelated to become.

The Principle Of Least Privilege

The practical rule this whole section has been building toward: use become: true only on the specific tasks that genuinely need elevated privileges, never as a blanket play-level default “just in case.” This isn’t just tidiness — a task running unnecessarily as root, if it has a bug, causes meaningfully more damage than the same bug running as an unprivileged user would. Scoping escalation tightly is a direct, practical security measure, not just good style.

A Combined Example: Selective Escalation In A Real Deployment

Bringing this whole section together — selective become, become_user, the delegate_to/become: false pairing, and a vault-sourced become password:

- name: Full Deployment With Careful Privilege Escalation
  hosts: servers
  vars_files:
    - secrets.yaml
  tasks:
    - name: Install myapp
      ansible.builtin.package:
        name: myapp
        state: present
      become: true

    - name: Deploy config as the app's own service account
      ansible.builtin.template:
        src: app.conf.j2
        dest: /etc/myapp/app.conf
      become: true
      become_user: myapp_service

    - name: Restart service
      ansible.builtin.systemd:
        name: myapp
        state: restarted
      become: true

    - name: Log deployment locally, without escalation
      ansible.builtin.debug:
        msg: "Deployed to {{ inventory_hostname }}"
      delegate_to: localhost
      become: false

Every escalation here is deliberate and scoped to exactly the task that needs it — nothing more.

Revisiting Earlier Examples: What Was Actually Happening

Worth being honest about directly: every package install throughout the common modules section, every file task creating a root-owned directory, every systemd restart across this entire course, implicitly depended on privilege escalation being available — our lab’s passwordless alice/bob sudo access, established all the way back in the installation chapter. Some of those earlier examples didn’t always show become: true explicitly in every snippet, for focus and brevity on whatever that chapter was actually teaching. Run literally as written, several of them would have needed it to actually succeed against a non-root user. That’s not a hidden mistake so much as an honest simplification worth naming now that this section exists to cover the mechanism properly — every one of those earlier tasks touching system-level resources was leaning on exactly what this section just explained.

Best Practices

  • Scope become: true to individual tasks that genuinely need it — treat a blanket play-level default as something to actively avoid, not a convenient shortcut.
  • Remember lookups and become operate on entirely separate boundaries — a lookup’s privileges are resolved locally, before a task’s own remote escalation is ever relevant.
  • Pair become_user with tasks that should run as a specific service account, not root, whenever that’s genuinely all the task needs.
  • Treat delegate_to: localhost plus any active become: true as requiring an explicit become: false override, every time, without exception.
Last updated on