Skip to content

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 -e on 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 -e on a CI command line instead of Vault or a secret manager.
  • become_user: root by default for tasks that only need one service account's privileges.
  • Forgetting no_log on a task that touches a secret, leaking it into -vvv output or CI logs.
  • Interpolating untrusted variables into shell: commands instead of using command or a real module.

Interview Questions

  • How does Ansible Vault protect secrets, and what are its limitations?
  • How would you scope become to avoid giving every task root access?
  • How can secrets accidentally leak through Ansible's own logging, and how do you prevent it?
  • Why is shell a bigger injection risk than command, 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.