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

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.vault_pass
vault/secrets.yml
*.retry
*.pyc
__pycache__/
.ansible/

109
README.md Normal file
View File

@@ -0,0 +1,109 @@
# Ansible Repository - Linux Server Infrastructure
Automated provisioning and hardening of Linux servers, integrated with an
existing Active Directory environment.
## Supported platforms
- Rocky Linux 9 / RHEL 9 (minimal install)
- Ubuntu 22.04 / 24.04 LTS
- Debian 11 / 12
All roles detect `ansible_facts['os_family']` (`RedHat` / `Debian`) and
`ansible_facts['distribution']` automatically; a single inventory and
playbook run can target a mix of both families. Where the underlying
tooling differs fundamentally (SELinux vs. AppArmor, dnf-automatic vs.
unattended-upgrades), a role runs only on its matching family - see the
role table below.
## Requirements
```bash
ansible-galaxy collection install -r requirements.yml
```
Target hosts must be reachable via SSH from the Ansible control node with
an administrative sudo-capable account.
## Getting started
1. Update `inventories/production/hosts.ini` for your environment
(hostnames, IP addresses, internal/DMZ group membership).
2. Review and adjust `inventories/production/group_vars/all.yml`
(domain, AD groups, syslog target, admin subnet, etc.).
3. Set up secrets:
```bash
cp vault/secrets.yml.example vault/secrets.yml
# fill in values (AD join account, CrowdStrike Falcon CID, ...)
ansible-vault encrypt vault/secrets.yml
```
4. Store the vault password in `.vault_pass` (chmod 600, **do not** commit
it) or wire it up to your organization's secret store.
## Running
```bash
# Dry run (recommended before every real run)
ansible-playbook playbooks/site.yml --check --diff
# Full rollout
ansible-playbook playbooks/site.yml
# Hardening roles only
ansible-playbook playbooks/site.yml --tags hardening
# Add a new server
ansible-playbook playbooks/baseline.yml --limit <new-hostname>
# Targeted patch run (e.g. for scheduled execution)
ansible-playbook playbooks/patch_only.yml
# Post-deployment sanity checks (read-only)
ansible-playbook playbooks/verify.yml
```
## Quality checks
```bash
ansible-lint
ansible-playbook playbooks/site.yml --syntax-check
```
## Role overview
| Role | Purpose | Platforms |
|---|---|---|
| `base_os` | Base packages, time sync, hostname, disable unneeded services | all |
| `repo_management` | Internal repos always; EPEL/CRB (RedHat) or universe/backports (Debian) only with internet access | all |
| `identity_ad` | sssd/realmd, domain join | all |
| `sudo_rbac` | sudoers.d per AD group | all |
| `local_accounts` | Break-glass account, optional local service accounts | all |
| `banners` | Pre-auth login warning banners (`/etc/issue`, `/etc/issue.net`) | all |
| `ssh_hardening` | sshd_config, public-key-only | all |
| `fail2ban` | SSH brute-force protection (requires EPEL on RedHat family) | all |
| `pam_hardening` | Password complexity (pwquality), account lockout (faillock) | all |
| `cis_hardening` | CIS-style baseline: sysctl, mounts, module blacklist, umask, TMOUT, Ctrl-Alt-Del, sticky bit, journald, auditd, GRUB password | all |
| `selinux_config` | SELinux enforcing mode, booleans, file contexts | RedHat only |
| `apparmor_config` | AppArmor enforce mode, complain-mode exceptions | Debian only |
| `firewall_config` | firewalld default-deny | all |
| `falcon_onboarding` | CrowdStrike Falcon sensor (rpm/deb) | all |
| `logging_rsyslog` | Log forwarding to SIEM, CA certificate distribution | all |
| `aide_integrity` | File integrity monitoring (AIDE) | all |
| `patch_mgmt` | dnf-automatic (RedHat) or unattended-upgrades (Debian) | all |
| `backup_agent` | Veeam agent (optional, per host) | all |
## Open items before rollout
- Finalize the IP scheme in `hosts.ini` (naming scheme: `<site>-<function><number>`, e.g. `GS-AP00015`).
- TLS syslog (6514) vs. plain syslog (514) - depends on the SIEM.
- Set `falcon_onboarding_sensor_package_url` or `falcon_onboarding_sensor_package_src` per host to the package matching that host's OS (rpm for RedHat family, deb for Debian family).
- Set `veeam_agent_repo_url` (RedHat) or `veeam_agent_apt_repo_line` (Debian).
- Set `syslog_ca_cert_enabled`/`syslog_ca_cert_src` once it's clear whether the SIEM certificate is signed by an internal CA (see `roles/logging_rsyslog/files/README.md`).
- Populate `custom_yum_repos`/`custom_apt_repos` with real internal repo URLs; only enable `epel_enabled`/`crb_enabled`/`ubuntu_universe_enabled`/`debian_backports_enabled` where genuinely needed.
- `fail2ban` requires EPEL on RedHat-family hosts - either enable `epel_enabled: true` or provide fail2ban via `custom_yum_repos`.
- Set `cis_grub_password_enabled` and `grub_bootloader_password_hash` (generate with `grub2-mkpasswd-pbkdf2`) if a GRUB bootloader password is desired.
- `admin_space_left_action = halt` in `cis_hardening` (auditd) is a strict setting - confirm this is acceptable, since it halts the system if the audit log volume fills up.
- `cis_boot_hardening_enabled` is off by default since it edits `/etc/fstab` for `/boot` and requires a reboot to take effect - review before enabling.
- The AIDE config-directory include mechanism and default database path/extension (`roles/aide_integrity/vars/Debian.yml`) should be verified against the actual installed `aide`/`aide-common` package version on first rollout - Debian/Ubuntu point releases have varied here.
- Coordinate AIDE database updates (`aide --update`) with the patch-management maintenance window, since scheduled patch runs will otherwise show up as AIDE findings.
- Define CIS Level 2 / AppArmor exceptions per application (see `selinux_config`, `apparmor_config`, and `firewall_config` defaults as a starting point).

