initial commit

This commit is contained in:
Riedel
2026-08-19 09:51:43 +02:00
commit d777c1e975
79 changed files with 2263 additions and 0 deletions

View File

@@ -0,0 +1,59 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [aide_integrity, always]
- name: Install AIDE
ansible.builtin.package:
name: aide
state: present
tags: [aide_integrity]
- name: Deploy custom AIDE configuration (additional watched paths)
ansible.builtin.template:
src: aide-custom.conf.j2
dest: "{{ aide_integrity_config_dest }}"
owner: root
group: root
mode: "0640"
tags: [aide_integrity]
- name: Check whether an AIDE database already exists
ansible.builtin.stat:
path: "{{ aide_integrity_db_path }}"
register: aide_integrity_db_stat
tags: [aide_integrity]
- name: Initialize AIDE database (first run only)
ansible.builtin.command: aide --init
when: not aide_integrity_db_stat.stat.exists
changed_when: true
tags: [aide_integrity]
- name: Activate the initial AIDE database
ansible.builtin.command: "mv {{ aide_integrity_db_new_path }} {{ aide_integrity_db_path }}"
when: not aide_integrity_db_stat.stat.exists
changed_when: true
tags: [aide_integrity]
- name: Schedule regular AIDE check via cron
ansible.builtin.cron:
name: "Daily AIDE integrity check"
hour: "{{ aide_check_cron_hour }}"
minute: "{{ aide_check_cron_minute }}"
job: >-
{{ aide_integrity_check_cmd }} | /usr/bin/mail -s
"AIDE integrity report {{ inventory_hostname }}" {{ aide_notify_email }}
user: root
tags: [aide_integrity]
- name: Note on updating the database after legitimate changes
ansible.builtin.debug:
msg: >-
After intended system changes (patches, config changes made outside
Ansible), update the AIDE database manually:
aide --update && mv {{ aide_integrity_db_new_path }} {{ aide_integrity_db_path }}
tags: [aide_integrity]

View File

@@ -0,0 +1,12 @@
# {{ ansible_managed }}
/etc/ssh/sshd_config FIPSR
/etc/sudoers.d FIPSR
/etc/security/pwquality.conf FIPSR
/etc/security/faillock.conf FIPSR
/etc/audit/rules.d FIPSR
/etc/firewalld FIPSR
!/var/log
!/var/cache
!/tmp
!/var/tmp

View File

@@ -0,0 +1,5 @@
---
aide_integrity_config_dest: /etc/aide/aide.conf.d/99-custom.conf
aide_integrity_db_path: /var/lib/aide/aide.db
aide_integrity_db_new_path: /var/lib/aide/aide.db.new
aide_integrity_check_cmd: /usr/bin/aide.wrapper --check

View File

@@ -0,0 +1,5 @@
---
aide_integrity_config_dest: /etc/aide.conf.d/99-custom.conf
aide_integrity_db_path: /var/lib/aide/aide.db.gz
aide_integrity_db_new_path: /var/lib/aide/aide.db.new.gz
aide_integrity_check_cmd: /usr/sbin/aide --check

View File

@@ -0,0 +1,4 @@
---
apparmor_config_complain_profiles: []
# apparmor_config_complain_profiles:
# - /usr/sbin/nginx

View File

