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: truebecome: 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: falseEvery 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: trueto individual tasks that genuinely need it — treat a blanket play-level default as something to actively avoid, not a convenient shortcut. - Remember lookups and
becomeoperate on entirely separate boundaries — a lookup’s privileges are resolved locally, before a task’s own remote escalation is ever relevant. - Pair
become_userwith tasks that should run as a specific service account, not root, whenever that’s genuinely all the task needs. - Treat
delegate_to: localhostplus any activebecome: trueas requiring an explicitbecome: falseoverride, every time, without exception.