21
ansible.cfg Normal file
View File

@@ -0,0 +1,21 @@
[defaults]
inventory = inventories/production/hosts.ini
roles_path = roles
remote_user = ansible_svc
host_key_checking = True
retry_files_enabled = False
interpreter_python = auto_silent
vault_password_file = .vault_pass
stdout_callback = default
result_format = yaml
forks = 10
timeout = 30
[privilege_escalation]
become = True
become_method = sudo
become_ask_pass = False
[ssh_connection]
pipelining = True
ssh_args = -o ControlMaster=auto -o ControlPersist=60s -o PreferredAuthentications=publickey

View File

@@ -0,0 +1,156 @@
---
# Global variables for all managed Linux servers.
# Replace placeholder values (domain, hosts, URLs) before rollout.
# base_os
# Base package lists and unwanted-package lists are OS-family-specific
# (see roles/base_os/vars/RedHat.yml and roles/base_os/vars/Debian.yml).
# Use base_extra_packages for additions that apply across all hosts.
timezone: "Europe/Berlin"
ntp_servers:
- "0.de.pool.ntp.org"
- "1.de.pool.ntp.org"
base_extra_packages: []
base_unneeded_services:
- avahi-daemon
- cups
- rpcbind
- bluetooth
# repo_management
repo_mgmt_check_internet: true
repo_mgmt_internet_check_url: "https://dl.rockylinux.org"
# RedHat family
epel_enabled: false
epel_major_version: "9"
crb_enabled: true
custom_yum_repos: []
# custom_yum_repos:
# - name: "internal"
# description: "Internal repository"
# baseurl: "https://repo.example.corp/rocky9/internal/"
# gpgcheck: true
# gpgkey: "https://repo.example.corp/RPM-GPG-KEY-internal"
# Debian family
ubuntu_universe_enabled: false
debian_backports_enabled: false
custom_apt_repos: []
# custom_apt_repos:
# - name: "internal"
# uris: "https://repo.example.corp/ubuntu"
# suites: "{{ ansible_facts['distribution_release'] }}"
# components: ["main"]
# signed_by: "https://repo.example.corp/gpg"
# identity_ad
ad_domain: "example.corp"
ad_domain_realm: "EXAMPLE.CORP"
ad_ou: "OU=LinuxServers,DC=example,DC=corp"
ad_admin_group: "GG_Linux_Admins"
ad_operator_group: "GG_Linux_Operators"
sssd_use_fully_qualified_names: false
# sudo_rbac
sudo_rbac_rules:
- group: "{{ ad_admin_group }}"
commands: "ALL"
nopasswd: false
- group: "{{ ad_operator_group }}"
commands: "/usr/bin/systemctl restart *, /usr/bin/systemctl status *, /usr/bin/journalctl *"
nopasswd: false
# local_accounts
breakglass_username: "bglocal_admin"
breakglass_comment: "Local emergency (break-glass) account"
local_service_accounts: []
# ssh_hardening
ssh_allow_groups: "linux_admins linux_operators"
ssh_port: 22
ssh_max_auth_tries: 3
ssh_client_alive_interval: 300
ssh_client_alive_count_max: 2
# pam_hardening
pam_pwquality_minlen: 14
pam_pwquality_dcredit: -1
pam_pwquality_ucredit: -1
pam_pwquality_lcredit: -1
pam_pwquality_ocredit: -1
pam_pwquality_retry: 3
pam_faillock_deny: 5
pam_faillock_unlock_time: 900
# fail2ban
fail2ban_bantime: 3600
fail2ban_findtime: 600
fail2ban_maxretry: 5
fail2ban_ignoreip: "127.0.0.1/8 ::1 {{ firewall_admin_subnet }}"
# banners
banner_enabled: true
# cis_hardening
cis_level: 1 # raised to 2 in group_vars/dmz.yml
cis_disable_filesystems:
- cramfs
- freevxfs
- jffs2
- hfs
- hfsplus
- udf
cis_disable_modules_level2:
- usb-storage
- bluetooth
- firewire-core
cis_shell_tmout_seconds: 900
cis_umask: "027"
cis_disable_ctrl_alt_del: true
cis_sticky_bit_enforce: true
cis_journald_persistent: true
cis_journald_max_use: "500M"
cis_boot_hardening_enabled: false # requires reboot to take effect, verify manually first
cis_unowned_files_report_enabled: true
cis_unowned_files_report_cron_hour: 4
cis_grub_password_enabled: false
grub_bootloader_username: "grubadmin"
grub_bootloader_is_uefi: false
# selinux_config
selinux_state: enforcing
selinux_policy: targeted
# firewall_config
firewall_default_zone: "drop"
firewall_admin_subnet: "10.10.5.0/24"
firewall_allowed_services: []
firewall_log_denied: "unicast"
# falcon_onboarding
falcon_onboarding_cloud_region: "eu-1"
# falcon_onboarding_sensor_package_url / falcon_onboarding_sensor_package_src
# and falcon_cid are set per environment / in vault/secrets.yml
# logging_rsyslog
syslog_collector_host: "siem.example.corp"
syslog_collector_port: 6514
syslog_use_tls: true
syslog_ca_cert_enabled: false
syslog_ca_cert_src: "siem-ca.crt" # relative to roles/logging_rsyslog/files/
# aide_integrity
aide_check_cron_hour: 5
aide_check_cron_minute: 0
aide_notify_email: "root"
# patch_mgmt
dnf_automatic_apply_security: true
dnf_automatic_apply_other: false
patch_reboot_window_hosts: []
patch_reboot_day: "sun"
patch_reboot_time: "03:00"
# backup_agent
veeam_agent_enabled: false
# veeam_agent_repo_url (RedHat family) / veeam_agent_apt_repo_line (Debian family)
# are set per environment, see roles/backup_agent/defaults/main.yml

