Traditional network security relies on perimeter defenses, but modern threats require a “never trust, always verify” approach. Organizations spend hours manually coordinating identity, secrets, policy, and network controls across fragmented tools, leading to security gaps and operational overhead. This guide demonstrates how to build and operate a Zero Trust Architecture using Red Hat Ansible Automation Platform as the central orchestration layer, reducing manual security operations by 80% while improving compliance posture.
Table of Contents
Zero Trust Architecture (ZTA) is a security framework that eliminates implicit trust and continuously validates every user, device, and application attempting to access resources. Unlike traditional perimeter-based security that assumes internal network traffic is safe, Zero Trust assumes breach and enforces strict verification at every layer.
What is Zero Trust? – redhat.com
The core principles include:
Traditional approaches require teams to manually coordinate identity (LDAP/AD), secrets (credential vaults), policy engines (OPA/RBAC), network controls (firewalls/ACLs), and monitoring (SIEM) across siloed tools. This creates security gaps, configuration drift, and slow incident response.
| Approach | Trust Model | Credential Lifetime | Policy Enforcement | Incident Response |
|---|---|---|---|---|
| Traditional Perimeter | Trust internal network traffic | Passwords last months/years | Manual reviews, change tickets | Human-driven, hours to days |
| Zero Trust Architecture | Verify every request | Credentials expire in minutes | Policy-as-code, enforced at platform level | Automated, seconds to minutes |
NIST SP 800-207: Zero Trust Architecture – csrc.nist.gov
This solution demonstrates an end-to-end Zero Trust Architecture using Ansible Automation Platform to orchestrate identity, secrets, policy, and network controls.
What makes up the solution?
| Persona | Challenge | What They Gain |
|---|---|---|
| Manually orchestrating identity, secrets, policy, and network controls across siloed tools | Reference architecture showing how AAP unifies Zero Trust components with automated policy enforcement | |
| Credential sprawl, standing access, and manual secret rotation create security risk and operational toil | Executable playbooks for dynamic credentials, short-lived certificates, and automated credential revocation that eliminate standing secrets | |
| Proving continuous compliance requires manual evidence collection across disconnected systems | Unified audit trail capturing OPA policy decisions, Vault credential lifecycle, IdM authentication, and network changes with full traceability | |
| Network segmentation requires manual firewall rules and switch ACL changes prone to drift | Automated micro-segmentation with policy-driven VLAN management, firewall rules, and ACL enforcement validated against CMDB |
Recommended Demos and Self-Paced Labs:
Source Code:
| Collection | Type | Purpose |
|---|---|---|
| ansible.posix | Certified | Firewall management, mount, selinux, sysctl |
| community.general | Validated | OPA queries, system configuration |
| community.hashi_vault | Validated | Vault integration, secrets lookups, SSH CA |
| netbox.netbox | Validated | CMDB inventory source, DCIM operations |
| arista.eos | Certified | Arista cEOS switch configuration, VLAN management |
| ansible.controller | Certified | Automation Controller configuration as code |
| System | Required | Purpose | Examples |
|---|---|---|---|
| Identity Provider | Yes | LDAP/Kerberos authentication and authorization | Red Hat IdM (FreeIPA), Active Directory |
| Secrets Manager | Yes | Dynamic credentials and SSH certificate authority | HashiCorp Vault, CyberArk |
| Policy Engine | Yes | Deny-by-default authorization decisions | Open Policy Agent (OPA) |
| CMDB | Yes | Infrastructure source of truth | Netbox, ServiceNow CMDB |
| SIEM / Log Analytics | Optional | Bypass detection and audit correlation in defense-in-depth scenarios | Splunk, Elastic Stack, IBM QRadar, Wazuh |
| Network Infrastructure | Yes | Micro-segmentation and traffic control | Arista cEOS, Cisco, Juniper |
| Workload Identity | Optional but recommended | Cryptographic workload attestation | SPIFFE/SPIRE |
| Git Server | Optional | GitOps workflow triggers | Gitea, GitHub, GitLab |
| Use Case | Impact Level | Reversibility |
|---|---|---|
| Use Case 1 (Verification) | None | N/A (read-only checks) |
| Use Case 2 (Credential Management) | Medium | Reversible (credentials auto-expire) |
| Use Case 3 (Policy Enforcement) | High | Configuration changes require testing |
| Use Case 4 (Network Operations) | High | Network changes, test in non-production first |
| Use Case 5 (Access Control) | High | Requires break-glass access path |
Warning: Use Cases 3, 4, and 5 make configuration changes to SSH, network, and access controls. Test in a non-production environment first and ensure break-glass access is validated before applying to production systems.
The solution implements defense-in-depth with multiple policy enforcement rings.
Pattern 1: Policy-Gated Operations
Every operation passes through AAP’s OPA gateway before execution. Template name patterns map to required IdM groups, enforced at platform level with no bypass possible.
Pattern 2: Just-In-Time Credentials
No standing access. Credentials are generated on-demand from Vault, used once, and automatically revoked after TTL expiration.
Pattern 3: Workload Identity Verification
SPIFFE/SPIRE provides cryptographic proof that the automation platform is legitimate, preventing rogue scripts from impersonating AAP.
Pattern 4: Defense-in-Depth Lockdown
Four independent layers (firewall, HBAC, Vault policy, SIEM monitoring) must all be bypassed to compromise a host, with break-glass recovery tested and validated.
Operational Impact: None (read-only verification)
Business Value: Reduce integration time by 70% through automated verification of identity, secrets, policy, and infrastructure components.
What This Use Case Demonstrates:
This use case establishes the foundation by verifying that Ansible Automation Platform can successfully integrate with all Zero Trust components. All credentials are pre-configured via automation, and you verify the integrations work correctly.
verify-zta-services.yml- name: Verify All ZTA Services
hosts: zta_services
tasks:
- name: Check IdM status
ansible.builtin.command: ipactl status
register: idm_status
failed_when: "'RUNNING' not in idm_status.stdout"
- name: Check Vault health
ansible.builtin.uri:
url: "http://{{ vault_host }}:8200/v1/sys/health"
method: GET
register: vault_health
failed_when: vault_health.json.sealed | bool
- name: Check OPA policies loaded
ansible.builtin.uri:
url: "http://{{ opa_host }}:8181/v1/policies"
method: GET
register: opa_policies
failed_when: "'aap.gateway' not in opa_policies.json.result"
- name: Display verification results
ansible.builtin.debug:
msg:
- "IdM: {{ 'RUNNING' if 'RUNNING' in idm_status.stdout else 'FAILED' }}"
- "Vault: {{ 'UNSEALED' if not vault_health.json.sealed else 'SEALED' }}"
- "OPA: {{ 'LOADED' if 'aap.gateway' in opa_policies.json.result else 'MISSING' }}"
Test Vault Dynamic Credentials:
# Generate a dynamic PostgreSQL user with 5-minute TTL
vault read database/creds/ztaapp-short-lived
# Observe credentials are unique each time
# Username format: v-root-ztaapp-s-<random>
# After 5 minutes, user is automatically revoked
Test Vault SSH Signed Certificates:
# Generate ephemeral keypair
ssh-keygen -t rsa -b 2048 -f /tmp/ephemeral -N '' -q
# Sign with Vault CA
vault write -field=signed_key ssh/sign/ssh-signer \
public_key=@/tmp/ephemeral.pub valid_principals=rhel > /tmp/ephemeral-cert.pub
# Inspect certificate
ssh-keygen -L -f /tmp/ephemeral-cert.pub
# Shows: TTL, valid principals, serial number
# SSH using signed certificate (not password)
ssh -i /tmp/ephemeral -o CertificateFile=/tmp/ephemeral-cert.pub \
-p 2023 rhel@192.168.1.11
Test OPA Policy Decisions:
# Query OPA for a patching decision
curl -s http://central.zta.lab:8181/v1/data/zta/patch_access/decision \
-d '{
"input": {
"user": "ztauser",
"user_groups": ["patch-admins"],
"host": "app.zta.lab",
"action": "apply_patch"
}
}' | python3 -m json.tool
# Expected result: {"allow": true, "reason": "user in patch-admins"}
Tip: IdM LDAP Configuration
On AAP 2.6, configure IdM LDAP authentication via Access Management → Authentication Methods. Use GroupOfNamesType with
name_attr: cnfor FreeIPA group lookups. See the workshop Section 1.2 for detailed UI configuration steps including authentication mapping rules for team assignment.
Operational Impact: Medium (creates database users, deploys application)
Business Value: Eliminate standing database credentials, reducing credential compromise risk by 95% through 5-minute TTL enforcement.
What This Use Case Demonstrates:
Deploy applications using dynamic database credentials from Vault with automatic expiration. The deployment is protected by OPA policy — only users in the app-deployers group can proceed. Demonstrates deny-by-default authorization and just-in-time credential issuance.
Credential Lifecycle Timeline:
| Time | Event | Component | Result |
|---|---|---|---|
| T+0s | User launches “Deploy Application” | AAP Controller | OPA policy check initiated |
| T+1s | OPA evaluates user groups | Open Policy Agent | ✅ User in app-deployers → ALLOW |
| T+2s | Request dynamic DB credentials | HashiCorp Vault | Creates v-root-ztaapp-s-abc123 with 300s TTL |
| T+3s | Deploy application with credentials | AAP Playbook | Application configured with ephemeral creds |
| T+4s | Application starts serving traffic | App Server | ✅ Healthy, connected to database |
| T+300s | Vault TTL expires | HashiCorp Vault | ⚠️ Automatic credential revocation |
| T+301s | Application loses database access | App Server | ❌ Connection errors logged |
deploy-application.yml- name: Deploy Application with Short-Lived Credentials
hosts: localhost
tasks:
# Step 1: Query OPA for authorization
- name: Check OPA database access policy
ansible.builtin.uri:
url: "http://{{ opa_host }}:8181/v1/data/zta/db_access/decision"
method: POST
body_format: json
body:
input:
user: "{{ tower_user_name }}"
user_groups: "{{ tower_user_groups }}"
database: "ztaapp"
action: "deploy"
register: opa_decision
failed_when: not opa_decision.json.result.allow
# Step 2: Generate dynamic database credentials
- name: Create Vault database credential
community.hashi_vault.vault_read:
url: "{{ vault_addr }}"
path: database/creds/ztaapp-short-lived
register: db_creds
# Vault creates: username, password with 5-minute TTL
# Step 3: Configure network ACL for micro-segmentation
- name: Apply database access ACL
arista.eos.eos_config:
lines:
- "10 permit tcp host 10.20.0.10 host 10.30.0.10 eq 5432"
- "20 deny ip any host 10.30.0.10"
parents: ip access-list ZTA-APP-TO-DB
delegate_to: ceos2
# Step 4: Deploy application with ephemeral credentials
- name: Deploy application
ansible.builtin.template:
src: app-config.j2
dest: /opt/ztaapp/.env
vars:
db_username: "{{ db_creds.data.username }}"
db_password: "{{ db_creds.data.password }}"
delegate_to: app
notify: restart ztaapp
Wrong user attempt (neteng — not in app-deployers):
OPA Database Access Decision:
User: neteng
Groups: []
Database: ztaapp
Decision: DENIED
Reason: user 'neteng' is not authorized to request database credentials
ACCESS DENIED by OPA policy. Vault never contacted, no credentials issued.
Correct user attempt (appdev — in app-deployers):
OPA Database Access Decision:
User: appdev
Groups: ['app-deployers']
Database: ztaapp
Decision: ALLOWED
Dynamic database credentials created:
Username: v-root-ztaapp-s-abc123def
TTL: 300s
Lease: database/creds/ztaapp-short-lived/xyz
Application deployed successfully:
URL: http://app.zta.lab:8081
Health: ok
DB User: v-root-ztaapp-s-abc123def (expires in 5 minutes)
After 5 minutes, the Vault-generated database user is automatically revoked:
# Before expiry
ssh -p 2022 rhel@central.zta.lab "sudo -u postgres psql -c '\du'"
# Shows: v-root-ztaapp-s-abc123def
# After 5 minutes
ssh -p 2022 rhel@central.zta.lab "sudo -u postgres psql -c '\du'"
# User is gone — Vault revoked it
# Application loses database access
curl http://app.zta.lab:8081/health
# Returns: unhealthy (database connection lost)
ZTA Principle: Credentials are ephemeral. Without rotation, access is automatically removed. This limits the window of opportunity for compromised credentials and eliminates standing access.
Operational Impact: High (applies SSH hardening, password policy, audit rules)
Business Value: Enforce separation of duties and reduce unauthorized change risk through platform-level policy gates that cannot be bypassed.
What This Use Case Demonstrates:
AAP Policy as Code enforces authorization at the platform level — before any playbook runs. The aap.gateway OPA policy evaluates every launch request and blocks unauthorized users from even starting a job. Demonstrates separation of duties and platform-level enforcement with no bypass possible.
| Template Name Contains | Required AAP Team | IdM Group |
|---|---|---|
| “Patch” | Infrastructure or Security | team-infrastructure, team-security |
| “VLAN” or “Network” | Infrastructure | team-infrastructure |
| “Deploy”, “Application” | Applications or DevOps | team-applications, team-devops |
The apply-security-patch.yml playbook deploys:
/etc/issue and /etc/motdapply-security-patch.yml- name: Apply Security Hardening Patch
hosts: "{{ target_host }}"
become: true
tasks:
- name: Deploy security login banner
ansible.builtin.copy:
content: |
****************************************************************************
WARNING: Unauthorized access to this system is forbidden and will be
prosecuted by law. By accessing this system, you agree that your actions
may be monitored if unauthorized usage is suspected.
****************************************************************************
dest: "{{ item }}"
mode: '0644'
loop:
- /etc/issue
- /etc/motd
- name: Apply SSH hardening
ansible.builtin.lineinfile:
path: /etc/ssh/sshd_config
regexp: "{{ item.regexp }}"
line: "{{ item.line }}"
loop:
- { regexp: '^PermitRootLogin', line: 'PermitRootLogin no' }
- { regexp: '^MaxAuthTries', line: 'MaxAuthTries 3' }
- { regexp: '^PermitEmptyPasswords', line: 'PermitEmptyPasswords no' }
notify: restart sshd
- name: Deploy password policy
ansible.builtin.copy:
content: |
minlen = 12
dcredit = -1
ucredit = -1
lcredit = -1
ocredit = -1
dest: /etc/security/pwquality.conf.d/zta-policy.conf
mode: '0644'
- name: Configure audit rules for ZTA
ansible.builtin.copy:
content: |
-w /var/log/lastlog -p wa -k zta-auth
-w /var/run/faillock/ -p wa -k zta-auth
-w /etc/ssh/sshd_config -p wa -k zta-sshd
-w /etc/sudoers -p wa -k zta-sudoers
dest: /etc/audit/rules.d/zta.rules
mode: '0640'
notify: reload auditd
Scenario 1: Wrong user (neteng — not in Infrastructure or Security team)
neteng clicks “Launch” on Apply Security Patch templatev1/data/aap/gateway/decisionTemplate: "Apply Security Patch" → contains "Patch"
Required: Infrastructure or Security team
User: neteng → teams: []
Result: DENIED
Scenario 2: Correct user (ztauser — in Infrastructure team)
ztauser clicks “Launch” on Apply Security PatchUser: ztauser → teams: ['Infrastructure']
Result: ALLOWED
Security Patch Applied
Host: app.zta.lab
Patch: ZTA-SEC-2026-001
Applied:
✓ Security login banner (/etc/issue + /etc/motd)
✓ SSH hardening (no root, max 3 auth tries)
✓ Password policy (12 char min, complexity)
✓ Audit logging (auth, identity, sudoers)
# SSH to patched host — see banner immediately
ssh -p 2023 rhel@central.zta.lab
# Verify SSH hardening
sudo sshd -T | grep -E 'permitrootlogin|maxauthtries'
# Expected: permitrootlogin no, maxauthtries 3
# Verify password policy
cat /etc/security/pwquality.conf.d/zta-policy.conf
# Verify audit rules
sudo auditctl -l | grep zta
# Expected: watches on /var/log/lastlog, /etc/ssh/sshd_config, /etc/sudoers
Separation of Duties: The
appdevuser (Applications team) cannot launch patching templates. Theztauser(Infrastructure team) cannot launch application deployment templates. Each team’s access is scoped by OPA policy, enforced at the platform level.
Operational Impact: High (creates VLANs, modifies network ACLs)
Business Value: Prevent rogue automation and insider threats through cryptographic workload verification that proves the automation platform is legitimate.
What This Use Case Demonstrates:
Defense-in-depth for network automation with two OPA policy rings: the outer ring (AAP gateway) checks if the user can launch the template, and the inner ring (in-playbook) validates SPIFFE workload identity, user group, VLAN range, and action. Demonstrates workload identity verification and runtime parameter validation.
SPIFFE (Secure Production Identity Framework For Everyone) provides cryptographic identities to workloads:
central — trust rootcontrol (AAP), db, vault — issue SVIDs to local workloadszta.lab (matches IdM domain)The AAP network automation workload receives identity:
spiffe://zta.lab/workload/network-automation
configure-vlan.yml- name: Verify SPIFFE Workload Identity
hosts: automation
tasks:
- name: Fetch SVID from local SPIRE Agent
ansible.builtin.command:
cmd: /opt/spire/bin/spire-agent api fetch x509 -socketPath /run/spire/sockets/agent.sock
register: spiffe_svid
- name: Parse SPIFFE ID from SVID
ansible.builtin.set_fact:
spiffe_id: "{{ spiffe_svid.stdout | regex_search('SPIFFE ID:\\s+(.+)', '\\1') | first }}"
- name: Display workload identity
ansible.builtin.debug:
msg: "Workload verified: {{ spiffe_id }}"
failed_when: spiffe_id != "spiffe://zta.lab/workload/network-automation"
- name: OPA Policy Decision (Inner Ring)
hosts: zta_services
tasks:
- name: Query OPA network policy
ansible.builtin.uri:
url: "http://{{ opa_host }}:8181/v1/data/zta/network/decision"
method: POST
body_format: json
body:
input:
user: "{{ tower_user_name }}"
user_groups: "{{ tower_user_groups }}"
spiffe_id: "{{ hostvars['control']['spiffe_id'] }}"
action: "create_vlan"
vlan_id: "{{ new_vlan_id | int }}"
register: opa_decision
- name: Display policy decision
ansible.builtin.debug:
msg:
- "Workload verified: {{ opa_decision.json.result.conditions.workload_verified }}"
- "User authorized: {{ opa_decision.json.result.conditions.user_authorized }}"
- "Valid VLAN: {{ opa_decision.json.result.conditions.valid_vlan }}"
- "Action permitted: {{ opa_decision.json.result.conditions.action_permitted }}"
failed_when: not opa_decision.json.result.allow
- name: Create VLAN on Arista Fabric
hosts: network
tasks:
- name: Configure VLAN
arista.eos.eos_vlans:
config:
- vlan_id: "{{ new_vlan_id }}"
name: "{{ new_vlan_name }}"
state: merged
- name: Update Netbox CMDB
hosts: zta_services
tasks:
- name: Create VLAN in Netbox
netbox.netbox.netbox_vlan:
netbox_url: "{{ netbox_url }}"
netbox_token: "{{ netbox_token }}"
data:
vid: "{{ new_vlan_id }}"
name: "{{ new_vlan_name }}"
description: "Created by {{ tower_user_name }} via {{ spiffe_id }}"
state: present
Scenario 1: Wrong user (neteng — not in network-admins)
SPIFFE Workload Identity Verification
SPIFFE ID: spiffe://zta.lab/workload/network-automation
Status: VERIFIED ✓
OPA Network Policy Decision
User: neteng
Action: create_vlan
VLAN ID: 200
Result: DENIED
Conditions:
Workload verified: PASS (legitimate AAP workload)
User in network-admins: FAIL (neteng not authorized)
Valid VLAN ID: PASS (200 is in range 100-999)
Action permitted: PASS (create_vlan is allowed)
DENIED: user 'neteng' is not a member of network-admins group
The SPIFFE check passes (the platform is legitimate) but the user is not authorized. Arista switches are never touched.
Scenario 2: Correct user, invalid VLAN (netadmin with VLAN 5000)
OPA Network Policy Decision
User: netadmin
Action: create_vlan
VLAN ID: 5000
Result: DENIED
Conditions:
Workload verified: PASS
User in network-admins: PASS
Valid VLAN ID: FAIL (5000 outside range 100-999)
Action permitted: PASS
DENIED: VLAN ID 5000 is outside the permitted range (100-999)
Scenario 3: Correct user, valid VLAN (netadmin with VLAN 200)
SPIFFE Workload Identity Verification
SPIFFE ID: spiffe://zta.lab/workload/network-automation
Status: VERIFIED ✓
OPA Network Policy Decision
Result: ALLOWED
All conditions: PASS
VLAN 200 (DMZ) created on ceos1, ceos2, ceos3
VLAN Configuration Complete
VLAN 200 (DMZ)
Switches: ceos1, ceos2, ceos3 (Arista cEOS fabric)
Netbox: Created with audit trail
User: netadmin
Workload: spiffe://zta.lab/workload/network-automation
Verify on switches:
ssh -p 2001 admin@central.zta.lab "show vlan brief"
# VLAN 200 appears on all switches
Verify in Netbox:
curl -H "Authorization: Token $NETBOX_TOKEN" \
http://netbox.zta.lab:8880/api/ipam/vlans/?vid=200 | python3 -m json.tool
# Description includes: "Created by netadmin via spiffe://zta.lab/workload/network-automation"
Why verify workload identity? A compromised script running outside AAP could impersonate a network admin user. SPIFFE cryptographically proves the platform is legitimate, not just the user.
Operational Impact: High (restricts SSH access, requires break-glass testing)
Business Value: Reduce attack surface through layered access controls while maintaining operational recovery paths for incident management.
What This Use Case Demonstrates:
Apply defense-in-depth SSH lockdown with four independent layers, then recover from a misconfiguration that locks AAP out of managed hosts. Demonstrates layered security and the critical importance of tested break-glass access paths.
Featured Task:
- name: Allow SSH only from AAP controller and break-glass host
ansible.posix.firewalld:
rich_rule: "rule family='ipv4' source address='{{ item }}' service name='ssh' accept"
permanent: true
state: enabled
loop:
- 192.168.1.10 # AAP controller
- 192.168.1.11 # central (break-glass)
notify: reload firewalld
- name: Block all other SSH
ansible.posix.firewalld:
service: ssh
permanent: true
state: disabled
notify: reload firewalld
Test:
# From workstation (blocked)
ssh ztauser@app.zta.lab
# Connection refused (firewall blocks before authentication)
# From AAP controller (allowed)
ssh rhel@control.zta.lab
ssh rhel@app.zta.lab
# Connection succeeds
IdM Host-Based Access Control rules determine which users can access which hosts for which services.
HBAC Rules Created:
| Rule Name | Users | Hosts | Services |
|---|---|---|---|
allow_aap_automation |
aap-service |
All managed hosts | sshd |
allow_breakglass |
breakglass-admins group |
central only |
sshd |
Test with ipa hbactest:
kinit admin
# Test unauthorized user
ipa hbactest --user=neteng --host=app.zta.lab --service=sshd
# Access denied (no matching rule)
# Test AAP service account
ipa hbactest --user=aap-service --host=app.zta.lab --service=sshd
# Access granted (matched rule: allow_aap_automation)
# Test break-glass admin
ipa hbactest --user=ztauser --host=central.zta.lab --service=sshd
# Access granted (matched rule: allow_breakglass)
ipa hbactest --user=ztauser --host=app.zta.lab --service=sshd
# Access denied (breakglass only allowed to central)
The instructor runs a playbook that accidentally removes the AAP service account from the HBAC rule. Every AAP job now fails:
UNREACHABLE! => {"msg": "Failed to connect to the host via ssh:
Permission denied (publickey,gssapi-keyex,gssapi-with-mic)"}
AAP cannot manage any hosts. The platform is locked out.
Step 1: Access via break-glass path
ssh ztauser@central.zta.lab
# Works — ztauser is in breakglass-admins, central is allowed
Step 2: Authenticate to IdM
kinit admin
Step 3: Diagnose the HBAC problem
# Test AAP service account access
ipa hbactest --user=aap-service --host=app.zta.lab --service=sshd
# Access denied — confirms HBAC is the problem
# Check the HBAC rule
ipa hbacrule-show allow_aap_automation --all
# Notice: aap-service is missing from memberuser
Step 4: Fix the HBAC rule
ipa hbacrule-add-user allow_aap_automation --users=aap-service
# Verify fix
ipa hbactest --user=aap-service --host=app.zta.lab --service=sshd
# Access granted (matched rule: allow_aap_automation)
Step 5: Return to AAP and re-run the job
The job now succeeds. AAP can manage hosts again.
After lockdown, human operators can inspect secrets but cannot sign SSH certificates or generate dynamic credentials. Only AAP’s AppRole can perform those operations.
Human operator attempt:
export VAULT_ADDR=http://vault.zta.lab:8200
vault login -method=userpass username=admin password=ansible123!
# Try to sign SSH certificate (DENIED)
vault write ssh/sign/ssh-signer public_key=@/tmp/test-key.pub
# Error: permission denied
# Read KV secret (ALLOWED — troubleshooting is permitted)
vault kv get secret/network/arista
# Success — human-readonly policy allows reads
AAP AppRole (ALLOWED):
- name: AAP signs SSH certificate using AppRole
community.hashi_vault.vault_write:
url: "{{ vault_addr }}"
auth_method: approle
role_id: "{{ approle_role_id }}"
secret_id: "{{ approle_secret_id }}"
path: ssh/sign/ssh-signer
data:
public_key: "{{ ssh_public_key }}"
register: signed_cert
Even when SSH is successfully blocked, attempts are logged and monitored.
Splunk saved searches:
Detection query:
index=zta_syslog sourcetype=syslog sshd "Permission denied" OR "not allowed"
| stats count by src_ip, user
| where count >= 3
Test:
# Attempt SSH as unauthorized user (denied by HBAC)
ssh neteng@app.zta.lab
# Permission denied
# Check Splunk
# Activity → Triggered Alerts → "ZTA: SSH Bypass — Repeated HBAC Denials"
Defense in Depth: Even if one layer fails, others still protect. If the firewall rule is accidentally removed, HBAC still denies unauthorized users. If HBAC is misconfigured, Vault policy prevents certificate signing. If all three fail, Splunk detects the bypass and alerts.
The solution is validated through executable tests at each stage. A fully functional Zero Trust Architecture must pass all validation criteria.
Validation Philosophy: Trust but Verify
Zero Trust extends beyond user access – it applies to the automation itself. Each use case includes concrete validation commands that prove the integration works correctly. These tests can be run in CI/CD pipelines, scheduled as AAP job templates, or executed manually during implementation.
Use Case 1: ZTA Components Integration
# Verify Vault unsealed and healthy
curl -s http://vault.zta.lab:8200/v1/sys/health | python3 -m json.tool
# Expected: {"sealed": false, "initialized": true}
# Verify OPA policies loaded
curl -s http://central.zta.lab:8181/v1/policies | python3 -m json.tool | grep -E 'aap.gateway|db_access|network'
# Expected: All three policies present
# Verify IdM running
ssh rhel@central.zta.lab "sudo ipactl status"
# Expected: All services RUNNING
# Verify Netbox inventory sync
# In AAP: Inventories → ZTA Lab Inventory → Sources → NetBox CMDB → Sync
# Expected: Green status, 10+ hosts
# Verify dynamic credentials
vault read database/creds/ztaapp-short-lived
# Expected: Unique username/password, 300s TTL
# Verify SSH certificate signing
ssh-keygen -t rsa -f /tmp/test -N '' -q
vault write -field=signed_key ssh/sign/ssh-signer public_key=@/tmp/test.pub > /tmp/test-cert.pub
ssh-keygen -L -f /tmp/test-cert.pub
# Expected: Shows certificate with TTL, valid principals
Use Case 2: Just-In-Time Credential Management
# Test OPA deny-by-default (wrong user)
curl -s http://central.zta.lab:8181/v1/data/zta/db_access/decision \
-d '{"input": {"user": "neteng", "user_groups": [], "database": "ztaapp"}}' \
| python3 -m json.tool
# Expected: {"result": {"allow": false}}
# Test OPA allow (correct user)
curl -s http://central.zta.lab:8181/v1/data/zta/db_access/decision \
-d '{"input": {"user": "appdev", "user_groups": ["app-deployers"], "database": "ztaapp"}}' \
| python3 -m json.tool
# Expected: {"result": {"allow": true}}
# Verify application health
curl http://app.zta.lab:8081/health
# Expected: {"status": "healthy", "database": "connected"}
# Verify dynamic DB user exists
ssh -p 2022 rhel@central.zta.lab "sudo -u postgres psql -c '\du'" | grep v-root
# Expected: Shows v-root-ztaapp-s-<random>
# Wait 5 minutes and verify credential expiry
sleep 300
ssh -p 2022 rhel@central.zta.lab "sudo -u postgres psql -c '\du'" | grep v-root
# Expected: No results (user auto-revoked)
curl http://app.zta.lab:8081/health
# Expected: unhealthy or connection error (lost DB access)
Use Case 3: Platform-Level Policy Enforcement
# Verify AAP Policy as Code enabled
curl -sk -u "admin:ansible123!" \
"https://control.zta.lab/api/controller/v2/settings/opa/" | python3 -m json.tool
# Expected: OPA_ENABLED: true, OPA_PRE_ACTION_ENABLED: true
# Test gateway policy enforcement (manual query)
curl -s http://central.zta.lab:8181/v1/data/aap/gateway/decision \
-d '{
"input": {
"user": {"username": "neteng", "teams": []},
"template_name": "Apply Security Patch",
"action": "launch"
}
}' | python3 -m json.tool
# Expected: {"result": {"allow": false}}
# Verify patch applied
ssh -p 2023 rhel@central.zta.lab
# Expected: Security banner displayed immediately
ssh -p 2023 rhel@central.zta.lab "sudo sshd -T | grep -E 'permitrootlogin|maxauthtries'"
# Expected: permitrootlogin no, maxauthtries 3
ssh -p 2023 rhel@central.zta.lab "cat /etc/security/pwquality.conf.d/zta-policy.conf"
# Expected: minlen = 12, complexity rules present
ssh -p 2023 rhel@central.zta.lab "sudo auditctl -l | grep zta"
# Expected: Audit rules for auth, sshd_config, sudoers
Use Case 4: Workload Identity Verification for Network Operations
# Verify SPIRE Agent running
ssh rhel@control.zta.lab "sudo systemctl status spire-agent"
# Expected: active (running)
# Fetch SVID
ssh rhel@control.zta.lab "sudo /opt/spire/bin/spire-agent api fetch x509 \
-socketPath /run/spire/sockets/agent.sock"
# Expected: SPIFFE ID: spiffe://zta.lab/workload/network-automation
# Test network policy with SPIFFE verification
curl -s http://central.zta.lab:8181/v1/data/zta/network/decision \
-d '{
"input": {
"user": "netadmin",
"user_groups": ["network-admins"],
"spiffe_id": "spiffe://zta.lab/workload/network-automation",
"action": "create_vlan",
"vlan_id": 200
}
}' | python3 -m json.tool
# Expected: {"result": {"allow": true}}
# Test wrong SPIFFE ID
curl -s http://central.zta.lab:8181/v1/data/zta/network/decision \
-d '{
"input": {
"user": "netadmin",
"user_groups": ["network-admins"],
"spiffe_id": "spiffe://evil.com/rogue",
"action": "create_vlan",
"vlan_id": 200
}
}' | python3 -m json.tool
# Expected: {"result": {"allow": false}}
# Verify VLAN created on Arista
ssh -p 2001 admin@central.zta.lab "show vlan brief" | grep 200
# Expected: VLAN 200 with configured name
# Verify Netbox CMDB updated
curl -H "Authorization: Token $NETBOX_TOKEN" \
"http://netbox.zta.lab:8880/api/ipam/vlans/?vid=200" | python3 -m json.tool
# Expected: VLAN 200 with description including SPIFFE ID
Use Case 5: Defense-in-Depth Access Control
# Layer 1: Firewall test
# From workstation (blocked source)
timeout 5 ssh ztauser@app.zta.lab
# Expected: Connection refused or timeout
# From AAP controller (allowed source)
ssh rhel@control.zta.lab "ssh rhel@app.zta.lab echo success"
# Expected: success
# Layer 2: HBAC test
ssh rhel@central.zta.lab
kinit admin
ipa hbactest --user=neteng --host=app.zta.lab --service=sshd
# Expected: Access denied
ipa hbactest --user=aap-service --host=app.zta.lab --service=sshd
# Expected: Access granted
# Layer 3: Vault policy test
vault login -method=userpass username=admin password=ansible123!
vault write ssh/sign/ssh-signer public_key=@/tmp/test.pub
# Expected: Error: permission denied
vault kv get secret/network/arista
# Expected: Success (read-only allowed)
# Layer 4: SIEM bypass detection
# Attempt SSH as unauthorized user
ssh neteng@app.zta.lab
# Denied by HBAC
# Check SIEM for denial events (query example varies by platform)
# Splunk example:
curl -u admin:ansible123! -k \
'https://central.zta.lab:8089/services/search/jobs/export' \
--data-urlencode 'search=index=zta_syslog sshd "Permission denied" user=neteng | head 1' \
--data-urlencode 'output_mode=json'
# Expected: Denial event logged
# Break-glass recovery test
# Simulate lockout
ansible-playbook section6/playbooks/break-hbac.yml
# AAP job should fail
# From AAP: Launch any template
# Expected: UNREACHABLE (Permission denied)
# Recover via break-glass
ssh ztauser@central.zta.lab
kinit admin
ipa hbacrule-add-user allow_aap_automation --users=aap-service
# Verify fix
ipa hbactest --user=aap-service --host=app.zta.lab --service=sshd
# Expected: Access granted
# AAP job should succeed
# From AAP: Re-launch template
# Expected: Success