Ansible Zero Trust Security: Implement Zero Trust Architecture for Enterprise Infrastructure
By Luca Berton · Published 2024-01-01 · Category: troubleshooting
Implement Zero Trust security architecture with Ansible. Automate micro-segmentation, identity-based access, certificate management, and continuous.
Introduction
Zero Trust assumes no implicit trust — every request must be authenticated, authorized, and encrypted regardless of network location. Implementing Zero Trust manually across hundreds of servers is impractical. Ansible automates the core pillars of Zero Trust: identity verification, micro-segmentation, certificate-based authentication, and continuous compliance checking.
See also: HashiCorp Vault Integration with Ansible Automation Platform: Credential Management at Scale
Zero Trust Pillars with Ansible
┌──────────────────────────────────────────────────┐
│ Zero Trust Architecture │
├──────────────┬───────────────┬───────────────────┤
│ Identity │ Network │ Data │
│ Verification│ Segmentation │ Protection │
├──────────────┼───────────────┼───────────────────┤
│ SSH certs │ iptables/nft │ Encryption at rest│
│ mTLS │ Security grps │ TLS everywhere │
│ Short-lived │ Micro-perimeter│ Vault secrets │
│ credentials │ Service mesh │ Key rotation │
└──────────────┴───────────────┴───────────────────┘
│ │ │
└───────────────┼───────────────┘
│
Continuous Verification
(Ansible scheduled scans)
SSH Certificate Authority
Replace static SSH keys with short-lived certificates signed by a CA:
---
- name: Configure SSH Certificate Authority
hosts: localhost
connection: local
tasks:
- name: Generate CA key pair
community.crypto.openssh_keypair:
path: /etc/ssh/ca/ssh_ca
type: ed25519
comment: "SSH CA - {{ ansible_date_time.date }}"
run_once: true
- name: Configure hosts to trust CA
hosts: all
become: true
tasks:
- name: Copy CA public key
ansible.builtin.copy:
src: /etc/ssh/ca/ssh_ca.pub
dest: /etc/ssh/trusted_ca.pub
mode: '0644'
- name: Configure sshd to trust CA
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
line: "TrustedUserCAKeys /etc/ssh/trusted_ca.pub"
regexp: "^TrustedUserCAKeys"
notify: restart sshd
- name: Set certificate max validity
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
line: "AuthorizedPrincipalsFile /etc/ssh/auth_principals/%u"
regexp: "^AuthorizedPrincipalsFile"
notify: restart sshd
- name: Create principals for users
ansible.builtin.copy:
content: |
deploy
admin
dest: "/etc/ssh/auth_principals/{{ item }}"
mode: '0644'
loop:
- deploy
- ubuntu
- root
- name: Issue short-lived user certificates
hosts: localhost
tasks:
- name: Sign user key (8-hour validity)
ansible.builtin.command: >
ssh-keygen -s /etc/ssh/ca/ssh_ca
-I "{{ user_name }}@{{ ansible_date_time.iso8601 }}"
-n deploy
-V +8h
{{ user_public_key_path }}
changed_when: true
See also: Ansible Container Security: Image Scanning, Runtime Protection, and Supply Chain Security
mTLS Everywhere
Deploy Internal CA and Certificates
- name: Deploy mTLS infrastructure
hosts: all
become: true
vars:
ca_cert_path: /etc/ssl/internal-ca
cert_validity: 90 # days
tasks:
- name: Create certificate directory
ansible.builtin.file:
path: "{{ ca_cert_path }}"
state: directory
mode: '0755'
- name: Generate server private key
community.crypto.openssl_privatekey:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.key"
size: 4096
- name: Generate CSR
community.crypto.openssl_csr:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.csr"
privatekey_path: "{{ ca_cert_path }}/{{ inventory_hostname }}.key"
common_name: "{{ inventory_hostname }}"
subject_alt_name:
- "DNS:{{ inventory_hostname }}"
- "DNS:{{ inventory_hostname }}.{{ domain }}"
- "IP:{{ ansible_default_ipv4.address }}"
- name: Sign certificate with internal CA
community.crypto.x509_certificate:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.crt"
csr_path: "{{ ca_cert_path }}/{{ inventory_hostname }}.csr"
ownca_path: "{{ ca_cert_path }}/ca.crt"
ownca_privatekey_path: "{{ ca_cert_path }}/ca.key"
ownca_not_after: "+{{ cert_validity }}d"
provider: ownca
- name: Deploy CA trust bundle
ansible.builtin.copy:
src: files/internal-ca.crt
dest: /etc/ssl/certs/internal-ca.crt
notify: update ca trust
handlers:
- name: update ca trust
ansible.builtin.command: update-ca-certificates
Configure Services for mTLS
- name: Configure Nginx with mTLS
ansible.builtin.template:
src: nginx-mtls.conf.j2
dest: /etc/nginx/sites-enabled/app.conf
notify: reload nginx
# templates/nginx-mtls.conf.j2
server {
listen 443 ssl;
server_name {{ inventory_hostname }};
ssl_certificate {{ ca_cert_path }}/{{ inventory_hostname }}.crt;
ssl_certificate_key {{ ca_cert_path }}/{{ inventory_hostname }}.key;
ssl_client_certificate {{ ca_cert_path }}/ca.crt;
ssl_verify_client on; # Require client certificate
ssl_protocols TLSv1.3;
ssl_prefer_server_ciphers on;
}
Micro-Segmentation
Host-Based Firewall (nftables)
- name: Implement micro-segmentation
hosts: all
become: true
tasks:
- name: Deploy nftables rules
ansible.builtin.template:
src: nftables.conf.j2
dest: /etc/nftables.conf
mode: '0600'
notify: reload nftables
- name: Enable nftables
ansible.builtin.systemd:
name: nftables
state: started
enabled: true
# templates/nftables.conf.j2
#!/usr/sbin/nft -f
flush ruleset
table inet filter {
chain input {
type filter hook input priority 0; policy drop;
# Allow established connections
ct state established,related accept
ct state invalid drop
# Allow loopback
iifname "lo" accept
# Allow SSH only from bastion
{% for bastion in groups['bastion'] %}
ip saddr {{ hostvars[bastion].ansible_host }} tcp dport 22 accept
{% endfor %}
# Allow monitoring from Prometheus
{% for mon in groups['monitoring'] %}
ip saddr {{ hostvars[mon].ansible_host }} tcp dport 9100 accept
{% endfor %}
{% if 'webservers' in group_names %}
# Web servers: allow HTTPS from load balancers only
{% for lb in groups['loadbalancers'] %}
ip saddr {{ hostvars[lb].ansible_host }} tcp dport 443 accept
{% endfor %}
{% endif %}
{% if 'databases' in group_names %}
# Databases: allow from app servers only
{% for app in groups['webservers'] %}
ip saddr {{ hostvars[app].ansible_host }} tcp dport 5432 accept
{% endfor %}
{% endif %}
# Log and drop everything else
log prefix "nftables-drop: " counter drop
}
chain output {
type filter hook output priority 0; policy accept;
}
}
See also: Ansible Secrets Management: Best Practices for Enterprise Credential Security
Continuous Verification
Security Compliance Scan
- name: Zero Trust compliance verification
hosts: all
become: true
tasks:
- name: Verify no password authentication
ansible.builtin.command: grep "^PasswordAuthentication no" /etc/ssh/sshd_config
register: ssh_nopass
changed_when: false
failed_when: ssh_nopass.rc != 0
- name: Verify TLS certificates not expired
community.crypto.x509_certificate_info:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.crt"
register: cert_info
- name: Alert on expiring certificates
ansible.builtin.debug:
msg: "WARNING: Certificate expires in {{ cert_info.not_after }} - {{ inventory_hostname }}"
when: cert_info.not_after | to_datetime('%Y%m%d%H%M%SZ') < (now() + timedelta(days=30))
- name: Verify firewall is active
ansible.builtin.systemd:
name: nftables
register: fw_status
- name: Assert firewall running
ansible.builtin.assert:
that: fw_status.status.ActiveState == 'active'
fail_msg: "CRITICAL: Firewall not running on {{ inventory_hostname }}"
- name: Check for unauthorized SSH keys
ansible.builtin.find:
paths: /root/.ssh/
patterns: "authorized_keys*"
register: ssh_keys
- name: Verify no static root SSH keys
ansible.builtin.assert:
that: ssh_keys.matched == 0
fail_msg: "Static SSH keys found on {{ inventory_hostname }} — should use CA certs"
- name: Check listening ports
ansible.builtin.shell: ss -tlnp | awk '{print $4}' | grep -v '^Local'
register: open_ports
changed_when: false
- name: Report unexpected ports
ansible.builtin.debug:
msg: "Unexpected ports on {{ inventory_hostname }}: {{ open_ports.stdout_lines }}"
when: open_ports.stdout_lines | length > expected_port_count | default(5)
Certificate Rotation
- name: Rotate certificates approaching expiry
hosts: all
become: true
tasks:
- name: Check certificate expiry
community.crypto.x509_certificate_info:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.crt"
register: cert_info
- name: Regenerate if expiring within 30 days
block:
- name: Generate new key
community.crypto.openssl_privatekey:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.key"
size: 4096
force: true
- name: Generate new CSR
community.crypto.openssl_csr:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.csr"
privatekey_path: "{{ ca_cert_path }}/{{ inventory_hostname }}.key"
common_name: "{{ inventory_hostname }}"
subject_alt_name:
- "DNS:{{ inventory_hostname }}"
- "IP:{{ ansible_default_ipv4.address }}"
- name: Sign new certificate
community.crypto.x509_certificate:
path: "{{ ca_cert_path }}/{{ inventory_hostname }}.crt"
csr_path: "{{ ca_cert_path }}/{{ inventory_hostname }}.csr"
ownca_path: "{{ ca_cert_path }}/ca.crt"
ownca_privatekey_path: "{{ ca_cert_path }}/ca.key"
ownca_not_after: "+90d"
provider: ownca
notify: reload services
when: >
cert_info.not_after | to_datetime('%Y%m%d%H%M%SZ')
< (now() + timedelta(days=30))
Best Practices
Short-lived credentials — SSH certs (8h), TLS certs (90d), API tokens (1h) No static keys — Replace authorized_keys with CA-signed certificates Default deny — Firewall drops everything not explicitly allowed Service-to-service mTLS — Every internal connection authenticated with certificates Continuous scanning — Schedule compliance checks in AAP (daily minimum) Automate rotation — Certificate and credential rotation should be automated, not manual Log everything — Firewall drops, certificate operations, access attempts Least privilege — Each service gets only the network access it needsFAQ
Zero Trust with legacy systems?
Start with firewall micro-segmentation (doesn't require app changes). Add mTLS where possible. Use jump hosts/bastion for systems that can't support modern auth.
How to handle certificate revocation?
Use short-lived certificates (hours/days) instead of long-lived + CRL. If certificates expire faster than your detection time, revocation becomes unnecessary.
Performance impact of mTLS?
Modern TLS 1.3 handshakes add ~1ms latency. For internal services, this is negligible. The security benefit far outweighs the cost.
Conclusion
Ansible makes Zero Trust achievable at scale by automating the hardest parts — certificate lifecycle, firewall rules, compliance verification, and credential rotation. Instead of a one-time security project, Zero Trust becomes a continuously enforced baseline across your entire infrastructure.
Related Articles
• HashiCorp Vault Integration with AAP • Ansible Compliance Automation • Ansible Network AutomationCategory: troubleshooting