View File

@@ -0,0 +1,12 @@
---
# Overrides for the "dmz" group (see hosts.ini).
cis_level: 2
firewall_allowed_services:
- service: "https"
source: "any"
- service: "ssh"
source: "{{ firewall_admin_subnet }}"
firewall_default_zone: "drop"

View File

@@ -0,0 +1,25 @@
; Example inventory - update hostnames/IPs before first run.
; Naming scheme: <site-prefix>-<function-code><5-digit-number>, e.g. GS-AP00015.
; Groups may contain a mix of Rocky/RHEL and Ubuntu/Debian hosts - all roles
; detect the OS family automatically (ansible_facts['os_family']).
[linux_internal]
GS-AP00021 ansible_host=10.10.20.11
GS-AP00022 ansible_host=10.10.20.12
[linux_dmz]
GS-AP00031 ansible_host=10.10.99.11
GS-AP00032 ansible_host=10.10.99.12
; Group "dmz" is used by group_vars/dmz.yml (CIS Level 2, stricter firewall)
[dmz:children]
linux_dmz
; Umbrella group for all managed Linux servers
[linux_all:children]
linux_internal
linux_dmz
[linux_all:vars]
ansible_user=ansible_svc
ansible_python_interpreter=/usr/bin/python3

33
playbooks/baseline.yml Normal file
View File

