Security¶
What You'll Learn¶
- Why Ansible security has fleet-wide blast radius, not single-host blast radius
- The concrete checklist: secrets, privilege escalation, and logging hygiene
- Where shell injection actually creeps into playbooks
Why This Matters¶
Ansible frequently runs with become: true against every host in an environment at once, often carrying secrets along the way. A mistake here doesn't compromise one server — it compromises whatever the playbook's reach and privileges cover, which in production is usually "everything."
Secrets¶
- Never commit plaintext secrets, even temporarily, even to a private repository — use Vault or an external secret manager from day one.
- Prefer an external secret manager (HashiCorp Vault, AWS Secrets Manager) via a lookup plugin over committing even encrypted values, when the team is large enough to justify the operational overhead.
- Never pass a secret via
-eon a CI command line — it can land in shell history, process listings (ps aux), and CI job logs.
# Risky — the token is visible in CI logs and shell history
ansible-playbook deploy.yml -e "api_token=abc123secret"
# Better — sourced from Vault or a CI secret store, referenced by lookup
ansible-playbook deploy.yml
- name: Register with the API
ansible.builtin.uri:
url: "https://api.internal/register"
headers:
Authorization: "Bearer {{ api_token }}"
no_log: true
no_log: true suppresses this task's arguments and result from all output — necessary any time a task's input or output could contain a secret, not just when a password is the obvious argument name.
Least-Privilege become¶
# Overbroad — every task in this play runs as root
- hosts: app
become: true
become_user: root
tasks: ...
# Scoped — only escalates to the service account this task actually needs
- name: Restart the app service
ansible.builtin.systemd_service:
name: myapp
state: restarted
become: true
become_user: myapp
Scope both SSH access and become targets to exactly what a given task needs — become_user: root by default, for tasks that only need a service-specific account's privileges, is one of the most common over-privileged patterns in real playbooks.
SSH Key Hygiene¶
Key-based auth is the production default (see SSH and Connectivity) — scope keys per environment or per team rather than one shared key reused everywhere, and rotate on a schedule, not only after a suspected incident.
Shell Injection¶
# Risky — a variable is interpolated directly into a shell command
- ansible.builtin.shell: "curl {{ user_supplied_url }}"
# Safer — command avoids the shell entirely; no injection surface
- ansible.builtin.command: "curl {{ user_supplied_url }}"
# Best — a real module, no shell involved at all
- ansible.builtin.uri:
url: "{{ user_supplied_url }}"
Any shell: task built from a variable that isn't fully trusted is a real command-injection vector — see Command vs. Shell. Prefer command (no shell, no injection surface) or a real module over shell whenever the input isn't a fixed, trusted string.
Common Mistakes¶
- Passing secrets via
-eon a CI command line instead of Vault or a secret manager. become_user: rootby default for tasks that only need one service account's privileges.- Forgetting
no_logon a task that touches a secret, leaking it into-vvvoutput or CI logs. - Interpolating untrusted variables into
shell:commands instead of usingcommandor a real module.
Interview Questions¶
- How does Ansible Vault protect secrets, and what are its limitations?
- How would you scope
becometo avoid giving every task root access? - How can secrets accidentally leak through Ansible's own logging, and how do you prevent it?
- Why is
shella bigger injection risk thancommand, and when is that risk actually relevant?
See Interview Prep: Roles, Collections & Modules and Senior & Architect Questions for the deeper scenarios.
Next¶
Continue to Performance.