Encryption #
Encryption is the last line of defense in an Ansible security architecture. It doesn’t replace access control or auditing, but it ensures that even when other controls fail — a stolen laptop, a leaked backup disk, logs exposed to third parties — the stolen data still can’t be read without the right key. Ansible touches encryption at many points: variables containing passwords are encrypted with Vault, transport connections to managed nodes are secured with SSH/TLS, SSL/TLS certificates for internal services must be distributed and rotated, and sensitive data in logs must be redacted. This article discusses each of these points systematically — when to use symmetric versus asymmetric algorithms, how to manage key lifecycles, and common safe patterns for each scenario. A good understanding of each algorithm’s strengths and weaknesses prevents choices that look secure but are actually fragile.
When Is Encryption Needed? #
SENSITIVE DATA:
✓ Passwords and API keys → ENCRYPT REQUIRED (Vault)
✓ Private keys (SSH, TLS) → ENCRYPT REQUIRED (file permissions + Vault)
✓ Database connection strings with credentials → ENCRYPT REQUIRED
✓ Production configuration files (web server, app config) → ENCRYPT REQUIRED
✓ Backup files containing data → ENCRYPT REQUIRED (LUKS/dm-crypt)
✓ Session tokens for users → ENCRYPT in transit (TLS), EXPIRE quickly
NON-SENSITIVE DATA:
✗ Hostnames and server IPs → not needed
✗ Directory paths (/var/log/app) → not needed
✗ Regular usernames → not needed
✗ Non-secret configuration (ports, log levels) → not needed
Encryption adds CPU overhead and operational complexity. Not all data needs encrypting — only what has material risk if leaked. Classifying sensitive data is the first step before determining the right encryption strategy.
Encryption Architecture in Ansible #
flowchart LR
A["Ansible<br/>Control Node"] -->|"SSH/TLS"| B["Managed Node"]
A -->|"Vault encrypt"| C["Vault File<br/>AES256"]
A -->|"TLS"| D["Secret Manager<br/>Vault/AWS SM"]
B -->|"SSH key"| E["authorized_keys"]
B -->|"TLS cert"| F["Service<br/>nginx/postgres"]
D -->|"runtime inject"| A
C -->|"decrypt at runtime"| A
A -->|"AnsibleModule"| BEncryption happens at four points in the Ansible ecosystem: Vault files on the control node (encrypted at commit, decrypted at runtime), transport connections (SSH between control and managed nodes, or HTTPS to APIs), certificates on managed nodes (TLS for internal services), and external secret managers (Vault, AWS Secrets Manager) that provide secrets to playbooks at runtime without ever storing plaintext on Ansible disk.
Symmetric vs Asymmetric Algorithms #
flowchart TD
A["Choose Algorithm"] --> B{"Data type?"}
B -->|"Large files/data<br/>encrypted once<br/>and read many times"| C["Symmetric<br/>AES-256-GCM"]
B -->|"Keys need<br/>sharing with<br/>many parties"| D["Asymmetric<br/>RSA/Ed25519/ECDSA"]
C --> E["Vault file<br/>LUKS disk<br/>TLS bulk encryption"]
D --> F["SSH key pair<br/>TLS handshake<br/>Digital signature"]The choice between symmetric and asymmetric algorithms determines the entire encryption operation. Symmetric (AES-256-GCM) suits large amounts of data encrypted once and read many times — Vault files, full disk encryption, or bulk data encryption. Asymmetric (RSA, Ed25519, ECDSA) suits situations where public keys must be shared with many parties without sacrificing the private key — TLS handshakes, SSH key pairs, and digital signatures. Almost all modern systems use both in a hybrid fashion: TLS for example uses RSA/ECDSA for session key exchange, then AES for bulk encryption.
Algorithm Comparison Table #
| Algorithm | Type | Key length | Use case | Notes |
|---|---|---|---|---|
| AES-256-GCM | Symmetric | 256 bit | Vault files, disk encryption, TLS bulk | Industry standard, hardware acceleration on modern CPUs |
| ChaCha20-Poly1305 | Symmetric | 256 bit | Environments without hardware AES acceleration | Fast in software, popular on mobile |
| RSA-2048 | Asymmetric | 2048 bit | Legacy TLS, signatures | Outdated, migration to ECDSA recommended |
| RSA-4096 | Asymmetric | 4096 bit | High-security signatures | Slow for handshakes, use only if truly needed |
| ECDSA P-256 | Asymmetric | 256 bit | TLS 1.3, SSH, modern | Fast, small signature size |
| Ed25519 | Asymmetric | 256 bit (key) | SSH (preferred), signatures | Fastest, 64-byte signatures |
| bcrypt | Hash + salt | variable | Password hashing | Minimum cost factor 12 in 2024 |
| Argon2id | Hash + memory-hard | variable | Newer password hashing | Stronger than bcrypt against GPU attacks |
| scrypt | Hash + memory-hard | variable | Password hashing | Argon2id alternative |
For Ansible, AES-256 via Vault is the default choice. For SSH keys, Ed25519 provides the best security and performance. For passwords, use bcrypt with cost factor ≥ 12 or Argon2id if the library is available.
Ansible Vault — At-Rest Encryption #
Vault encrypts YAML files with AES-256. Vault files can still be included in playbooks as usual, but their contents can’t be read without the vault password. Ansible recognizes Vault files automatically based on the $ANSIBLE_VAULT;1.1;AES256 header.
# Create a new Vault file
ansible-vault create secrets/production.yml
# Edit a Vault file (prompts for password)
ansible-vault edit secrets/production.yml
# Encrypt an existing YAML file
ansible-vault encrypt existing-file.yml
# View contents without editing
ansible-vault view secrets/production.yml
# Decrypt for operations needing plaintext (rare)
ansible-vault decrypt secrets/production.yml
Every operation requires the vault password. The password can be entered interactively (--ask-vault-pass), from a file (--vault-password-file), from an environment variable (ANSIBLE_VAULT_PASSWORD_FILE), or from a custom script (--vault-password-client) that fetches the password from an external secret manager at runtime.
# ANTI-PATTERN: vault password in Git
# Wrong .gitignore: forgot to ignore the vault password file
# Consequence: vault_password_prod.txt gets committed, attacker can decrypt all vaults
# $ cat /var/log/git-history/vault_password_prod.txt
# my-s3cr3t-vault-password
# CORRECT: vault password from a secret manager, injected at runtime
# playbooks/deploy.yml
- name: Deploy with the vault password from AWS Secrets Manager
hosts: production
vars:
vault_password: "{{ lookup('aws_secret', 'ansible/vault-password', region='ap-southeast-1') }}"
vars_files:
- secrets/production.yml # Vault file, decrypted at runtime
tasks:
- name: Deploy the application
template:
src: app.conf.j2
dest: /etc/myapp/app.conf
mode: '0640'
owner: root
group: myapp
Injecting the vault password at runtime from an external secret manager ensures the password is never stored in Git, in Ansible configuration files, or in persistent environment variables.
Multi-Vault Strategy #
One vault password for all environments is very inflexible. Each environment should have its own vault, with a different password, so a leak in one vault doesn’t expose other environments.
# group_vars/all/vaults.yml
# Vault definitions per environment — included in vars_files
vault_files:
development: secrets/dev.yml
staging: secrets/staging.yml
production: secrets/production.yml
# Vault passwords also per environment
vault_passwords:
development: "{{ lookup('env', 'ANSIBLE_VAULT_DEV_PASSWORD') }}"
staging: "{{ lookup('env', 'ANSIBLE_VAULT_STAGING_PASSWORD') }}"
production: "{{ lookup('aws_secret', 'ansible/vault-password', region='ap-southeast-1') }}"
# ANTI-PATTERN: one vault for all environments
# secrets/all.yml — contains production AND dev AND staging passwords
# vault_password: 'one-password-for-everyone'
# A leak = compromise of the entire organization
# CORRECT: vault per environment + audit
# secrets/production.yml
---
production_db_password: "strong-random-32-char-password"
production_api_key: "very-long-random-api-key"
production_tls_cert: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
Separating vaults per environment enables separate auditing — the production vault is only accessed from trusted CI/CD, while the development vault can be accessed from developer workstations without sacrificing production security.
Rotate passwords periodically. Vault passwords, database passwords, and API keys must be rotated at least every 90 days. Ansible can automate rotation with a playbook that generates a new password, updates the Vault, and restarts services depending on that secret. Without rotation, a leaked password stays valid until the leak is detected — which could take years.
Transport Encryption with SSH #
The connection between the Ansible control node and managed nodes is always encrypted by SSH. But weak SSH configuration (old protocols, weak ciphers, weak key exchange) reduces the effectiveness of transport encryption.
# roles/ssh-hardening/tasks/main.yml
---
- name: Deploy a secure sshd_config
template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: '0600'
notify: Restart sshd
- name: Disable old protocols and ciphers
lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
state: present
loop:
- regexp: '^#?Protocol'
line: 'Protocol 2'
- regexp: '^#?Ciphers'
line: 'Ciphers [email protected],[email protected],[email protected]'
- regexp: '^#?MACs'
line: 'MACs [email protected],[email protected]'
- regexp: '^#?KexAlgorithms'
line: 'KexAlgorithms [email protected],diffie-hellman-group-exchange-sha256'
The combination of chacha20-poly1305 + curve25519-sha256 + hmac-sha2-512-etm provides strong transport security without sacrificing performance, even on CPUs without hardware AES acceleration.
SSH Key Distribution #
SSH key pairs provide more secure authentication than passwords. Distributing public keys to managed nodes should be automated, but private keys must stay secure.
# Generate an SSH key pair on the control node
ssh-keygen -t ed25519 -C "ansible-control-prod" -f ~/.ssh/ansible_ed25519
# Public key distributed to managed nodes via Ansible
ansible all -m authorized_key -a "
user=ansible
key='{{ lookup('file', '~/.ssh/ansible_ed25519.pub') }}'
state=present
"
# ANTI-PATTERN: SSH key without passphrase, manual copy to every server
# ssh-keygen -t rsa -b 2048 -N "" -f deploy_key
# scp deploy_key.pub user@server1:~/.ssh/authorized_keys
# scp deploy_key.pub user@server2:~/.ssh/authorized_keys
# ... (thousands of servers)
# scp deploy_key user@laptop:/tmp/ # leaks to a new laptop
# CORRECT: passphrase + Ansible push + periodic rotation
# 1. Generate a key with a passphrase, store the passphrase in Vault
# 2. Ansible pushes the public key to all managed nodes
# 3. ssh-agent on the control node holds the decrypted key at runtime
# 4. Rotate the keypair every 6-12 months
- name: Distribute the new SSH public key to managed nodes
hosts: all
tasks:
- name: Add the ansible public key to authorized_keys
authorized_key:
user: ansible
key: "{{ lookup('file', '~/.ssh/ansible_ed25519.pub') }}"
state: present
exclusive: true # Remove other keys, ensure only the new key is valid
notify: Audit SSH key change
An SSH key without a passphrase on a developer laptop is a security nightmare — once the laptop is stolen, the attacker has access to the entire infrastructure. Passphrase + ssh-agent provides security without sacrificing convenience.
SSL/TLS Certificates #
Internal services (nginx, PostgreSQL, etc.) often use TLS for encrypted communication. Certificates must be generated, distributed, and rotated periodically.
# roles/tls-cert/tasks/main.yml
---
- name: Generate the private key
community.crypto.openssl_privatekey:
path: /etc/ssl/private/{{ service_name }}.key
size: 4096
type: RSA
mode: '0600'
owner: root
group: ssl-cert
register: private_key
- name: Generate the CSR (Certificate Signing Request)
community.crypto.openssl_csr:
path: /etc/ssl/csr/{{ service_name }}.csr
privatekey_path: /etc/ssl/private/{{ service_name }}.key
common_name: "{{ service_name }}.internal.example.com"
subject_alt_name:
- "DNS:{{ service_name }}.internal.example.com"
- "DNS:{{ service_name }}"
organization_name: "Example Corp"
organizational_unit_name: "Infrastructure"
country_name: "ID"
key_usage:
- digitalSignature
- keyEncipherment
extended_key_usage:
- serverAuth
- clientAuth
when: private_key.changed
- name: Self-sign the certificate (for an internal CA)
community.crypto.x509_certificate:
path: /etc/ssl/certs/{{ service_name }}.crt
privatekey_path: /etc/ssl/private/{{ service_name }}.key
csr_path: /etc/ssl/csr/{{ service_name }}.csr
provider: selfsigned
selfsigned_not_after: "+365d" # Valid for 1 year
mode: '0644'
owner: root
group: root
when: private_key.changed
For internal services, self-signed certificates from an internal private CA are sufficient. For services accessed from the internet, use Let’s Encrypt or another external CA with the ACME protocol for auto-renewal.
Self-signed certificates need explicit trust. Browsers and other TLS clients reject self-signed certificates from unknown CAs. For internal infrastructure, deploy the CA certificate to all managed nodes (via Ansible, of course) and add it to the system trust store. Without this step, internal TLS services will always fail the handshake with a “certificate verify failed” error.
Backup Encryption #
Backups are often the weak point in an encryption strategy — created from a live database (plaintext), stored on disk or cloud storage without encryption, and carrying sensitive data to a wider attack surface. Backups must be encrypted before leaving the originating host.
# Backup PostgreSQL + encrypt with GPG before upload
pg_dump -Fc production_db > /tmp/production_db.dump
gpg --symmetric --cipher-algo AES256 \
--output /backup/production_db.dump.gpg \
/tmp/production_db.dump
rm -f /tmp/production_db.dump # Remove the plaintext
aws s3 cp /backup/production_db.dump.gpg \
s3://company-backups/production/$(date +%Y%m%d)/
shred -u /backup/production_db.dump.gpg # Remove locally after upload
Or use LUKS/dm-crypt for full disk encryption on backup volumes — the entire volume is encrypted at the block device level, transparent to applications.
Redacting Sensitive Data in Logs #
Logs that display passwords, API keys, or private keys in plaintext are a delayed data leak — usually undetected until an audit or until the logs are exposed to third parties.
# ANTI-PATTERN: logging output that displays rendered templates
# ansible-playbook site.yml -vvv
# TASK [Deploy config] ****
# ok: [server1] => {
# "msg": "Rendered template content: db_password=MyS3cretP@ss"
# }
# This log now exists in syslog, journal, and possibly CI artifacts
# CORRECT: no_log: true for tasks that render secrets
- name: Deploy the database configuration
template:
src: db.conf.j2
dest: /etc/myapp/db.conf
mode: '0640'
no_log: true # Don't log this task's output
- name: Test the database connection
command: psql -f /etc/myapp/db.conf -c "SELECT 1"
no_log: true # Query output might leak credentials
changed_when: false
no_log: true is one line that prevents many incidents. Task output won’t appear in Ansible logs, syslog, or CI artifacts. For CI/CD that stores log artifacts, without no_log: true you’re effectively writing passwords to public storage.
Decision Tree — Choosing the Encryption Approach #
flowchart TD
A["What data<br/>is being encrypted?"] --> B{"Category"}
B -->|"Secret variables<br/>passwords, API keys"| C["Vault file<br/>AES-256"]
B -->|"Transport<br/>SSH/TLS"| D["SSH key pair<br/>or TLS cert"]
B -->|"Disk/storage"| E["LUKS/dm-crypt"]
B -->|"Backup files"| F["gpg symmetric<br/>or LUKS"]
B -->|"User passwords"| G["bcrypt cost 12+<br/>or Argon2id"]
C --> H{"Environment?"}
H -->|"Production"| I["Separate Vault<br/>+ AWS SM inject"]
H -->|"Dev/staging"| J["Separate Vault<br/>+ env var password"]
D --> K{"Internal or<br/>public-facing?"}
K -->|"Internal"| L["Self-signed cert<br/>+ private CA"]
K -->|"Public"| M["Let's Encrypt<br/>+ ACME renewal"]This decision tree helps choose the right encryption approach for various data types. The key is classifying the data first — then choosing the algorithm.
Summary #
- AES-256 via Vault is the default for at-rest data in Ansible. Use a separate Vault per environment, and inject the vault password from an external secret manager at runtime.
- Ed25519 is the best choice for SSH keys — fast, small, and secure. Always use a passphrase and keep the decrypted key in ssh-agent, never in a passphrase-less file.
- Internal TLS can use self-signed certificates from a private CA — deploy the CA certificate to all managed nodes via Ansible for trust.
- Backups must be encrypted before leaving the host — GPG symmetric for individual files, LUKS for full disk encryption on backup volumes.
no_log: trueis a mandatory line for tasks that render templates containing credentials or whose output might leak secrets.- bcrypt with cost factor ≥ 12 or Argon2id for password hashing. Never store plaintext passwords or hashes with SHA1/MD5.
- Periodic rotation — Vault passwords, API keys, and SSH keys must be rotated at least every 90 days. Ansible can automate the entire rotation process.