@@ -0,0 +1,33 @@
---
# Onboarding process for a newly added server.
# Run scoped to exactly the new host:
# ansible-playbook playbooks/baseline.yml --limit GS-AP00023 --ask-vault-pass
#
# Prerequisite: the host has already been added to
# inventories/production/hosts.ini (and group_vars/host_vars as needed).
- name: Baseline rollout for a newly added Linux server
hosts: "{{ target_host | default('linux_all') }}"
become: true
vars_files:
- ../vault/secrets.yml
roles:
- role: base_os
- role: repo_management
- role: identity_ad
- role: sudo_rbac
- role: local_accounts
- role: banners
- role: ssh_hardening
- role: fail2ban
- role: pam_hardening
- role: cis_hardening
- role: selinux_config
when: ansible_facts['os_family'] == "RedHat"
- role: apparmor_config
when: ansible_facts['os_family'] == "Debian"
- role: firewall_config
- role: falcon_onboarding
- role: logging_rsyslog
- role: aide_integrity
- role: patch_mgmt

10
playbooks/patch_only.yml Normal file
View File

@@ -0,0 +1,10 @@
---
# Targeted patch-management run without re-applying the other roles.
# Intended for scheduled execution (cron/AWX).
# Run: ansible-playbook playbooks/patch_only.yml
- name: Apply patch management configuration only
hosts: linux_all
become: true
roles:
- role: patch_mgmt

51
playbooks/site.yml Normal file
View File

@@ -0,0 +1,51 @@
---
# Full rollout of all roles.
# Full run: ansible-playbook playbooks/site.yml --ask-vault-pass
# Dry run: ansible-playbook playbooks/site.yml --check --diff
# Subset only: ansible-playbook playbooks/site.yml --tags hardening
- name: Deploy hardened Linux baseline configuration
hosts: linux_all
become: true
vars_files:
- ../vault/secrets.yml
roles:
- role: base_os
tags: [base_os]
- role: repo_management
tags: [repo_management]
- role: identity_ad
tags: [identity_ad, hardening]
- role: sudo_rbac
tags: [sudo_rbac, hardening]
- role: local_accounts
tags: [local_accounts, hardening]
- role: banners
tags: [banners, hardening]
- role: ssh_hardening
tags: [ssh_hardening, hardening]
- role: fail2ban
tags: [fail2ban, hardening]
- role: pam_hardening
tags: [pam_hardening, hardening]
- role: cis_hardening
tags: [cis_hardening, hardening]
- role: selinux_config
tags: [selinux_config, hardening]
when: ansible_facts['os_family'] == "RedHat"
- role: apparmor_config
tags: [apparmor_config, hardening]
when: ansible_facts['os_family'] == "Debian"
- role: firewall_config
tags: [firewall_config, hardening]
- role: falcon_onboarding
tags: [falcon_onboarding, hardening]
- role: logging_rsyslog
tags: [logging_rsyslog, monitoring]
- role: aide_integrity
tags: [aide_integrity, monitoring]
- role: patch_mgmt
tags: [patch_mgmt, patching]
- role: backup_agent
tags: [backup_agent]
when: veeam_agent_enabled | default(false) | bool

78
playbooks/verify.yml Normal file
View File

