208 lines
10 KiB
YAML
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
|