Files
ansible-testing/methodologies/ansible/playbooks/examples/cisco_firewall.yml
T

269 lines
14 KiB
YAML

---
# ══════════════════════════════════════════════════════════════════════════════
# IEC 62443-3-3 SL2 — Cisco ASA Firewall Compliance
#
# Target type : Cisco ASA 9.x+ (physical or virtual)
# Connection : SSH via ansible.netcommon.network_cli
# Collections : cisco.asa, ansible.netcommon (installed in ansible-node image)
# Python pkg : paramiko (installed in ansible-node image)
#
# Inventory group : cisco_firewalls (see assets.yml)
#
# Run:
# ansible-playbook -i assets.yml methodologies/ansible/playbooks/examples/cisco_firewall.yml
#
# ASA-specific notes:
# - asa_command returns stdout as a list, same as ios_command.
# - 'show running-config' on ASA is a single large string; use regex_search
# and regex_findall to extract specific configuration lines.
# - 'enable' privilege is required for most 'show' commands.
# ansible_become=yes + ansible_become_method=enable handles this.
# - Multi-context ASAs: add 'changeto context <name>' as a command prefix,
# or target individual context admin contexts.
# ══════════════════════════════════════════════════════════════════════════════
- name: "IEC 62443-3-3 SL2 — Cisco ASA Firewall Compliance"
hosts: cisco_firewalls
gather_facts: yes # runs cisco.asa.asa_facts → ansible_net_*
vars:
report_dir: "../../reports"
pre_tasks:
- name: "Gather local facts for report timestamp and user"
ansible.builtin.setup:
gather_subset:
- date_time
- user_id
delegate_to: localhost
run_once: true
- name: "Ensure report directory exists"
ansible.builtin.file:
path: "{{ report_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
run_once: true
tasks:
# ── FR5 · SR 5.3: No Telnet on VTY lines — SSH only ──────────────────────
- block:
- name: "Gather: VTY line transport configuration"
cisco.asa.asa_command:
commands:
- show running-config | include telnet|ssh
register: _mgmt_access
- name: "Evaluate: FW-RDF-01 — Telnet management access disabled"
ansible.builtin.set_fact:
test_results: "{{ test_results | default([]) + [{
'test_id': 'FW-RDF-01',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.3 — Communication Constraints',
'description': 'Telnet management access to the ASA shall be disabled',
'passed': (
_mgmt_access.stdout[0] | regex_search('telnet [0-9]') is none
),
'expected': 'No telnet <network> lines in running-config',
'actual': _mgmt_access.stdout[0] | regex_findall('telnet[^\n]+') | join(' | ') | default('No telnet statements found', true),
'severity': 'critical',
'remediation': 'Remove all "telnet" management statements; use "ssh" only'
}] }}"
- name: "Evaluate: FW-RDF-02 — SSH management access configured"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'FW-RDF-02',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.3 — Communication Constraints',
'description': 'SSH management access shall be restricted to specific management networks',
'passed': (_mgmt_access.stdout[0] | regex_search('ssh [0-9]') is not none),
'expected': 'At least one ssh <network> statement present',
'actual': _mgmt_access.stdout[0] | regex_findall('ssh[^\n]+') | join(' | ') | default('No SSH access statements', true),
'severity': 'high',
'remediation': 'ssh <management-net> <mask> <interface>\nssh version 2'
}] }}"
ignore_errors: yes
# ── FR1 · SR 1.2: IKEv1 disabled — IKEv2 only for VPN ────────────────────
# IKEv1 is vulnerable to several known attacks. IEC 62443 SL2 requires v2.
- block:
- name: "Gather: IKE/ISAKMP policy configuration"
cisco.asa.asa_command:
commands:
- show running-config | include crypto isakmp|crypto ikev
register: _ike_cfg
- name: "Evaluate: FW-IAC-01 — IKEv1 disabled"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'FW-IAC-01',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.2 — Software Process and Device Identification',
'description': 'IKEv1 (crypto isakmp) shall be disabled; only IKEv2 is permitted',
'passed': (
_ike_cfg.stdout[0] | regex_search('crypto isakmp enable') is none and
_ike_cfg.stdout[0] | regex_search('crypto isakmp policy') is none
),
'expected': 'No crypto isakmp enable or isakmp policy statements',
'actual': _ike_cfg.stdout[0] | regex_findall('crypto isakmp[^\n]+') | join(' | ') | default('No IKEv1 config found', true),
'severity': 'high',
'remediation': 'no crypto isakmp enable\nno crypto isakmp policy <n>'
}] }}"
ignore_errors: yes
# ── FR2 · SR 2.8: Syslog forwarding to remote server ─────────────────────
- block:
- name: "Gather: Syslog configuration"
cisco.asa.asa_command:
commands:
- show running-config | include logging
register: _syslog_cfg
- name: "Evaluate: FW-UC-01 — Syslog forwarding to remote host"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'FW-UC-01',
'category': 'FR2 — Use Control',
'requirement': 'SR 2.8 — Auditable Events',
'description': 'ASA syslog shall be forwarded to a remote syslog server (not stored locally only)',
'passed': (
_syslog_cfg.stdout[0] | regex_search('logging host') is not none and
_syslog_cfg.stdout[0] | regex_search('logging enable') is not none
),
'expected': 'logging enable; logging host <interface> <syslog-server>',
'actual': _syslog_cfg.stdout[0] | regex_findall('logging[^\n]+') | join(' | ') | default('No logging config', true),
'severity': 'high',
'remediation': 'logging enable\nlogging host <inside/mgmt> <syslog-server-ip>'
}] }}"
ignore_errors: yes
# ── FR1 · SR 1.11: AAA authentication for management ─────────────────────
# Require AAA (TACACS+/RADIUS) for management access; reject local fallback
# without documented justification.
- block:
- name: "Gather: AAA and local user configuration"
cisco.asa.asa_command:
commands:
- show running-config | include ^aaa|^username
register: _aaa_cfg
- name: "Evaluate: FW-IAC-02 — AAA authentication configured for SSH/enable"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'FW-IAC-02',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.11 — Unsuccessful Login Attempts',
'description': 'Management access (SSH and enable) shall use AAA authentication',
'passed': (
_aaa_cfg.stdout[0] | regex_search('aaa authentication ssh') is not none or
_aaa_cfg.stdout[0] | regex_search('aaa authentication enable') is not none
),
'expected': 'aaa authentication ssh|enable console <server-group> LOCAL',
'actual': _aaa_cfg.stdout[0] | regex_findall('aaa authentication[^\n]+') | join(' | ') | default('No AAA authentication config', true),
'severity': 'high',
'remediation': 'aaa authentication ssh console TACACS+ LOCAL\naaa authentication enable console TACACS+ LOCAL'
}] }}"
ignore_errors: yes
# ── FR5 · SR 5.2: Default deny — check access-group on interfaces ─────────
- block:
- name: "Gather: Interface ACL bindings and ACL counts"
cisco.asa.asa_command:
commands:
- show running-config | include access-group
- show access-list | include elements
register: _acl_cfg
- name: "Evaluate: FW-RDF-03 — Access lists applied inbound on all interfaces"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'FW-RDF-03',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.2 — Zone Boundary Protection',
'description': 'Access control lists shall be applied inbound on all zone-facing interfaces',
'passed': (_acl_cfg.stdout[0] | regex_findall('access-group.*in interface') | length > 0),
'expected': 'At least one access-group <name> in interface <name>',
'actual': _acl_cfg.stdout[0] | regex_findall('access-group[^\n]+') | join(' | ') | default('No access-group statements', true),
'severity': 'critical',
'remediation': 'access-group <ACL_NAME> in interface <outside|ics_zone>'
}] }}"
ignore_errors: yes
# ── HITL · FR5 · SR 5.2: Firewall rule review ────────────────────────────
# Collect the full ACL and ask the reviewer if rules are minimal / correct.
- block:
- name: "Gather: [HITL] Full access-list detail for review"
cisco.asa.asa_command:
commands:
- show access-list
register: _full_acl
- name: "Display: [HITL] FW-RDF-HITL-01 — ACL rule review"
ansible.builtin.debug:
msg: |
══════════════════════════════════════════════════════════════
MANUAL REVIEW REQUIRED · FW-RDF-HITL-01 · {{ inventory_hostname }}
══════════════════════════════════════════════════════════════
Requirement : SR 5.2 — Zone Boundary Protection
Check : Firewall rules implement least-privilege; no
'permit any any' or broad permit rules exist
Access list summary
───────────────────
{{ _full_acl.stdout[0] | truncate(1200, false) | indent(1) }}
Verify:
- No 'permit ip any any' or 'permit any any' rules present
- Rules are specific (source/destination/service all named)
- Each rule has a documented business justification
- Implicit deny at the end of each list
══════════════════════════════════════════════════════════════
- name: "Prompt: FW-RDF-HITL-01 — verdict for {{ inventory_hostname }}"
ansible.builtin.pause:
prompt: |
Do the firewall ACLs implement least-privilege with no broad permit rules?
Enter verdict [pass / fail / skip]:
register: _hitl_acl_verdict
delegate_to: localhost
- name: "Prompt: FW-RDF-HITL-01 — notes on failure"
ansible.builtin.pause:
prompt: "Describe the offending rule(s) (e.g. 'ACL OUTSIDE line 3: permit ip any any'):"
register: _hitl_acl_notes
delegate_to: localhost
when: _hitl_acl_verdict.user_input | lower | trim in ['fail', 'f']
- name: "Evaluate: FW-RDF-HITL-01"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'FW-RDF-HITL-01',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.2 — Zone Boundary Protection',
'description': 'Firewall rules shall implement least-privilege; no broad permit rules',
'passed': (
'skipped' if (_hitl_acl_verdict.user_input | lower | trim in ['skip', 's', ''])
else (_hitl_acl_verdict.user_input | lower | trim in ['pass', 'p'])
),
'expected': 'All permit rules are specific (src/dst/svc); no permit any any',
'actual': 'Full ACL captured — see report evidence',
'severity': 'critical',
'remediation': 'Replace broad permit rules with specific source/destination/service entries',
'reviewer': ansible_user_id,
'notes': (_hitl_acl_notes.user_input | trim) if _hitl_acl_notes is defined else ''
}] }}"
ignore_errors: yes
# ── Generate report ────────────────────────────────────────────────────────
- name: "Generate compliance report"
ansible.builtin.include_tasks: ../library/report.yml