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

208 lines
10 KiB
YAML

---
# ══════════════════════════════════════════════════════════════════════════════
# IEC 62443-3-3 SL2 — Linux VM / Server Compliance
#
# Target type : Linux (any distro with systemd + SSH)
# Connection : SSH — native Ansible, no extra collection required
# Privilege : become: yes (sudo) for /etc/shadow, audit rules, sysctl
#
# Inventory group : linux_vms (see assets.yml)
#
# Run:
# ansible-playbook -i assets.yml methodologies/ansible/playbooks/examples/linux_vm.yml -K
# ansible-playbook -i assets.yml methodologies/ansible/playbooks/examples/linux_vm.yml \
# --limit linux-vm-01.example.com -K
#
# What this covers (beyond the core fr1/fr2/fr5 suites):
# FR1: SSH daemon hardening (PermitRootLogin, PasswordAuthentication)
# FR2: Sudo command logging (Defaults log_input/log_output)
# FR3: Kernel integrity — /proc/sys hardening via sysctl
# FR5: IPv4 forwarding disabled (host is not a router)
# Kernel module loading restricted (kmod_blacklist)
# ══════════════════════════════════════════════════════════════════════════════
- name: "IEC 62443-3-3 SL2 — Linux VM Compliance"
hosts: linux_vms
gather_facts: yes
become: yes
vars:
report_dir: "../../reports"
pre_tasks:
- name: "Ensure report directory exists"
ansible.builtin.file:
path: "{{ report_dir }}"
state: directory
mode: "0755"
delegate_to: localhost
run_once: true
tasks:
# ── FR1 · SR 1.1: SSH root login disabled ─────────────────────────────────
- block:
- name: "Gather: SSH PermitRootLogin setting"
ansible.builtin.shell: |
sshd -T 2>/dev/null | grep -i '^permitrootlogin' | awk '{print $2}'
register: _sshd_rootlogin
changed_when: false
- name: "Evaluate: LX-IAC-01 — SSH root login disabled"
ansible.builtin.set_fact:
test_results: "{{ test_results | default([]) + [{
'test_id': 'LX-IAC-01',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.1 — Unique User Identification',
'description': 'SSH shall not permit direct root login',
'passed': (_sshd_rootlogin.stdout | trim | lower in ['no', 'prohibit-password', 'forced-commands-only']),
'expected': 'PermitRootLogin no (or prohibit-password / forced-commands-only)',
'actual': 'PermitRootLogin ' + (_sshd_rootlogin.stdout | trim | default('NOT SET', true)),
'severity': 'high',
'remediation': 'Set PermitRootLogin no in /etc/ssh/sshd_config and restart sshd'
}] }}"
ignore_errors: yes
# ── FR1 · SR 1.4: SSH password authentication disabled ────────────────────
- block:
- name: "Gather: SSH PasswordAuthentication setting"
ansible.builtin.shell: |
sshd -T 2>/dev/null | grep -i '^passwordauthentication' | awk '{print $2}'
register: _sshd_pwauth
changed_when: false
- name: "Evaluate: LX-IAC-02 — SSH key-only authentication"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'LX-IAC-02',
'category': 'FR1 — Identification and Authentication Control',
'requirement': 'SR 1.4 — Identifier Strength',
'description': 'SSH shall use key-based authentication only (PasswordAuthentication no)',
'passed': (_sshd_pwauth.stdout | trim | lower == 'no'),
'expected': 'PasswordAuthentication no',
'actual': 'PasswordAuthentication ' + (_sshd_pwauth.stdout | trim | default('NOT SET', true)),
'severity': 'high',
'remediation': 'Set PasswordAuthentication no in /etc/ssh/sshd_config and restart sshd'
}] }}"
ignore_errors: yes
# ── FR2 · SR 2.1: Sudo command logging ────────────────────────────────────
- block:
- name: "Gather: Sudo log_input / log_output Defaults"
ansible.builtin.shell: |
grep -rh 'Defaults.*log_' /etc/sudoers /etc/sudoers.d/ 2>/dev/null |
grep -v '^\s*#' | tr -s ' ' | head -5
register: _sudo_log
changed_when: false
- name: "Evaluate: LX-UC-01 — Sudo session logging enabled"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'LX-UC-01',
'category': 'FR2 — Use Control',
'requirement': 'SR 2.4 — Audit Log Integrity',
'description': 'Sudo shall log all input and output (Defaults log_input, log_output)',
'passed': (
'log_input' in _sudo_log.stdout and
'log_output' in _sudo_log.stdout
),
'expected': 'Defaults log_input, log_output in /etc/sudoers[.d]',
'actual': _sudo_log.stdout | trim | default('No sudo logging Defaults found', true),
'severity': 'medium',
'remediation': 'Add "Defaults log_input,log_output" to /etc/sudoers'
}] }}"
ignore_errors: yes
# ── FR3 · SR 3.1: Kernel IP forwarding disabled ────────────────────────────
- block:
- name: "Gather: IPv4 forwarding sysctl"
ansible.builtin.command:
cmd: sysctl net.ipv4.ip_forward
register: _ip_forward
changed_when: false
- name: "Evaluate: LX-SI-01 — IP forwarding disabled"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'LX-SI-01',
'category': 'FR3 — System Integrity',
'requirement': 'SR 3.1 — Communication Integrity',
'description': 'Kernel IP forwarding shall be disabled (host is not a router)',
'passed': (_ip_forward.stdout | trim | regex_search('= 0$') is not none),
'expected': 'net.ipv4.ip_forward = 0',
'actual': _ip_forward.stdout | trim,
'severity': 'high',
'remediation': 'Add "net.ipv4.ip_forward = 0" to /etc/sysctl.d/99-ics-hardening.conf and run sysctl -p'
}] }}"
ignore_errors: yes
# ── FR3 · SR 3.1: ICMP redirect acceptance disabled ──────────────────────
- block:
- name: "Gather: ICMP accept_redirects (all interfaces)"
ansible.builtin.shell: |
sysctl net.ipv4.conf.all.accept_redirects net.ipv4.conf.default.accept_redirects 2>/dev/null
register: _redirects
changed_when: false
- name: "Evaluate: LX-SI-02 — ICMP redirects rejected"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'LX-SI-02',
'category': 'FR3 — System Integrity',
'requirement': 'SR 3.1 — Communication Integrity',
'description': 'ICMP redirect acceptance shall be disabled on all interfaces',
'passed': (
_redirects.stdout | regex_findall('= ([01])') | unique | list == ['0']
),
'expected': 'net.ipv4.conf.*.accept_redirects = 0',
'actual': _redirects.stdout | trim,
'severity': 'medium',
'remediation': 'Set net.ipv4.conf.all.accept_redirects = 0 in /etc/sysctl.d/99-ics-hardening.conf'
}] }}"
ignore_errors: yes
# ── FR5 · SR 5.3: Core dump disabled ──────────────────────────────────────
# Core dumps can expose sensitive memory content; disable on ICS nodes.
- block:
- name: "Gather: Core dump hard limit (ulimit -c)"
ansible.builtin.shell: |
grep -rh '^[^#]*hard.*core' /etc/security/limits.conf /etc/security/limits.d/ 2>/dev/null |
awk '{print $NF}' | head -1 || echo "NOT SET"
register: _core_limit
changed_when: false
- name: "Gather: systemd DefaultLimitCORE"
ansible.builtin.shell: |
grep -h 'DefaultLimitCORE' /etc/systemd/system.conf /etc/systemd/user.conf 2>/dev/null |
tail -1 | awk -F= '{print $2}' || echo "NOT SET"
register: _systemd_core
changed_when: false
- name: "Evaluate: LX-RDF-01 — Core dumps disabled"
ansible.builtin.set_fact:
test_results: "{{ test_results + [{
'test_id': 'LX-RDF-01',
'category': 'FR5 — Restricted Data Flow',
'requirement': 'SR 5.3 — Communication Constraints',
'description': 'Core dumps shall be disabled to prevent memory disclosure',
'passed': (
(_core_limit.stdout | trim in ['0', '']) or
(_systemd_core.stdout | trim == '0')
),
'expected': 'hard core 0 in limits.conf OR DefaultLimitCORE=0 in systemd',
'actual': 'limits.conf: ' + _core_limit.stdout | trim + ' | systemd: ' + _systemd_core.stdout | trim,
'severity': 'medium',
'remediation': 'Add "* hard core 0" to /etc/security/limits.d/99-no-core.conf'
}] }}"
ignore_errors: yes
# ── Generate report ────────────────────────────────────────────────────────
- name: "Generate compliance report"
ansible.builtin.include_tasks: ../library/report.yml