269 lines
14 KiB
YAML
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
|