@@ -0,0 +1,42 @@
---
- name: Install AppArmor packages
ansible.builtin.package:
name:
- apparmor
- apparmor-utils
- apparmor-profiles
state: present
tags: [apparmor_config]
- name: Enable and start AppArmor
ansible.builtin.systemd:
name: apparmor
enabled: true
state: started
tags: [apparmor_config]
- name: Set enforce mode for all loaded profiles
ansible.builtin.shell: aa-enforce /etc/apparmor.d/*
register: apparmor_config_enforce_result
changed_when: "'Setting' in apparmor_config_enforce_result.stdout"
failed_when: false
tags: [apparmor_config]
- name: Set application-specific profiles to complain mode (exceptions)
ansible.builtin.command: "aa-complain {{ item }}"
loop: "{{ apparmor_config_complain_profiles }}"
when: apparmor_config_complain_profiles | length > 0
changed_when: true
tags: [apparmor_config]
- name: Query AppArmor status
ansible.builtin.command: aa-status --verbose
register: apparmor_config_status
changed_when: false
failed_when: false
tags: [apparmor_config]
- name: Report AppArmor status
ansible.builtin.debug:
msg: "AppArmor status on {{ inventory_hostname }}: {{ apparmor_config_status.stdout_lines[0] | default('unavailable') }}"
tags: [apparmor_config]

View File

@@ -0,0 +1,6 @@
---
# veeam_agent_repo_url: "https://repo.example.corp/veeam/veeam.repo"
# veeam_agent_apt_uri: "https://repo.example.corp/veeam/deb"
# veeam_agent_apt_suite: "stable"
# veeam_agent_apt_components: "main"
# veeam_agent_apt_signed_by: "https://repo.example.corp/veeam/gpg"

View File

@@ -0,0 +1,48 @@
---
- name: Report that backup_agent is disabled for this host
ansible.builtin.debug:
msg: "veeam_agent_enabled is false - skipping backup_agent role for {{ inventory_hostname }}."
when: not veeam_agent_enabled | bool
tags: [backup_agent]
- name: Configure Veeam repository (RedHat family, customer-specific URL required)
ansible.builtin.get_url:
url: "{{ veeam_agent_repo_url }}"
dest: /etc/yum.repos.d/veeam.repo
owner: root
group: root
mode: "0644"
when:
- veeam_agent_enabled | bool
- ansible_facts['os_family'] == "RedHat"
- veeam_agent_repo_url is defined
tags: [backup_agent]
- name: Configure Veeam repository (Debian family, customer-specific values required)
ansible.builtin.deb822_repository:
name: veeam
types: deb
uris: "{{ veeam_agent_apt_uri }}"
suites: "{{ veeam_agent_apt_suite }}"
components: "{{ veeam_agent_apt_components }}"
signed_by: "{{ veeam_agent_apt_signed_by | default(omit) }}"
when:
- veeam_agent_enabled | bool
- ansible_facts['os_family'] == "Debian"
- veeam_agent_apt_uri is defined
tags: [backup_agent]
- name: Install Veeam agent
ansible.builtin.package:
name: veeam
state: present
when: veeam_agent_enabled | bool
tags: [backup_agent]
- name: Enable and start Veeam agent service
ansible.builtin.systemd:
name: veeamservice
enabled: true
state: started
when: veeam_agent_enabled | bool
tags: [backup_agent]

View File

@@ -0,0 +1,5 @@
---
- name: restart sshd (banners)
ansible.builtin.systemd:
name: sshd
state: restarted

View File

@@ -0,0 +1,21 @@
---
- name: Deploy local console login banner
ansible.builtin.template:
src: issue.j2
dest: /etc/issue
owner: root
group: root
mode: "0644"
when: banner_enabled | bool
tags: [banners]
- name: Deploy network (pre-auth SSH) login banner
ansible.builtin.template:
src: issue.net.j2
dest: /etc/issue.net
owner: root
group: root
mode: "0644"
when: banner_enabled | bool
notify: restart sshd (banners)
tags: [banners]

View File

@@ -0,0 +1,5 @@
******************************************************************
AUTHORIZED ACCESS ONLY.
All activity on this system is logged and monitored.
Unauthorized access is prohibited and may be prosecuted.
******************************************************************

View File

@@ -0,0 +1,5 @@
******************************************************************
AUTHORIZED ACCESS ONLY.
All activity on this system is logged and monitored.
Unauthorized access is prohibited and may be prosecuted.
******************************************************************

View File

@@ -0,0 +1,5 @@
---
- name: restart chrony
ansible.builtin.systemd:
name: "{{ base_os_chrony_service_name }}"
state: restarted

View File

@@ -0,0 +1,75 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [base_os, always]
- name: Update apt cache
ansible.builtin.apt:
update_cache: true
cache_valid_time: 3600
when: ansible_facts['os_family'] == "Debian"
tags: [base_os]
- name: Install base packages
ansible.builtin.package:
name: "{{ base_os_packages + (base_extra_packages | default([])) }}"
state: present
tags: [base_os]
- name: Set timezone
community.general.timezone:
name: "{{ timezone }}"
tags: [base_os]
- name: Deploy chrony configuration
ansible.builtin.template:
src: chrony.conf.j2
dest: "{{ base_os_chrony_config_path }}"
owner: root
group: root
mode: "0644"
notify: restart chrony
tags: [base_os]
- name: Enable and start chrony
ansible.builtin.systemd:
name: "{{ base_os_chrony_service_name }}"
enabled: true
state: started
tags: [base_os]
- name: Set hostname from inventory
ansible.builtin.hostname:
name: "{{ inventory_hostname }}"
tags: [base_os]
- name: Remove unwanted packages
ansible.builtin.package:
name: "{{ base_os_unwanted_packages }}"
state: absent
tags: [base_os, cis_hardening]
- name: Gather service facts
ansible.builtin.service_facts:
tags: [base_os]
- name: Disable unneeded services if installed
ansible.builtin.systemd:
name: "{{ item }}"
enabled: false
state: stopped
loop: "{{ base_unneeded_services }}"
when: (item + '.service') in ansible_facts.services
tags: [base_os]
- name: Deploy post-login MOTD
ansible.builtin.template:
src: motd.j2
dest: /etc/motd
owner: root
group: root
mode: "0644"
tags: [base_os]

View File

@@ -0,0 +1,9 @@
# {{ ansible_managed }}
{% for server in ntp_servers %}
server {{ server }} iburst
{% endfor %}
driftfile /var/lib/chrony/drift
makestep 1.0 3
rtcsync
logdir /var/log/chrony

View File

@@ -0,0 +1,6 @@
******************************************************************
AUTHORIZED ACCESS ONLY.
All activity on this system is logged and monitored.
Unauthorized access is prohibited and may be prosecuted.
Managed via Ansible ({{ inventory_hostname }}).
******************************************************************

View File

@@ -0,0 +1,17 @@
---
base_os_packages:
- vim
- chrony
- auditd
- firewalld
- rsyslog
- sudo
base_os_unwanted_packages:
- sendmail
- telnet
- rsh-client
- rsh-redone-client
- nis
- tftp
base_os_chrony_config_path: /etc/chrony/chrony.conf
base_os_chrony_service_name: chrony

View File

@@ -0,0 +1,17 @@
---
base_os_packages:
- vim-enhanced
- chrony
- audit
- policycoreutils-python-utils
- firewalld
- rsyslog
- sudo
base_os_unwanted_packages:
- sendmail
- telnet
- rsh
- ypbind
- tftp
base_os_chrony_config_path: /etc/chrony.conf
base_os_chrony_service_name: chronyd

View File

@@ -0,0 +1,20 @@
---
- name: reload auditd rules
ansible.builtin.command: augenrules --load
changed_when: true
- name: restart auditd
ansible.builtin.systemd:
name: auditd
state: restarted
- name: restart systemd-journald
ansible.builtin.systemd:
name: systemd-journald
state: restarted
- name: regenerate grub config
ansible.builtin.command: >-
{{ cis_hardening_grub_mkconfig_cmd }}
{{ ('-o ' + (cis_hardening_grub_cfg_path_uefi if grub_bootloader_is_uefi | default(false) else cis_hardening_grub_cfg_path_bios)) if (cis_hardening_grub_cfg_path_bios | default('')) | length > 0 else '' }}
changed_when: true

View File

@@ -0,0 +1,255 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [cis_hardening, always]
- name: Disable unneeded filesystem kernel modules
ansible.builtin.copy:
dest: /etc/modprobe.d/cis-disable-filesystems.conf
owner: root
group: root
mode: "0644"
content: |
# {{ ansible_managed }}
{% for fs in cis_disable_filesystems %}
install {{ fs }} /bin/true
{% endfor %}
tags: [cis_hardening]
- name: Set kernel/network sysctl hardening parameters
ansible.posix.sysctl:
name: "{{ item.name }}"
value: "{{ item.value }}"
sysctl_set: true
state: present
reload: true
loop:
- { name: "net.ipv4.ip_forward", value: "0" }
- { name: "net.ipv4.conf.all.send_redirects", value: "0" }
- { name: "net.ipv4.conf.all.accept_redirects", value: "0" }
- { name: "net.ipv4.conf.all.accept_source_route", value: "0" }
- { name: "net.ipv4.conf.all.log_martians", value: "1" }
- { name: "net.ipv4.icmp_echo_ignore_broadcasts", value: "1" }
- { name: "kernel.randomize_va_space", value: "2" }
- { name: "fs.suid_dumpable", value: "0" }
tags: [cis_hardening]
- name: Enable strict reverse-path filtering (CIS Level 2)
ansible.posix.sysctl:
name: "net.ipv4.conf.all.rp_filter"
value: "1"
sysctl_set: true
state: present
reload: true
when: cis_level | int >= 2
tags: [cis_hardening]
- name: Harden /tmp mount options (noexec,nosuid,nodev)
ansible.posix.mount:
path: /tmp
src: tmpfs
fstype: tmpfs
opts: "defaults,rw,nosuid,nodev,noexec,relatime"
state: mounted
tags: [cis_hardening]
- name: Bind and harden /var/tmp (CIS Level 2)
ansible.posix.mount:
path: /var/tmp
src: /tmp
fstype: none
opts: "bind,nosuid,nodev,noexec"
state: mounted
when: cis_level | int >= 2
tags: [cis_hardening]
- name: Disable additional kernel modules (CIS Level 2)
ansible.builtin.copy:
dest: /etc/modprobe.d/cis-disable-level2-modules.conf
owner: root
group: root
mode: "0644"
content: |
# {{ ansible_managed }}
{% for mod in cis_disable_modules_level2 %}
install {{ mod }} /bin/true
{% endfor %}
when: cis_level | int >= 2
tags: [cis_hardening]
- name: Disable core dumps
ansible.builtin.copy:
dest: /etc/security/limits.d/99-cis-disable-coredumps.conf
owner: root
group: root
mode: "0644"
content: |
# {{ ansible_managed }}
* hard core 0
tags: [cis_hardening]
- name: Enforce default umask
ansible.builtin.copy:
dest: /etc/profile.d/99-cis-umask.sh
owner: root
group: root
mode: "0644"
content: |
# {{ ansible_managed }}
umask {{ cis_umask }}
tags: [cis_hardening]
- name: Set shell auto-logout timeout (TMOUT)
ansible.builtin.copy:
dest: /etc/profile.d/99-cis-tmout.sh
owner: root
group: root
mode: "0644"
content: |
# {{ ansible_managed }}
TMOUT={{ cis_shell_tmout_seconds }}
readonly TMOUT
export TMOUT
tags: [cis_hardening]
- name: Mask Ctrl-Alt-Del reboot target
ansible.builtin.systemd:
name: ctrl-alt-del.target
masked: true
when: cis_disable_ctrl_alt_del | bool
tags: [cis_hardening]
- name: Find world-writable directories missing the sticky bit
ansible.builtin.shell: >
set -o pipefail;
df --local -P | awk '{if (NR!=1) print $6}' |
xargs -I '{}' find '{}' -xdev -type d \( -perm -0002 -a ! -perm -1000 \) 2>/dev/null
register: cis_hardening_sticky_bit_dirs
changed_when: false
when: cis_sticky_bit_enforce | bool
tags: [cis_hardening]
- name: Apply sticky bit to world-writable directories
ansible.builtin.file:
path: "{{ item }}"
mode: "a+t"
loop: "{{ cis_hardening_sticky_bit_dirs.stdout_lines | default([]) }}"
when: cis_sticky_bit_enforce | bool
tags: [cis_hardening]
- name: Configure journald persistent storage and size cap
ansible.builtin.lineinfile:
path: /etc/systemd/journald.conf
regexp: "^#?{{ item.key }}="
line: "{{ item.key }}={{ item.value }}"
loop:
- { key: "Storage", value: "persistent" }
- { key: "SystemMaxUse", value: "{{ cis_journald_max_use }}" }
when: cis_journald_persistent | bool
notify: restart systemd-journald
tags: [cis_hardening]
- name: Schedule weekly report of unowned/ungrouped files
ansible.builtin.cron:
name: "Weekly unowned files report"
weekday: "0"
hour: "{{ cis_unowned_files_report_cron_hour }}"
minute: "0"
job: >-
/usr/bin/find / -xdev \( -nouser -o -nogroup \) 2>/dev/null |
/usr/bin/mail -s "Unowned files report {{ inventory_hostname }}" root
user: root
when: cis_unowned_files_report_enabled | bool
tags: [cis_hardening]
- name: Harden /boot mount options in fstab (requires manual review and reboot)
ansible.builtin.replace:
path: /etc/fstab
regexp: '^(\S+\s+/boot\s+\S+\s+)(?!.*nodev)(\S+)(\s+.*)$'
replace: '\1\2,nodev,nosuid\3'
when: cis_boot_hardening_enabled | bool
tags: [cis_hardening]
- name: Deploy baseline auditd rules (logins, time changes, account changes)
ansible.builtin.template:
src: audit-baseline.rules.j2
dest: /etc/audit/rules.d/10-baseline.rules
owner: root
group: root
mode: "0640"
notify: reload auditd rules
tags: [cis_hardening]
- name: Deploy extended auditd rules (CIS Level 2)
ansible.builtin.template:
src: audit-level2.rules.j2
dest: /etc/audit/rules.d/20-level2.rules
owner: root
group: root
mode: "0640"
when: cis_level | int >= 2
notify: reload auditd rules
tags: [cis_hardening]
- name: Configure auditd log rotation and disk-space handling
ansible.builtin.lineinfile:
path: /etc/audit/auditd.conf
regexp: "^{{ item.key }}\\s*="
line: "{{ item.key }} = {{ item.value }}"
loop:
- { key: "max_log_file", value: "50" }
- { key: "max_log_file_action", value: "rotate" }
- { key: "num_logs", value: "10" }
- { key: "space_left", value: "10%" }
- { key: "space_left_action", value: "email" }
- { key: "admin_space_left", value: "5%" }
- { key: "admin_space_left_action", value: "halt" }
notify: restart auditd
tags: [cis_hardening]
- name: Set login.defs password aging policy
ansible.builtin.lineinfile:
path: /etc/login.defs
regexp: "^{{ item.key }}"
line: "{{ item.key }}\t{{ item.value }}"
loop:
- { key: "PASS_MAX_DAYS", value: "90" }
- { key: "PASS_MIN_DAYS", value: "1" }
- { key: "PASS_WARN_AGE", value: "7" }
tags: [cis_hardening]
- name: Restrict cron/at to authorized users
ansible.builtin.file:
path: "{{ item }}"
state: touch
owner: root
group: root
mode: "0600"
loop:
- /etc/cron.allow
- /etc/at.allow
tags: [cis_hardening]
- name: Configure GRUB bootloader password (pre-generated PBKDF2 hash)
ansible.builtin.blockinfile:
path: /etc/grub.d/40_custom
marker: "# {mark} ANSIBLE MANAGED BLOCK"
insertafter: "EOF"
block: |
set superusers="{{ grub_bootloader_username }}"
password_pbkdf2 {{ grub_bootloader_username }} {{ grub_bootloader_password_hash }}
when:
- cis_grub_password_enabled | bool
- grub_bootloader_password_hash | length > 0
notify: regenerate grub config
tags: [cis_hardening]
- name: Enable and start auditd
ansible.builtin.systemd:
name: auditd
enabled: true
state: started
tags: [cis_hardening]

View File

@@ -0,0 +1,19 @@
# {{ ansible_managed }}
-D
-b 8192
-w /var/log/lastlog -p wa -k logins
-w /var/run/faillock -p wa -k logins
-a always,exit -F arch=b64 -S adjtimex,settimeofday,clock_settime -k time-change
-w /etc/localtime -p wa -k time-change
-w /etc/passwd -p wa -k identity
-w /etc/group -p wa -k identity
-w /etc/shadow -p wa -k identity
-w /etc/sudoers -p wa -k identity
-w /etc/sudoers.d/ -p wa -k identity
-w /home/{{ breakglass_username }} -p rwxa -k breakglass-usage
-e 2

View File

@@ -0,0 +1,9 @@
# {{ ansible_managed }}
-a always,exit -F arch=b64 -S open,truncate,ftruncate,creat,openat -F exit=-EACCES -F auid>=1000 -F auid!=unset -k access-denied
-a always,exit -F arch=b64 -S open,truncate,ftruncate,creat,openat -F exit=-EPERM -F auid>=1000 -F auid!=unset -k access-denied
-a always,exit -F arch=b64 -S mount -F auid>=1000 -F auid!=unset -k mounts
-w /sbin/insmod -p x -k modules
-w /sbin/rmmod -p x -k modules
-w /sbin/modprobe -p x -k modules

View File

@@ -0,0 +1,4 @@
---
cis_hardening_grub_mkconfig_cmd: "update-grub"
cis_hardening_grub_cfg_path_bios: ""
cis_hardening_grub_cfg_path_uefi: ""

View File

@@ -0,0 +1,4 @@
---
cis_hardening_grub_mkconfig_cmd: "grub2-mkconfig"
cis_hardening_grub_cfg_path_bios: "/boot/grub2/grub.cfg"
cis_hardening_grub_cfg_path_uefi: "/boot/efi/EFI/rocky/grub.cfg"

View File

@@ -0,0 +1,5 @@
---
- name: restart fail2ban
ansible.builtin.systemd:
name: fail2ban
state: restarted

View File

@@ -0,0 +1,37 @@
---
- name: Gather package facts
ansible.builtin.package_facts:
manager: auto
when: ansible_facts['os_family'] == "RedHat"
tags: [fail2ban]
- name: Fail with guidance if EPEL is missing on RedHat family
ansible.builtin.fail:
msg: "fail2ban requires EPEL on RedHat family. Set epel_enabled: true (repo_management role) or provide fail2ban via custom_yum_repos."
when:
- ansible_facts['os_family'] == "RedHat"
- "'epel-release' not in ansible_facts.packages"
tags: [fail2ban]
- name: Install fail2ban
ansible.builtin.package:
name: fail2ban
state: present
tags: [fail2ban]
- name: Deploy fail2ban local jail configuration
ansible.builtin.template:
src: jail.local.j2
dest: /etc/fail2ban/jail.local
owner: root
group: root
mode: "0644"
notify: restart fail2ban
tags: [fail2ban]
- name: Enable and start fail2ban
ansible.builtin.systemd:
name: fail2ban
enabled: true
state: started
tags: [fail2ban]

View File

@@ -0,0 +1,11 @@
# {{ ansible_managed }}
[DEFAULT]
bantime = {{ fail2ban_bantime }}
findtime = {{ fail2ban_findtime }}
maxretry = {{ fail2ban_maxretry }}
ignoreip = {{ fail2ban_ignoreip }}
backend = systemd
[sshd]
enabled = true
port = {{ ssh_port }}

View File

@@ -0,0 +1,9 @@
---
# The sensor can only be downloaded via an authenticated CrowdStrike API/
# console session, so exactly one of these must be provided per environment:
# falcon_onboarding_sensor_package_url - internal hosted download URL
# falcon_onboarding_sensor_package_src - local path on the control node
falcon_onboarding_sensor_package_url: ""
falcon_onboarding_sensor_package_src: ""
falcon_onboarding_provisioning_token: ""
falcon_onboarding_cloud_region: "eu-1"

View File

@@ -0,0 +1,5 @@
---
- name: restart falcon-sensor
ansible.builtin.systemd:
name: falcon-sensor
state: restarted

View File

@@ -0,0 +1,100 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [falcon_onboarding, always]
- name: Verify a sensor package source is defined
ansible.builtin.assert:
that:
- falcon_onboarding_sensor_package_url | length > 0 or falcon_onboarding_sensor_package_src | length > 0
fail_msg: >-
Neither falcon_onboarding_sensor_package_url nor
falcon_onboarding_sensor_package_src is set. Provide the sensor
package matching this host's OS (see roles/falcon_onboarding/defaults/main.yml).
tags: [falcon_onboarding]
- name: Download sensor package from internal URL
ansible.builtin.get_url:
url: "{{ falcon_onboarding_sensor_package_url }}"
dest: "/tmp/falcon-sensor.{{ falcon_onboarding_package_ext }}"
owner: root
group: root
mode: "0644"
when: falcon_onboarding_sensor_package_url | length > 0
tags: [falcon_onboarding]
- name: Copy sensor package from the Ansible control node
ansible.builtin.copy:
src: "{{ falcon_onboarding_sensor_package_src }}"
dest: "/tmp/falcon-sensor.{{ falcon_onboarding_package_ext }}"
owner: root
group: root
mode: "0644"
when: falcon_onboarding_sensor_package_url | length == 0 and falcon_onboarding_sensor_package_src | length > 0
tags: [falcon_onboarding]
- name: Install Falcon sensor (RedHat family)
ansible.builtin.dnf:
name: "/tmp/falcon-sensor.rpm"
state: present
disable_gpg_check: false
when: ansible_facts['os_family'] == "RedHat"
register: falcon_onboarding_install_result
tags: [falcon_onboarding]
- name: Install Falcon sensor (Debian family)
ansible.builtin.apt:
deb: "/tmp/falcon-sensor.deb"
when: ansible_facts['os_family'] == "Debian"
register: falcon_onboarding_install_result
tags: [falcon_onboarding]
- name: Remove downloaded/copied sensor package
ansible.builtin.file:
path: "/tmp/falcon-sensor.{{ falcon_onboarding_package_ext }}"
state: absent
tags: [falcon_onboarding]
- name: Configure Customer ID (CID)
ansible.builtin.command: "/opt/CrowdStrike/falconctl -s --cid={{ falcon_cid }}"
changed_when: true
no_log: true
notify: restart falcon-sensor
tags: [falcon_onboarding]
- name: Configure provisioning token if required
ansible.builtin.command: "/opt/CrowdStrike/falconctl -s --provisioning-token={{ falcon_onboarding_provisioning_token }}"
changed_when: true
no_log: true
when: falcon_onboarding_provisioning_token | length > 0
notify: restart falcon-sensor
tags: [falcon_onboarding]
- name: Enable and start the Falcon sensor service
ansible.builtin.systemd:
name: falcon-sensor
enabled: true
state: started
tags: [falcon_onboarding]
- name: Check reduced-functionality mode (RFM)
ansible.builtin.command: /opt/CrowdStrike/falconctl -g --rfm-state
register: falcon_onboarding_rfm_state
changed_when: false
failed_when: false
tags: [falcon_onboarding]
- name: Query agent ID (AID) for verification
ansible.builtin.command: /opt/CrowdStrike/falconctl -g --aid
register: falcon_onboarding_aid
changed_when: false
failed_when: false
tags: [falcon_onboarding]
- name: Report Falcon onboarding status
ansible.builtin.debug:
msg: "CrowdStrike Falcon on {{ inventory_hostname }} - {{ falcon_onboarding_rfm_state.stdout }} | {{ falcon_onboarding_aid.stdout }}"
tags: [falcon_onboarding]

View File

@@ -0,0 +1,2 @@
---
falcon_onboarding_package_ext: deb

View File

@@ -0,0 +1,2 @@
---
falcon_onboarding_package_ext: rpm

View File

@@ -0,0 +1,56 @@
---
- name: Install firewalld
ansible.builtin.package:
name: firewalld
state: present
tags: [firewall_config]
- name: Enable and start firewalld
ansible.builtin.systemd:
name: firewalld
enabled: true
state: started
tags: [firewall_config]
- name: Enable the restrictive default zone
ansible.posix.firewalld:
zone: "{{ firewall_default_zone }}"
state: enabled
permanent: true
immediate: true
tags: [firewall_config]
- name: Set the restrictive zone as system default
ansible.builtin.command: "firewall-cmd --set-default-zone={{ firewall_default_zone }}"
changed_when: true
tags: [firewall_config]
- name: Enable logging of denied packets
ansible.builtin.command: "firewall-cmd --set-log-denied={{ firewall_log_denied }}"
changed_when: true
tags: [firewall_config]
- name: Allow SSH only from the administration subnet
ansible.posix.firewalld:
zone: "{{ firewall_default_zone }}"
rich_rule: >-
rule family="ipv4" source address="{{ firewall_admin_subnet }}"
port protocol="tcp" port="{{ ssh_port | default(22) }}" accept
permanent: true
immediate: true
state: enabled
tags: [firewall_config]
- name: Allow defined application services/ports
ansible.posix.firewalld:
zone: "{{ firewall_default_zone }}"
rich_rule: >-
rule family="ipv4"
{{ 'source address="' + item.source + '"' if item.source is defined and item.source != 'any' else '' }}
service name="{{ item.service }}" accept
permanent: true
immediate: true
state: enabled
loop: "{{ firewall_allowed_services }}"
when: firewall_allowed_services | length > 0
tags: [firewall_config]

View File

@@ -0,0 +1,5 @@
---
- name: restart sssd
ansible.builtin.systemd:
name: sssd
state: restarted

View File

@@ -0,0 +1,89 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [identity_ad, always]
- name: Install AD integration packages
ansible.builtin.package:
name: "{{ identity_ad_packages }}"
state: present
tags: [identity_ad]
- name: Check whether host is already domain-joined
ansible.builtin.command: realm list
register: identity_ad_realm_status
changed_when: false
failed_when: false
tags: [identity_ad]
- name: Join Active Directory domain
ansible.builtin.command: >
realm join --user={{ ad_join_username }}
--computer-ou="{{ ad_ou }}"
{{ ad_domain }}
args:
stdin: "{{ ad_join_password }}"
when: ad_domain not in identity_ad_realm_status.stdout
changed_when: true
no_log: true
tags: [identity_ad]
- name: Deploy sssd.conf
ansible.builtin.template:
src: sssd.conf.j2
dest: /etc/sssd/sssd.conf
owner: root
group: root
mode: "0600"
notify: restart sssd
tags: [identity_ad]
- name: Enable oddjobd (required for pam_mkhomedir, RedHat family)
ansible.builtin.systemd:
name: oddjobd
enabled: true
state: started
when: ansible_facts['os_family'] == "RedHat"
tags: [identity_ad]
- name: Enable automatic home directory creation (RedHat family)
ansible.builtin.command: authselect enable-feature with-mkhomedir
register: identity_ad_authselect_result
changed_when: "'already enabled' not in identity_ad_authselect_result.stderr"
failed_when: false
when: ansible_facts['os_family'] == "RedHat"
tags: [identity_ad]
- name: Check whether pam_mkhomedir is already enabled (Debian family)
ansible.builtin.command: grep -q pam_mkhomedir.so /etc/pam.d/common-session
register: identity_ad_mkhomedir_check
changed_when: false
failed_when: false
when: ansible_facts['os_family'] == "Debian"
tags: [identity_ad]
- name: Enable automatic home directory creation (Debian family)
ansible.builtin.command: pam-auth-update --enable mkhomedir
when:
- ansible_facts['os_family'] == "Debian"
- identity_ad_mkhomedir_check.rc != 0
changed_when: true
tags: [identity_ad]
- name: Enable and start sssd
ansible.builtin.systemd:
name: sssd
enabled: true
state: started
tags: [identity_ad]
- name: Restrict login access to authorized AD groups
ansible.builtin.lineinfile:
path: /etc/sssd/sssd.conf
regexp: "^access_provider"
line: "access_provider = simple"
notify: restart sssd
tags: [identity_ad]

View File

@@ -0,0 +1,23 @@
# {{ ansible_managed }}
[sssd]
services = nss, pam
domains = {{ ad_domain }}
[domain/{{ ad_domain }}]
id_provider = ad
access_provider = simple
simple_allow_groups = {{ ad_admin_group }}, {{ ad_operator_group }}
auth_provider = ad
chpass_provider = ad
ad_domain = {{ ad_domain }}
krb5_realm = {{ ad_domain_realm }}
use_fully_qualified_names = {{ sssd_use_fully_qualified_names | lower }}
fallback_homedir = /home/%d/%u
default_shell = /bin/bash
cache_credentials = true
enumerate = false
ldap_id_mapping = true

View File

@@ -0,0 +1,10 @@
---
identity_ad_packages:
- realmd
- sssd
- sssd-tools
- adcli
- samba-common-bin
- krb5-user
- libnss-sss
- libpam-sss

View File

@@ -0,0 +1,10 @@
---
identity_ad_packages:
- realmd
- sssd
- sssd-tools
- oddjob
- oddjob-mkhomedir
- adcli
- samba-common-tools
- krb5-workstation

View File

@@ -0,0 +1,5 @@
---
- name: restart sshd (local_accounts)
ansible.builtin.systemd:
name: sshd
state: restarted

View File

@@ -0,0 +1,67 @@
---
- name: Create break-glass group
ansible.builtin.group:
name: "{{ breakglass_username }}"
state: present
tags: [local_accounts]
- name: Create break-glass account
ansible.builtin.user:
name: "{{ breakglass_username }}"
comment: "{{ breakglass_comment }}"
group: "{{ breakglass_username }}"
shell: /bin/bash
create_home: true
home: "/home/{{ breakglass_username }}"
password: "{{ breakglass_password_hash | default('!', true) }}"
update_password: on_create
state: present
tags: [local_accounts]
- name: Lock down break-glass home directory
ansible.builtin.file:
path: "/home/{{ breakglass_username }}"
owner: "{{ breakglass_username }}"
group: "{{ breakglass_username }}"
mode: "0700"
tags: [local_accounts]
- name: Deploy unrestricted sudo drop-in for break-glass account
ansible.builtin.copy:
dest: "/etc/sudoers.d/00-{{ breakglass_username }}"
owner: root
group: root
mode: "0440"
content: "{{ breakglass_username }} ALL=(ALL) ALL\n"
validate: "visudo -cf %s"
tags: [local_accounts]
- name: Block SSH access for the break-glass account
ansible.builtin.blockinfile:
path: /etc/ssh/sshd_config.d/00-breakglass-deny.conf
create: true
owner: root
group: root
mode: "0600"
marker: "# {mark} ANSIBLE MANAGED BLOCK"
block: |
Match User {{ breakglass_username }}
PasswordAuthentication no
PubkeyAuthentication no
validate: "/usr/sbin/sshd -t -f %s"
notify: restart sshd (local_accounts)
tags: [local_accounts]
- name: Create optional additional local service accounts
ansible.builtin.user:
name: "{{ item.name }}"
comment: "{{ item.comment | default('Local service account, managed via Ansible') }}"
system: "{{ item.system | default(true) }}"
shell: "{{ item.shell | default('/sbin/nologin') }}"
create_home: "{{ item.create_home | default(false) }}"
state: present
loop: "{{ local_service_accounts }}"
loop_control:
label: "{{ item.name }}"
when: local_service_accounts | length > 0
tags: [local_accounts]

View File

@@ -0,0 +1,6 @@
Place the customer's internal CA root/intermediate certificate here
(filename set via syslog_ca_cert_src in group_vars, default: siem-ca.crt),
unless the SIEM/syslog-collector certificate is already signed by a
publicly trusted CA already present in the system trust store.
Only relevant when syslog_ca_cert_enabled: true.

View File

@@ -0,0 +1,9 @@
---
- name: restart rsyslog
ansible.builtin.systemd:
name: rsyslog
state: restarted
- name: update CA trust
ansible.builtin.command: "{{ logging_rsyslog_ca_trust_update_cmd }}"
changed_when: true

View File

@@ -0,0 +1,60 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [logging_rsyslog, always]
- name: Install rsyslog with TLS support
ansible.builtin.package:
name:
- rsyslog
- rsyslog-gnutls
state: present
when: syslog_use_tls | bool
tags: [logging_rsyslog]
- name: Install rsyslog without TLS support
ansible.builtin.package:
name: rsyslog
state: present
when: not syslog_use_tls | bool
tags: [logging_rsyslog]
- name: Distribute internal CA certificate for SIEM TLS connection
ansible.builtin.copy:
src: "{{ syslog_ca_cert_src }}"
dest: "{{ logging_rsyslog_ca_cert_dest }}"
owner: root
group: root
mode: "0644"
when: syslog_ca_cert_enabled | bool
notify: update CA trust
tags: [logging_rsyslog]
- name: Deploy forwarding configuration
ansible.builtin.template:
src: 60-forward-siem.conf.j2
dest: /etc/rsyslog.d/60-forward-siem.conf
owner: root
group: root
mode: "0644"
notify: restart rsyslog
tags: [logging_rsyslog]
- name: Deploy logrotate configuration
ansible.builtin.template:
src: logrotate-cis.j2
dest: /etc/logrotate.d/rsyslog-cis
owner: root
group: root
mode: "0644"
tags: [logging_rsyslog]
- name: Enable and start rsyslog
ansible.builtin.systemd:
name: rsyslog
enabled: true
state: started
tags: [logging_rsyslog]

View File

@@ -0,0 +1,24 @@
# {{ ansible_managed }}
{% if syslog_use_tls %}
$DefaultNetstreamDriver gtls
$DefaultNetstreamDriverCAFile /etc/pki/tls/certs/ca-bundle.crt
$ActionSendStreamDriverMode 1
$ActionSendStreamDriverAuthMode x509/name
$ActionSendStreamDriverPermittedPeer {{ syslog_collector_host }}
$ActionQueueType LinkedList
$ActionQueueFileName siem_fwd_queue
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionResumeRetryCount -1
auth,authpriv.*;kern.*;cron.* @@(o){{ syslog_collector_host }}:{{ syslog_collector_port }}
{% else %}
$ActionQueueType LinkedList
$ActionQueueFileName siem_fwd_queue
$ActionQueueMaxDiskSpace 1g
$ActionQueueSaveOnShutdown on
$ActionResumeRetryCount -1
auth,authpriv.*;kern.*;cron.* @{{ syslog_collector_host }}:{{ syslog_collector_port }}
{% endif %}

View File

@@ -0,0 +1,14 @@
# {{ ansible_managed }}
/var/log/messages /var/log/secure /var/log/maillog /var/log/cron /var/log/spooler {
weekly
rotate 12
compress
delaycompress
missingok
notifempty
create 0640 root root
sharedscripts
postrotate
/usr/bin/systemctl kill -s HUP rsyslog.service >/dev/null 2>&1 || true
endscript
}

View File

@@ -0,0 +1,3 @@
---
logging_rsyslog_ca_cert_dest: /usr/local/share/ca-certificates/siem-ca.crt
logging_rsyslog_ca_trust_update_cmd: update-ca-certificates

View File

@@ -0,0 +1,3 @@
---
logging_rsyslog_ca_cert_dest: /etc/pki/ca-trust/source/anchors/siem-ca.crt
logging_rsyslog_ca_trust_update_cmd: update-ca-trust extract

View File

@@ -0,0 +1,63 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [pam_hardening, always]
- name: Ensure pwquality library is installed
ansible.builtin.package:
name: "{{ pam_hardening_pwquality_package }}"
state: present
tags: [pam_hardening]
- name: Set password complexity requirements
ansible.builtin.lineinfile:
path: /etc/security/pwquality.conf
regexp: "^#?\\s*{{ item.key }}\\s*="
line: "{{ item.key }} = {{ item.value }}"
loop:
- { key: "minlen", value: "{{ pam_pwquality_minlen }}" }
- { key: "dcredit", value: "{{ pam_pwquality_dcredit }}" }
- { key: "ucredit", value: "{{ pam_pwquality_ucredit }}" }
- { key: "lcredit", value: "{{ pam_pwquality_lcredit }}" }
- { key: "ocredit", value: "{{ pam_pwquality_ocredit }}" }
- { key: "retry", value: "{{ pam_pwquality_retry }}" }
tags: [pam_hardening]
- name: Enable pam_faillock via authselect (RedHat family)
ansible.builtin.command: authselect enable-feature with-faillock
register: pam_hardening_faillock_enable
changed_when: "'already enabled' not in pam_hardening_faillock_enable.stderr"
failed_when: false
when: ansible_facts['os_family'] == "RedHat"
tags: [pam_hardening]
- name: Check whether pam_faillock is already enabled (Debian family)
ansible.builtin.command: grep -q pam_faillock.so /etc/pam.d/common-auth
register: pam_hardening_faillock_check
changed_when: false
failed_when: false
when: ansible_facts['os_family'] == "Debian"
tags: [pam_hardening]
- name: Enable pam_faillock via pam-auth-update (Debian family)
ansible.builtin.command: pam-auth-update --enable faillock
register: pam_hardening_faillock_debian
changed_when: true
failed_when: false
when:
- ansible_facts['os_family'] == "Debian"
- pam_hardening_faillock_check.rc != 0
tags: [pam_hardening]
- name: Set account lockout thresholds
ansible.builtin.lineinfile:
path: /etc/security/faillock.conf
regexp: "^#?\\s*{{ item.key }}\\s*="
line: "{{ item.key }} = {{ item.value }}"
loop:
- { key: "deny", value: "{{ pam_faillock_deny }}" }
- { key: "unlock_time", value: "{{ pam_faillock_unlock_time }}" }
tags: [pam_hardening]

View File

@@ -0,0 +1,2 @@
---
pam_hardening_pwquality_package: libpam-pwquality

View File

@@ -0,0 +1,2 @@
---
pam_hardening_pwquality_package: libpwquality

View File

@@ -0,0 +1,90 @@
---
- name: Include OS-family-specific variables
ansible.builtin.include_vars: "{{ item }}"
with_first_found:
- "{{ ansible_facts['os_family'] }}.yml"
- "default.yml"
tags: [patch_mgmt, always]
- name: Install patch management packages
ansible.builtin.package:
name: "{{ patch_mgmt_packages }}"
state: present
tags: [patch_mgmt]
- name: Deploy dnf-automatic configuration (RedHat family)
ansible.builtin.template:
src: automatic.conf.j2
dest: /etc/dnf/automatic.conf
owner: root
group: root
mode: "0644"
when: ansible_facts['os_family'] == "RedHat"
tags: [patch_mgmt]
- name: Enable dnf-automatic download timer (RedHat family)
ansible.builtin.systemd:
name: dnf-automatic.timer
enabled: true
state: started
when: ansible_facts['os_family'] == "RedHat"
tags: [patch_mgmt]
- name: Deploy unattended-upgrades allowed-origins configuration (Debian family)
ansible.builtin.template:
src: 50unattended-upgrades.j2
dest: /etc/apt/apt.conf.d/50unattended-upgrades
owner: root
group: root
mode: "0644"
when: ansible_facts['os_family'] == "Debian"
tags: [patch_mgmt]
- name: Deploy apt periodic update/upgrade configuration (Debian family)
ansible.builtin.template:
src: 20auto-upgrades.j2
dest: /etc/apt/apt.conf.d/20auto-upgrades
owner: root
group: root
mode: "0644"
when: ansible_facts['os_family'] == "Debian"
tags: [patch_mgmt]
- name: Enable apt-daily timers (Debian family)
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
loop:
- apt-daily.timer
- apt-daily-upgrade.timer
when: ansible_facts['os_family'] == "Debian"
tags: [patch_mgmt]
- name: Deploy reboot-required check script
ansible.builtin.template:
src: check-reboot-needed.sh.j2
dest: /usr/local/sbin/check-reboot-needed.sh
owner: root
group: root
mode: "0750"
tags: [patch_mgmt]
- name: Schedule daily reboot-required check
ansible.builtin.cron:
name: "Check for pending reboot after patching"
special_time: daily
job: "/usr/local/sbin/check-reboot-needed.sh"
user: root
tags: [patch_mgmt]
- name: Schedule automatic reboot in maintenance window (non-critical systems only)
ansible.builtin.cron:
name: "Automatic reboot in maintenance window"
weekday: "{{ patch_reboot_day }}"
hour: "{{ patch_reboot_time.split(':')[0] }}"
minute: "{{ patch_reboot_time.split(':')[1] }}"
job: "/usr/local/sbin/check-reboot-needed.sh --auto-reboot"
user: root
when: inventory_hostname in patch_reboot_window_hosts
tags: [patch_mgmt]

View File

@@ -0,0 +1,5 @@
// {{ ansible_managed }}
APT::Periodic::Update-Package-Lists "1";
APT::Periodic::Download-Upgradeable-Packages "1";
APT::Periodic::Unattended-Upgrade "{{ '1' if dnf_automatic_apply_security else '0' }}";
APT::Periodic::AutocleanInterval "7";

View File

@@ -0,0 +1,11 @@
// {{ ansible_managed }}
Unattended-Upgrade::Allowed-Origins {
"${distro_id}:${distro_codename}-security";
{% if dnf_automatic_apply_other %}
"${distro_id}:${distro_codename}-updates";
{% endif %}
};
Unattended-Upgrade::Remove-Unused-Dependencies "true";
Unattended-Upgrade::Automatic-Reboot "false";
Unattended-Upgrade::Mail "root";
Unattended-Upgrade::MailReport "on-change";

View File

@@ -0,0 +1,17 @@
# {{ ansible_managed }}
[commands]
upgrade_type = {{ 'security' if not dnf_automatic_apply_other else 'default' }}
random_sleep = 360
download_updates = yes
apply_updates = {{ 'yes' if dnf_automatic_apply_security else 'no' }}
[emitters]
emit_via = stdio
[email]
email_from = dnf-automatic@{{ ansible_domain | default('example.corp') }}
email_to = root
email_host = localhost
[base]
debuglevel = 1

View File

@@ -0,0 +1,25 @@
#!/usr/bin/env bash
# {{ ansible_managed }}
set -euo pipefail
{% if ansible_facts['os_family'] == "RedHat" %}
reboot_required() {
! /usr/bin/needs-restarting -r >/dev/null 2>&1
}
{% else %}
reboot_required() {
test -f /var/run/reboot-required
}
{% endif %}
if reboot_required; then
logger -t check-reboot-needed "Reboot required on {{ inventory_hostname }} after security update."
echo "Reboot required on {{ inventory_hostname }}." | mail -s "Reboot required: {{ inventory_hostname }}" root || true
if [[ "${1:-}" == "--auto-reboot" ]]; then
logger -t check-reboot-needed "Performing automatic reboot (scheduled maintenance window)."
/usr/sbin/shutdown -r +1 "Automatic reboot after security update (maintenance window)"
fi
else
logger -t check-reboot-needed "No reboot required on {{ inventory_hostname }}."
fi

View File

@@ -0,0 +1,4 @@
---
patch_mgmt_packages:
- unattended-upgrades
- apt-listchanges

View File

@@ -0,0 +1,4 @@
---
patch_mgmt_packages:
- dnf-automatic
- dnf-utils

View File

@@ -0,0 +1,122 @@
---
- name: Ensure python3-debian is installed (required by deb822_repository)
ansible.builtin.package:
name: python3-debian
state: present
when: ansible_facts['os_family'] == "Debian"
tags: [repo_management]
- name: Configure internal/custom yum repositories (RedHat family)
ansible.builtin.yum_repository:
name: "{{ item.name }}"
description: "{{ item.description | default(item.name) }}"
baseurl: "{{ item.baseurl }}"
gpgcheck: "{{ item.gpgcheck | default(true) }}"
gpgkey: "{{ item.gpgkey | default(omit) }}"
enabled: "{{ item.enabled | default(true) }}"
loop: "{{ custom_yum_repos }}"
loop_control:
label: "{{ item.name }}"
when: ansible_facts['os_family'] == "RedHat" and custom_yum_repos | length > 0
tags: [repo_management]
- name: Configure internal/custom apt repositories (Debian family)
ansible.builtin.deb822_repository:
name: "{{ item.name }}"
types: deb
uris: "{{ item.uris }}"
suites: "{{ item.suites }}"
components: "{{ item.components | default(omit) }}"
signed_by: "{{ item.signed_by | default(omit) }}"
loop: "{{ custom_apt_repos }}"
loop_control:
label: "{{ item.name }}"
when: ansible_facts['os_family'] == "Debian" and custom_apt_repos | length > 0
tags: [repo_management]
- name: Probe internet connectivity
ansible.builtin.uri:
url: "{{ repo_mgmt_internet_check_url }}"
method: HEAD
timeout: 5
status_code: [200, 301, 302, 403]
register: repo_management_internet_probe
failed_when: false
when: repo_mgmt_check_internet | bool
tags: [repo_management]
- name: Set internet availability fact
ansible.builtin.set_fact:
repo_management_internet_available: "{{ (repo_management_internet_probe is defined) and (repo_management_internet_probe.status is defined) and (repo_management_internet_probe.status | default(0) in [200, 301, 302, 403]) }}"
tags: [repo_management]
- name: Report internet availability
ansible.builtin.debug:
msg: "Internet access on {{ inventory_hostname }}: {{ 'available' if repo_management_internet_available else 'not available / check disabled' }}"
tags: [repo_management]
- name: Enable CRB repository (RedHat family, internet access required)
community.general.dnf_config_manager:
name: crb
state: enabled
when:
- ansible_facts['os_family'] == "RedHat"
- repo_management_internet_available | default(false)
- crb_enabled | bool
tags: [repo_management]
- name: Install EPEL release package (RedHat family, internet access required)
ansible.builtin.dnf:
name: "https://dl.fedoraproject.org/pub/epel/epel-release-latest-{{ epel_major_version }}.noarch.rpm"
state: present
disable_gpg_check: false
when:
- ansible_facts['os_family'] == "RedHat"
- repo_management_internet_available | default(false)
- epel_enabled | bool
tags: [repo_management]
- name: Warn if EPEL/CRB requested but no internet access
ansible.builtin.debug:
msg: "EPEL/CRB requested but no internet access detected on {{ inventory_hostname }}. Add an internal mirror via custom_yum_repos instead."
when:
- ansible_facts['os_family'] == "RedHat"
- not (repo_management_internet_available | default(false))
- (epel_enabled | bool) or (crb_enabled | bool)
tags: [repo_management]
- name: Enable universe/multiverse components (Ubuntu, internet access required)
ansible.builtin.deb822_repository:
name: ubuntu-universe-multiverse
types: deb
uris: "http://archive.ubuntu.com/ubuntu"
suites:
- "{{ ansible_facts['distribution_release'] }}"
- "{{ ansible_facts['distribution_release'] }}-updates"
components:
- universe
- multiverse
when:
- ansible_facts['distribution'] == "Ubuntu"
- repo_management_internet_available | default(false)
- ubuntu_universe_enabled | bool
tags: [repo_management]
- name: Enable backports component (Debian, internet access required)
ansible.builtin.deb822_repository:
name: debian-backports
types: deb
uris: "http://deb.debian.org/debian"
suites: "{{ ansible_facts['distribution_release'] }}-backports"
components: main
when:
- ansible_facts['distribution'] == "Debian"
- repo_management_internet_available | default(false)
- debian_backports_enabled | bool
tags: [repo_management]
- name: Refresh apt cache after repository changes (Debian family)
ansible.builtin.apt:
update_cache: true
when: ansible_facts['os_family'] == "Debian"
tags: [repo_management]

View File

@@ -0,0 +1,8 @@
---
selinux_config_booleans: []
# selinux_config_booleans:
# - { name: "httpd_can_network_connect", state: true }
selinux_config_fcontexts: []
# selinux_config_fcontexts:
# - { target: "/opt/myapp/data(/.*)?", setype: "httpd_sys_rw_content_t" }

View File

@@ -0,0 +1,2 @@
---
# No handlers needed - restorecon is applied directly in tasks/main.yml

View File

@@ -0,0 +1,58 @@
---
- name: Verify this role only runs on RedHat-family hosts
ansible.builtin.assert:
that:
- ansible_facts['os_family'] == "RedHat"
fail_msg: "selinux_config only supports RedHat-family hosts. Use apparmor_config for Debian family."
tags: [selinux_config, always]
- name: Install SELinux management tools
ansible.builtin.dnf:
name:
- policycoreutils-python-utils
- setroubleshoot-server
- checkpolicy
state: present
tags: [selinux_config]
- name: Set SELinux mode and policy
ansible.posix.selinux:
state: "{{ selinux_state }}"
policy: "{{ selinux_policy }}"
tags: [selinux_config]
- name: Set application-specific SELinux booleans
ansible.posix.seboolean:
name: "{{ item.name }}"
state: "{{ item.state }}"
persistent: true
loop: "{{ selinux_config_booleans }}"
when: selinux_config_booleans | length > 0
tags: [selinux_config]
- name: Set application-specific file contexts
community.general.sefcontext:
target: "{{ item.target }}"
setype: "{{ item.setype }}"
state: present
loop: "{{ selinux_config_fcontexts }}"
register: selinux_config_fcontext_result
tags: [selinux_config]
- name: Apply restorecon to changed file context paths
ansible.builtin.command: "restorecon -Rv {{ item.item.target | regex_replace('\\(/\\.\\*\\)\\?$', '') }}"
loop: "{{ selinux_config_fcontext_result.results }}"
when: selinux_config_fcontext_result.changed and item.changed
changed_when: true
tags: [selinux_config]
- name: Query current SELinux status
ansible.builtin.command: getenforce
register: selinux_config_current
changed_when: false
tags: [selinux_config]
- name: Report SELinux status
ansible.builtin.debug:
msg: "SELinux status on {{ inventory_hostname }}: {{ selinux_config_current.stdout }}"
tags: [selinux_config]

View File

@@ -0,0 +1,5 @@
---
- name: restart sshd
ansible.builtin.systemd:
name: sshd
state: restarted

View File

@@ -0,0 +1,27 @@
---
- name: Deploy sshd_config
ansible.builtin.template:
src: sshd_config.j2
dest: /etc/ssh/sshd_config
owner: root
group: root
mode: "0600"
validate: "/usr/sbin/sshd -t -f %s"
notify: restart sshd
tags: [ssh_hardening]
- name: Create sshd_config.d drop-in directory for future exceptions
ansible.builtin.file:
path: /etc/ssh/sshd_config.d
state: directory
owner: root
group: root
mode: "0755"
tags: [ssh_hardening]
- name: Enable and start sshd
ansible.builtin.systemd:
name: sshd
enabled: true
state: started
tags: [ssh_hardening]

View File

@@ -0,0 +1,34 @@
# {{ ansible_managed }}
Port {{ ssh_port }}
Protocol 2
PermitRootLogin no
PasswordAuthentication no
PubkeyAuthentication yes
ChallengeResponseAuthentication no
KbdInteractiveAuthentication no
UsePAM yes
MaxAuthTries {{ ssh_max_auth_tries }}
AllowGroups {{ ssh_allow_groups }}
ClientAliveInterval {{ ssh_client_alive_interval }}
ClientAliveCountMax {{ ssh_client_alive_count_max }}
LoginGraceTime 30
MaxSessions 4
MaxStartups 10:30:60
X11Forwarding no
AllowTcpForwarding no
AllowAgentForwarding no
PermitTunnel no
GatewayPorts no
Ciphers chacha20-poly1305@openssh.com,aes256-gcm@openssh.com,aes128-gcm@openssh.com,aes256-ctr,aes192-ctr,aes128-ctr
MACs hmac-sha2-512-etm@openssh.com,hmac-sha2-256-etm@openssh.com,umac-128-etm@openssh.com
KexAlgorithms curve25519-sha256,curve25519-sha256@libssh.org,diffie-hellman-group16-sha512
Banner /etc/issue.net
LogLevel VERBOSE
SyslogFacility AUTHPRIV
Include /etc/ssh/sshd_config.d/*.conf

View File

@@ -0,0 +1,4 @@
---
- name: reload auditd rules
ansible.builtin.command: augenrules --load
changed_when: true

View File

@@ -0,0 +1,31 @@
---
- name: Deploy sudoers.d drop-in file per AD group
ansible.builtin.template:
src: sudoers_group.j2
dest: "/etc/sudoers.d/10-{{ item.group | lower }}"
owner: root
group: root
mode: "0440"
validate: "visudo -cf %s"
loop: "{{ sudo_rbac_rules }}"
loop_control:
label: "{{ item.group }}"
tags: [sudo_rbac]
- name: Restrict direct root console login
ansible.builtin.lineinfile:
path: /etc/securetty
state: absent
regexp: "^tty[1-9]$"
tags: [sudo_rbac, cis_hardening]
- name: Audit rule for privileged command usage
ansible.builtin.lineinfile:
path: /etc/audit/rules.d/50-privileged.rules
create: true
owner: root
group: root
mode: "0640"
line: "-a exit,always -F arch=b64 -C euid!=uid -F auid!=unset -S execve -k privileged"
notify: reload auditd rules
tags: [sudo_rbac, cis_hardening]

View File

@@ -0,0 +1,2 @@
# {{ ansible_managed }} - do not edit manually, changes will be overwritten
%{{ item.group }} ALL=(ALL) {{ 'NOPASSWD:' if item.nopasswd else '' }} {{ item.commands }}