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 — root only where root is needed, the app account for app work
- hosts: app
tasks:
- name: Install the runtime (needs root)
ansible.builtin.package:
name: python3.12
state: present
become: true
- name: Run database migrations as the application account
ansible.builtin.command: /opt/myapp/venv/bin/myapp migrate
become: true
become_user: myapp
register: migrate
changed_when: "'No migrations to apply' not in migrate.stdout"
- name: Check the app's health endpoint (no escalation at all)
ansible.builtin.uri:
url: http://localhost:8080/healthz
Scope both SSH access and become targets to exactly what a given task needs. Running application commands as root "because the play has become: true" is one of the most common over-privileged patterns in real playbooks: a migration script run as root can write root-owned files the app later can't read, and any bug in it runs with full privileges. Managing system services still needs root (or a polkit rule), so that's where escalation belongs.
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 value like "x; rm -rf /" becomes a second shell command
- ansible.builtin.shell: "curl {{ user_supplied_url }}"
# Safer — no shell, and argv keeps the value as exactly one argument
- ansible.builtin.command:
argv: [curl, --fail, --silent, --, "{{ user_supplied_url }}"]
# Best — a real module, no external process 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. command removes the shell, but a value that starts with - can still be read as an option (argument injection), which is why the example uses argv and ends options with --. When you genuinely need shell, escape each interpolated value with the quote filter: "grep -- {{ pattern | quote }} /var/log/app.log".
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.