You've already forked ansible-linux-infra
initial commit
This commit is contained in:
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
.vault_pass
|
||||||
|
vault/secrets.yml
|
||||||
|
*.retry
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
.ansible/
|
||||||
109
README.md
Normal file
109
README.md
Normal 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
21
ansible.cfg
Normal 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
|
||||||
156
inventories/production/group_vars/all.yml
Normal file
156
inventories/production/group_vars/all.yml
Normal 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
|
||||||
12
inventories/production/group_vars/dmz.yml
Normal file
12
inventories/production/group_vars/dmz.yml
Normal 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"
|
||||||
25
inventories/production/hosts.ini
Normal file
25
inventories/production/hosts.ini
Normal 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
33
playbooks/baseline.yml
Normal 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
10
playbooks/patch_only.yml
Normal 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
51
playbooks/site.yml
Normal 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
78
playbooks/verify.yml
Normal 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
7
requirements.yml
Normal 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"
|
||||||
59
roles/aide_integrity/tasks/main.yml
Normal file
59
roles/aide_integrity/tasks/main.yml
Normal 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]
|
||||||
12
roles/aide_integrity/templates/aide-custom.conf.j2
Normal file
12
roles/aide_integrity/templates/aide-custom.conf.j2
Normal 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
|
||||||
5
roles/aide_integrity/vars/Debian.yml
Normal file
5
roles/aide_integrity/vars/Debian.yml
Normal 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
|
||||||
5
roles/aide_integrity/vars/RedHat.yml
Normal file
5
roles/aide_integrity/vars/RedHat.yml
Normal 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
|
||||||
4
roles/apparmor_config/defaults/main.yml
Normal file
4
roles/apparmor_config/defaults/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
apparmor_config_complain_profiles: []
|
||||||
|
# apparmor_config_complain_profiles:
|
||||||
|
# - /usr/sbin/nginx
|
||||||
42
roles/apparmor_config/tasks/main.yml
Normal file
42
roles/apparmor_config/tasks/main.yml
Normal 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]
|
||||||
6
roles/backup_agent/defaults/main.yml
Normal file
6
roles/backup_agent/defaults/main.yml
Normal 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"
|
||||||
48
roles/backup_agent/tasks/main.yml
Normal file
48
roles/backup_agent/tasks/main.yml
Normal 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]
|
||||||
5
roles/banners/handlers/main.yml
Normal file
5
roles/banners/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart sshd (banners)
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
21
roles/banners/tasks/main.yml
Normal file
21
roles/banners/tasks/main.yml
Normal 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]
|
||||||
5
roles/banners/templates/issue.j2
Normal file
5
roles/banners/templates/issue.j2
Normal 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.
|
||||||
|
******************************************************************
|
||||||
5
roles/banners/templates/issue.net.j2
Normal file
5
roles/banners/templates/issue.net.j2
Normal 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.
|
||||||
|
******************************************************************
|
||||||
5
roles/base_os/handlers/main.yml
Normal file
5
roles/base_os/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart chrony
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: "{{ base_os_chrony_service_name }}"
|
||||||
|
state: restarted
|
||||||
75
roles/base_os/tasks/main.yml
Normal file
75
roles/base_os/tasks/main.yml
Normal 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]
|
||||||
9
roles/base_os/templates/chrony.conf.j2
Normal file
9
roles/base_os/templates/chrony.conf.j2
Normal 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
|
||||||
6
roles/base_os/templates/motd.j2
Normal file
6
roles/base_os/templates/motd.j2
Normal 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 }}).
|
||||||
|
******************************************************************
|
||||||
17
roles/base_os/vars/Debian.yml
Normal file
17
roles/base_os/vars/Debian.yml
Normal 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
|
||||||
17
roles/base_os/vars/RedHat.yml
Normal file
17
roles/base_os/vars/RedHat.yml
Normal 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
|
||||||
20
roles/cis_hardening/handlers/main.yml
Normal file
20
roles/cis_hardening/handlers/main.yml
Normal 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
|
||||||
255
roles/cis_hardening/tasks/main.yml
Normal file
255
roles/cis_hardening/tasks/main.yml
Normal 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]
|
||||||
19
roles/cis_hardening/templates/audit-baseline.rules.j2
Normal file
19
roles/cis_hardening/templates/audit-baseline.rules.j2
Normal 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
|
||||||
9
roles/cis_hardening/templates/audit-level2.rules.j2
Normal file
9
roles/cis_hardening/templates/audit-level2.rules.j2
Normal 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
|
||||||
4
roles/cis_hardening/vars/Debian.yml
Normal file
4
roles/cis_hardening/vars/Debian.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
cis_hardening_grub_mkconfig_cmd: "update-grub"
|
||||||
|
cis_hardening_grub_cfg_path_bios: ""
|
||||||
|
cis_hardening_grub_cfg_path_uefi: ""
|
||||||
4
roles/cis_hardening/vars/RedHat.yml
Normal file
4
roles/cis_hardening/vars/RedHat.yml
Normal 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"
|
||||||
5
roles/fail2ban/handlers/main.yml
Normal file
5
roles/fail2ban/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart fail2ban
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: fail2ban
|
||||||
|
state: restarted
|
||||||
37
roles/fail2ban/tasks/main.yml
Normal file
37
roles/fail2ban/tasks/main.yml
Normal 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]
|
||||||
11
roles/fail2ban/templates/jail.local.j2
Normal file
11
roles/fail2ban/templates/jail.local.j2
Normal 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 }}
|
||||||
9
roles/falcon_onboarding/defaults/main.yml
Normal file
9
roles/falcon_onboarding/defaults/main.yml
Normal 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"
|
||||||
5
roles/falcon_onboarding/handlers/main.yml
Normal file
5
roles/falcon_onboarding/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart falcon-sensor
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: falcon-sensor
|
||||||
|
state: restarted
|
||||||
100
roles/falcon_onboarding/tasks/main.yml
Normal file
100
roles/falcon_onboarding/tasks/main.yml
Normal 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]
|
||||||
2
roles/falcon_onboarding/vars/Debian.yml
Normal file
2
roles/falcon_onboarding/vars/Debian.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
falcon_onboarding_package_ext: deb
|
||||||
2
roles/falcon_onboarding/vars/RedHat.yml
Normal file
2
roles/falcon_onboarding/vars/RedHat.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
falcon_onboarding_package_ext: rpm
|
||||||
56
roles/firewall_config/tasks/main.yml
Normal file
56
roles/firewall_config/tasks/main.yml
Normal 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]
|
||||||
5
roles/identity_ad/handlers/main.yml
Normal file
5
roles/identity_ad/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart sssd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sssd
|
||||||
|
state: restarted
|
||||||
89
roles/identity_ad/tasks/main.yml
Normal file
89
roles/identity_ad/tasks/main.yml
Normal 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]
|
||||||
23
roles/identity_ad/templates/sssd.conf.j2
Normal file
23
roles/identity_ad/templates/sssd.conf.j2
Normal 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
|
||||||
10
roles/identity_ad/vars/Debian.yml
Normal file
10
roles/identity_ad/vars/Debian.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
identity_ad_packages:
|
||||||
|
- realmd
|
||||||
|
- sssd
|
||||||
|
- sssd-tools
|
||||||
|
- adcli
|
||||||
|
- samba-common-bin
|
||||||
|
- krb5-user
|
||||||
|
- libnss-sss
|
||||||
|
- libpam-sss
|
||||||
10
roles/identity_ad/vars/RedHat.yml
Normal file
10
roles/identity_ad/vars/RedHat.yml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
identity_ad_packages:
|
||||||
|
- realmd
|
||||||
|
- sssd
|
||||||
|
- sssd-tools
|
||||||
|
- oddjob
|
||||||
|
- oddjob-mkhomedir
|
||||||
|
- adcli
|
||||||
|
- samba-common-tools
|
||||||
|
- krb5-workstation
|
||||||
5
roles/local_accounts/handlers/main.yml
Normal file
5
roles/local_accounts/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart sshd (local_accounts)
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
67
roles/local_accounts/tasks/main.yml
Normal file
67
roles/local_accounts/tasks/main.yml
Normal 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]
|
||||||
6
roles/logging_rsyslog/files/README.md
Normal file
6
roles/logging_rsyslog/files/README.md
Normal 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.
|
||||||
9
roles/logging_rsyslog/handlers/main.yml
Normal file
9
roles/logging_rsyslog/handlers/main.yml
Normal 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
|
||||||
60
roles/logging_rsyslog/tasks/main.yml
Normal file
60
roles/logging_rsyslog/tasks/main.yml
Normal 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]
|
||||||
24
roles/logging_rsyslog/templates/60-forward-siem.conf.j2
Normal file
24
roles/logging_rsyslog/templates/60-forward-siem.conf.j2
Normal 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 %}
|
||||||
14
roles/logging_rsyslog/templates/logrotate-cis.j2
Normal file
14
roles/logging_rsyslog/templates/logrotate-cis.j2
Normal 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
|
||||||
|
}
|
||||||
3
roles/logging_rsyslog/vars/Debian.yml
Normal file
3
roles/logging_rsyslog/vars/Debian.yml
Normal 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
|
||||||
3
roles/logging_rsyslog/vars/RedHat.yml
Normal file
3
roles/logging_rsyslog/vars/RedHat.yml
Normal 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
|
||||||
63
roles/pam_hardening/tasks/main.yml
Normal file
63
roles/pam_hardening/tasks/main.yml
Normal 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]
|
||||||
2
roles/pam_hardening/vars/Debian.yml
Normal file
2
roles/pam_hardening/vars/Debian.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
pam_hardening_pwquality_package: libpam-pwquality
|
||||||
2
roles/pam_hardening/vars/RedHat.yml
Normal file
2
roles/pam_hardening/vars/RedHat.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
pam_hardening_pwquality_package: libpwquality
|
||||||
90
roles/patch_mgmt/tasks/main.yml
Normal file
90
roles/patch_mgmt/tasks/main.yml
Normal 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]
|
||||||
5
roles/patch_mgmt/templates/20auto-upgrades.j2
Normal file
5
roles/patch_mgmt/templates/20auto-upgrades.j2
Normal 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";
|
||||||
11
roles/patch_mgmt/templates/50unattended-upgrades.j2
Normal file
11
roles/patch_mgmt/templates/50unattended-upgrades.j2
Normal 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";
|
||||||
17
roles/patch_mgmt/templates/automatic.conf.j2
Normal file
17
roles/patch_mgmt/templates/automatic.conf.j2
Normal 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
|
||||||
25
roles/patch_mgmt/templates/check-reboot-needed.sh.j2
Normal file
25
roles/patch_mgmt/templates/check-reboot-needed.sh.j2
Normal 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
|
||||||
4
roles/patch_mgmt/vars/Debian.yml
Normal file
4
roles/patch_mgmt/vars/Debian.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
patch_mgmt_packages:
|
||||||
|
- unattended-upgrades
|
||||||
|
- apt-listchanges
|
||||||
4
roles/patch_mgmt/vars/RedHat.yml
Normal file
4
roles/patch_mgmt/vars/RedHat.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
patch_mgmt_packages:
|
||||||
|
- dnf-automatic
|
||||||
|
- dnf-utils
|
||||||
122
roles/repo_management/tasks/main.yml
Normal file
122
roles/repo_management/tasks/main.yml
Normal 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]
|
||||||
8
roles/selinux_config/defaults/main.yml
Normal file
8
roles/selinux_config/defaults/main.yml
Normal 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" }
|
||||||
2
roles/selinux_config/handlers/main.yml
Normal file
2
roles/selinux_config/handlers/main.yml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
---
|
||||||
|
# No handlers needed - restorecon is applied directly in tasks/main.yml
|
||||||
58
roles/selinux_config/tasks/main.yml
Normal file
58
roles/selinux_config/tasks/main.yml
Normal 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]
|
||||||
5
roles/ssh_hardening/handlers/main.yml
Normal file
5
roles/ssh_hardening/handlers/main.yml
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
- name: restart sshd
|
||||||
|
ansible.builtin.systemd:
|
||||||
|
name: sshd
|
||||||
|
state: restarted
|
||||||
27
roles/ssh_hardening/tasks/main.yml
Normal file
27
roles/ssh_hardening/tasks/main.yml
Normal 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]
|
||||||
34
roles/ssh_hardening/templates/sshd_config.j2
Normal file
34
roles/ssh_hardening/templates/sshd_config.j2
Normal 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
|
||||||
4
roles/sudo_rbac/handlers/main.yml
Normal file
4
roles/sudo_rbac/handlers/main.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
---
|
||||||
|
- name: reload auditd rules
|
||||||
|
ansible.builtin.command: augenrules --load
|
||||||
|
changed_when: true
|
||||||
31
roles/sudo_rbac/tasks/main.yml
Normal file
31
roles/sudo_rbac/tasks/main.yml
Normal 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]
|
||||||
2
roles/sudo_rbac/templates/sudoers_group.j2
Normal file
2
roles/sudo_rbac/templates/sudoers_group.j2
Normal 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
27
vault/secrets.yml.example
Normal 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.
|
||||||
Reference in New Issue
Block a user