@@ -0,0 +1,78 @@
---
# Post-deployment sanity checks. Read-only, makes no changes.
# Run: ansible-playbook playbooks/verify.yml
- name: Verify baseline hardening status
hosts: linux_all
become: true
gather_facts: true
tasks:
- name: Collect service facts
ansible.builtin.service_facts:
- name: Check expected services are running
ansible.builtin.assert:
that:
- "'sshd.service' in ansible_facts.services"
- "ansible_facts.services['sshd.service'].state == 'running'"
- "'firewalld.service' in ansible_facts.services"
- "ansible_facts.services['firewalld.service'].state == 'running'"
- "'auditd.service' in ansible_facts.services"
- "ansible_facts.services['auditd.service'].state == 'running'"
fail_msg: "One or more expected core services are not running."
success_msg: "Core services (sshd, firewalld, auditd) are running."
- name: Check SELinux is enforcing (RedHat family)
ansible.builtin.command: getenforce
register: verify_selinux_status
changed_when: false
when: ansible_facts['os_family'] == "RedHat"
- name: Assert SELinux enforcing (RedHat family)
ansible.builtin.assert:
that:
- "verify_selinux_status.stdout == 'Enforcing'"
fail_msg: "SELinux is not in enforcing mode."
success_msg: "SELinux is enforcing."
when: ansible_facts['os_family'] == "RedHat"
- name: Check AppArmor status (Debian family)
ansible.builtin.command: aa-status --enforced
register: verify_apparmor_status
changed_when: false
failed_when: false
when: ansible_facts['os_family'] == "Debian"
- name: Report AppArmor enforced profile count (Debian family)
ansible.builtin.debug:
msg: "AppArmor enforced profiles on {{ inventory_hostname }}: {{ verify_apparmor_status.stdout | default('unavailable') }}"
when: ansible_facts['os_family'] == "Debian"
- name: Check CrowdStrike Falcon sensor health
ansible.builtin.command: /opt/CrowdStrike/falconctl -g --rfm-state
register: verify_falcon_rfm
changed_when: false
failed_when: false
- name: Report Falcon sensor status
ansible.builtin.debug:
msg: "Falcon sensor RFM state on {{ inventory_hostname }}: {{ verify_falcon_rfm.stdout | default('not installed / not reachable') }}"
- name: Check AIDE database exists (RedHat family)
ansible.builtin.stat:
path: /var/lib/aide/aide.db.gz
register: verify_aide_db_redhat
when: ansible_facts['os_family'] == "RedHat"
- name: Check AIDE database exists (Debian family)
ansible.builtin.stat:
path: /var/lib/aide/aide.db
register: verify_aide_db_debian
when: ansible_facts['os_family'] == "Debian"
- name: Assert AIDE database is present
ansible.builtin.assert:
that:
- (verify_aide_db_redhat.stat.exists | default(false)) or (verify_aide_db_debian.stat.exists | default(false))
fail_msg: "AIDE database has not been initialized."
success_msg: "AIDE database is present."

7
requirements.yml Normal file
View File

@@ -0,0 +1,7 @@
---
# Install with: ansible-galaxy collection install -r requirements.yml
collections:
- name: ansible.posix
version: ">=1.5.0"
- name: community.general
version: ">=8.0.0"

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 }}

27
vault/secrets.yml.example Normal file
View File

@@ -0,0 +1,27 @@
---
# Template for sensitive variables.
#
# Usage:
# 1. cp secrets.yml.example secrets.yml
# 2. Fill in the values below
# 3. ansible-vault encrypt vault/secrets.yml
# 4. Store the vault password in .vault_pass (chmod 600, do NOT commit it)
# or wire it up to your organization's secret store.
# identity_ad
ad_join_username: "svc-linuxjoin"
ad_join_password: "CHANGE_ME"
# falcon_onboarding
falcon_cid: "CHANGE_ME-CID-FROM-FALCON-CONSOLE"
falcon_onboarding_provisioning_token: ""
# cis_hardening: GRUB bootloader password
# Generate the hash beforehand with: grub2-mkpasswd-pbkdf2
# Only relevant if cis_grub_password_enabled: true
grub_bootloader_password_hash: ""
# local_accounts: the break-glass account password is intentionally NOT
# managed here. Ansible only creates the account with a locked password
# hash; set the real password manually on the host and store it in your
# organization's password safe.