CIS + inital
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
---
|
||||
# Secure Firewall Role - Default Variables
|
||||
|
||||
# Firewall backend (ufw or iptables)
|
||||
firewall_backend: ufw
|
||||
|
||||
# Default policies
|
||||
firewall_default_input_policy: deny
|
||||
firewall_default_output_policy: allow
|
||||
firewall_default_forward_policy: deny
|
||||
|
||||
# VPN-only mode: Only allow management access from specified networks/IPs
|
||||
vpn_only_mode: true
|
||||
|
||||
# Management access sources
|
||||
# Can be:
|
||||
# - VPN network CIDR (e.g., "10.100.0.0/24" for admin VPN)
|
||||
# - Single IP (e.g., "185.112.147.205" for ValleyForge public IP)
|
||||
# - List of both
|
||||
management_allowed_sources: []
|
||||
# Example:
|
||||
# management_allowed_sources:
|
||||
# - "10.100.0.0/24" # Admin VPN network
|
||||
# - "185.112.147.205" # ValleyForge public IP
|
||||
|
||||
# Management ports (restricted to management_allowed_sources if vpn_only_mode is true)
|
||||
management_ports:
|
||||
- port: 22
|
||||
proto: tcp
|
||||
comment: "SSH"
|
||||
- port: 80
|
||||
proto: tcp
|
||||
comment: "HTTP"
|
||||
- port: 443
|
||||
proto: tcp
|
||||
comment: "HTTPS"
|
||||
- port: 8080
|
||||
proto: tcp
|
||||
comment: "Outline Manager"
|
||||
- port: 8065
|
||||
proto: tcp
|
||||
comment: "Mattermost"
|
||||
- port: 8443
|
||||
proto: tcp
|
||||
comment: "Nextcloud HTTPS"
|
||||
|
||||
# Public ports (always accessible from internet)
|
||||
public_ports:
|
||||
- port: 51820
|
||||
proto: udp
|
||||
comment: "WireGuard VPN"
|
||||
|
||||
# Rate limiting for SSH
|
||||
ssh_rate_limit: true
|
||||
ssh_rate_limit_burst: 10
|
||||
ssh_rate_limit_rate: "30/minute"
|
||||
|
||||
# Logging
|
||||
firewall_logging: "low" # off, low, medium, high, full
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
# Secure Firewall Role - Handlers
|
||||
|
||||
- name: reload ufw
|
||||
community.general.ufw:
|
||||
state: reloaded
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: secure_firewall
|
||||
author: Security Infrastructure Team
|
||||
description: Secure firewall configuration with VPN-only access mode
|
||||
company: Your Organization
|
||||
license: MIT
|
||||
min_ansible_version: "2.15"
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- noble # 24.04
|
||||
- jammy # 22.04
|
||||
galaxy_tags:
|
||||
- firewall
|
||||
- ufw
|
||||
- security
|
||||
- vpn
|
||||
|
||||
dependencies:
|
||||
- role: wireguard_server
|
||||
|
||||
collections:
|
||||
- community.general
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
# Secure Firewall Role - Main Tasks
|
||||
|
||||
- name: Include UFW firewall tasks
|
||||
ansible.builtin.include_tasks: ufw.yml
|
||||
when: firewall_backend == "ufw"
|
||||
|
||||
- name: Include VPN-only access tasks
|
||||
ansible.builtin.include_tasks: vpn_only.yml
|
||||
when: vpn_only_mode | bool
|
||||
@@ -0,0 +1,46 @@
|
||||
---
|
||||
# UFW Firewall Configuration Tasks
|
||||
|
||||
- name: Ensure UFW is installed
|
||||
ansible.builtin.apt:
|
||||
name: ufw
|
||||
state: present
|
||||
|
||||
- name: Reset UFW to default state
|
||||
community.general.ufw:
|
||||
state: reset
|
||||
when: firewall_reset | default(false) | bool
|
||||
|
||||
- name: Set UFW default policies
|
||||
community.general.ufw:
|
||||
direction: "{{ item.direction }}"
|
||||
policy: "{{ item.policy }}"
|
||||
loop:
|
||||
- { direction: 'incoming', policy: "{{ firewall_default_input_policy }}" }
|
||||
- { direction: 'outgoing', policy: "{{ firewall_default_output_policy }}" }
|
||||
- { direction: 'routed', policy: "{{ firewall_default_forward_policy }}" }
|
||||
|
||||
- name: Allow public ports (unrestricted)
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ item.port }}"
|
||||
proto: "{{ item.proto }}"
|
||||
comment: "{{ item.comment }}"
|
||||
loop: "{{ public_ports }}"
|
||||
|
||||
- name: Configure UFW logging
|
||||
community.general.ufw:
|
||||
logging: "{{ firewall_logging }}"
|
||||
|
||||
- name: Enable UFW
|
||||
community.general.ufw:
|
||||
state: enabled
|
||||
|
||||
- name: Display firewall status
|
||||
ansible.builtin.command: ufw status verbose
|
||||
register: ufw_status
|
||||
changed_when: false
|
||||
|
||||
- name: Show firewall status
|
||||
ansible.builtin.debug:
|
||||
var: ufw_status.stdout_lines
|
||||
@@ -0,0 +1,83 @@
|
||||
---
|
||||
# VPN-Only Access Configuration Tasks
|
||||
|
||||
- name: Validate management_allowed_sources is defined
|
||||
ansible.builtin.assert:
|
||||
that:
|
||||
- management_allowed_sources is defined
|
||||
- management_allowed_sources | length > 0
|
||||
fail_msg: "management_allowed_sources must be defined and non-empty when vpn_only_mode is true"
|
||||
success_msg: "Management access sources configured: {{ management_allowed_sources | join(', ') }}"
|
||||
|
||||
- name: Restrict management ports to allowed sources only
|
||||
community.general.ufw:
|
||||
rule: allow
|
||||
port: "{{ item.0.port }}"
|
||||
proto: "{{ item.0.proto }}"
|
||||
from_ip: "{{ item.1 }}"
|
||||
comment: "{{ item.0.comment }} (from {{ item.1 }})"
|
||||
loop: "{{ management_ports | product(management_allowed_sources) | list }}"
|
||||
loop_control:
|
||||
label: "{{ item.0.comment }} port {{ item.0.port }} from {{ item.1 }}"
|
||||
|
||||
- name: Apply SSH rate limiting for each allowed source
|
||||
community.general.ufw:
|
||||
rule: limit
|
||||
port: "22"
|
||||
proto: tcp
|
||||
from_ip: "{{ item }}"
|
||||
loop: "{{ management_allowed_sources }}"
|
||||
when: ssh_rate_limit | bool
|
||||
|
||||
- name: Deny management ports from all other sources
|
||||
community.general.ufw:
|
||||
rule: deny
|
||||
port: "{{ item.port }}"
|
||||
proto: "{{ item.proto }}"
|
||||
comment: "Block {{ item.comment }} from public"
|
||||
loop: "{{ management_ports }}"
|
||||
|
||||
- name: Create VPN-only access summary
|
||||
ansible.builtin.copy:
|
||||
dest: /root/firewall-config.txt
|
||||
content: |
|
||||
# Firewall Configuration Summary
|
||||
|
||||
## VPN-Only Mode: ENABLED
|
||||
|
||||
## Management Access Allowed From:
|
||||
{% for source in management_allowed_sources %}
|
||||
- {{ source }}
|
||||
{% endfor %}
|
||||
|
||||
## Management Ports (Restricted Access):
|
||||
{% for port in management_ports %}
|
||||
- {{ port.port }}/{{ port.proto }} - {{ port.comment }}
|
||||
{% endfor %}
|
||||
|
||||
## Public Ports (Unrestricted):
|
||||
{% for port in public_ports %}
|
||||
- {{ port.port }}/{{ port.proto }} - {{ port.comment }}
|
||||
{% endfor %}
|
||||
|
||||
## Security Notes:
|
||||
- Management access ONLY from: {{ management_allowed_sources | join(', ') }}
|
||||
- SSH is rate-limited to prevent brute force
|
||||
- Default policy: DENY all incoming (except allowed above)
|
||||
- Logging level: {{ firewall_logging }}
|
||||
|
||||
## To view firewall rules:
|
||||
sudo ufw status verbose
|
||||
|
||||
## To modify rules:
|
||||
Edit inventory variables and re-run playbook
|
||||
mode: '0600'
|
||||
|
||||
- name: Display VPN-only configuration
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "VPN-Only Mode: ENABLED"
|
||||
- "Management access allowed from: {{ management_allowed_sources | join(', ') }}"
|
||||
- "Management ports are ONLY accessible from allowed sources"
|
||||
- "Public ports remain accessible from internet"
|
||||
- "Configuration saved to /root/firewall-config.txt"
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
# SSH Users Role - Default Variables
|
||||
|
||||
# Admin users to create
|
||||
# Each user will get:
|
||||
# - User account created
|
||||
# - Added to sudo group
|
||||
# - SSH key pair generated (if generate_keys: true)
|
||||
# - Authorized SSH keys configured
|
||||
# - Shell set to /bin/bash
|
||||
|
||||
admin_users: []
|
||||
# Example:
|
||||
# admin_users:
|
||||
# - username: alice
|
||||
# comment: "Alice Admin"
|
||||
# groups: ["sudo", "adm"]
|
||||
# generate_keys: true # Generate SSH key pair on control node
|
||||
# authorized_keys: [] # List of public keys to add
|
||||
# shell: /bin/bash
|
||||
# state: present
|
||||
#
|
||||
# - username: bob
|
||||
# comment: "Bob Admin"
|
||||
# groups: ["sudo"]
|
||||
# generate_keys: false
|
||||
# authorized_keys:
|
||||
# - "ssh-ed25519 AAAAC3... bob@laptop"
|
||||
# shell: /bin/bash
|
||||
# state: present
|
||||
|
||||
# SSH key generation settings
|
||||
ssh_key_type: "ed25519"
|
||||
ssh_key_bits: 4096 # Only used for RSA
|
||||
ssh_key_comment: "{{ ansible_user }}@{{ inventory_hostname }}"
|
||||
|
||||
# Directory to store generated SSH keys on control node
|
||||
ssh_keys_local_dir: "{{ playbook_dir }}/../ssh-keys"
|
||||
|
||||
# Sudo configuration
|
||||
sudo_nopasswd: true # Allow sudo without password (for automation)
|
||||
sudo_timeout: 15 # Sudo timeout in minutes
|
||||
|
||||
# Root account restrictions
|
||||
disable_root_login: true # Disable root SSH login
|
||||
lock_root_account: false # Lock root account (prevents su/sudo to root)
|
||||
|
||||
# Password policies (CIS compliance)
|
||||
password_max_days: 365
|
||||
password_min_days: 1
|
||||
password_warn_age: 7
|
||||
password_inactive_days: 30
|
||||
|
||||
# Default umask
|
||||
default_umask: "027" # More restrictive than default 022
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
# SSH Users Role - Handlers
|
||||
|
||||
- name: restart sshd
|
||||
ansible.builtin.service:
|
||||
name: sshd
|
||||
state: restarted
|
||||
@@ -0,0 +1,24 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: ssh_users
|
||||
author: Security Infrastructure Team
|
||||
description: SSH user management with key generation, sudo configuration, and CIS compliance
|
||||
license: MIT
|
||||
min_ansible_version: "2.15"
|
||||
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- focal
|
||||
- jammy
|
||||
- noble
|
||||
|
||||
galaxy_tags:
|
||||
- security
|
||||
- ssh
|
||||
- users
|
||||
- cis
|
||||
- hardening
|
||||
- sudo
|
||||
|
||||
dependencies: []
|
||||
@@ -0,0 +1,63 @@
|
||||
---
|
||||
# Create Admin Users
|
||||
|
||||
- name: Create admin user accounts
|
||||
ansible.builtin.user:
|
||||
name: "{{ item.username }}"
|
||||
comment: "{{ item.comment | default(item.username) }}"
|
||||
groups: "{{ item.groups | default(['sudo']) }}"
|
||||
append: yes
|
||||
shell: "{{ item.shell | default('/bin/bash') }}"
|
||||
create_home: yes
|
||||
state: "{{ item.state | default('present') }}"
|
||||
loop: "{{ admin_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.username }}"
|
||||
|
||||
- name: Set password policies for admin users
|
||||
ansible.builtin.shell: |
|
||||
chage -M {{ password_max_days }} -m {{ password_min_days }} -W {{ password_warn_age }} -I {{ password_inactive_days }} {{ item.username }}
|
||||
loop: "{{ admin_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.username }}"
|
||||
when: item.state | default('present') == 'present'
|
||||
changed_when: false
|
||||
|
||||
- name: Configure authorized SSH keys for admin users
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ item.0.username }}"
|
||||
key: "{{ item.1 }}"
|
||||
state: present
|
||||
exclusive: no
|
||||
loop: "{{ admin_users | subelements('authorized_keys', skip_missing=True) }}"
|
||||
loop_control:
|
||||
label: "{{ item.0.username }}"
|
||||
when:
|
||||
- item.0.state | default('present') == 'present'
|
||||
- item.0.authorized_keys is defined
|
||||
- item.0.authorized_keys | length > 0
|
||||
|
||||
- name: Ensure .ssh directory exists for admin users
|
||||
ansible.builtin.file:
|
||||
path: "/home/{{ item.username }}/.ssh"
|
||||
state: directory
|
||||
owner: "{{ item.username }}"
|
||||
group: "{{ item.username }}"
|
||||
mode: '0700'
|
||||
loop: "{{ admin_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.username }}"
|
||||
when: item.state | default('present') == 'present'
|
||||
|
||||
- name: Set umask for admin users
|
||||
ansible.builtin.lineinfile:
|
||||
path: "/home/{{ item.username }}/.bashrc"
|
||||
line: "umask {{ default_umask }}"
|
||||
create: yes
|
||||
owner: "{{ item.username }}"
|
||||
group: "{{ item.username }}"
|
||||
mode: '0644'
|
||||
loop: "{{ admin_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.username }}"
|
||||
when: item.state | default('present') == 'present'
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
# Generate SSH Keys for Admin Users
|
||||
|
||||
- name: Create local SSH keys directory on control node
|
||||
ansible.builtin.file:
|
||||
path: "{{ ssh_keys_local_dir }}/{{ inventory_hostname }}"
|
||||
state: directory
|
||||
mode: '0700'
|
||||
delegate_to: localhost
|
||||
run_once: false
|
||||
|
||||
- name: Generate SSH key pairs on control node
|
||||
community.crypto.openssh_keypair:
|
||||
path: "{{ ssh_keys_local_dir }}/{{ inventory_hostname }}/{{ item.username }}_id_{{ ssh_key_type }}"
|
||||
type: "{{ ssh_key_type }}"
|
||||
size: "{{ ssh_key_bits if ssh_key_type == 'rsa' else omit }}"
|
||||
comment: "{{ item.username }}@{{ inventory_hostname }}"
|
||||
state: present
|
||||
loop: "{{ admin_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.username }}"
|
||||
when:
|
||||
- item.generate_keys | default(false)
|
||||
- item.state | default('present') == 'present'
|
||||
delegate_to: localhost
|
||||
run_once: false
|
||||
register: generated_keys
|
||||
|
||||
- name: Read generated public keys
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ ssh_keys_local_dir }}/{{ inventory_hostname }}/{{ item.username }}_id_{{ ssh_key_type }}.pub"
|
||||
loop: "{{ admin_users }}"
|
||||
loop_control:
|
||||
label: "{{ item.username }}"
|
||||
when:
|
||||
- item.generate_keys | default(false)
|
||||
- item.state | default('present') == 'present'
|
||||
delegate_to: localhost
|
||||
run_once: false
|
||||
register: public_keys
|
||||
|
||||
- name: Add generated public keys to authorized_keys
|
||||
ansible.posix.authorized_key:
|
||||
user: "{{ item.item.username }}"
|
||||
key: "{{ item.content | b64decode }}"
|
||||
state: present
|
||||
loop: "{{ public_keys.results }}"
|
||||
loop_control:
|
||||
label: "{{ item.item.username }}"
|
||||
when:
|
||||
- not item.skipped | default(false)
|
||||
- item.content is defined
|
||||
|
||||
- name: Create SSH key summary file
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ ssh_keys_local_dir }}/{{ inventory_hostname }}/README.md"
|
||||
content: |
|
||||
# SSH Keys for {{ inventory_hostname }}
|
||||
|
||||
Generated: {{ ansible_date_time.iso8601 }}
|
||||
|
||||
## Admin Users
|
||||
|
||||
{% for user in admin_users %}
|
||||
{% if user.generate_keys | default(false) %}
|
||||
### {{ user.username }}
|
||||
|
||||
- **Private Key**: `{{ user.username }}_id_{{ ssh_key_type }}`
|
||||
- **Public Key**: `{{ user.username }}_id_{{ ssh_key_type }}.pub`
|
||||
- **Comment**: {{ user.username }}@{{ inventory_hostname }}
|
||||
|
||||
**Usage**:
|
||||
```bash
|
||||
# Copy private key to your machine
|
||||
scp {{ ssh_keys_local_dir }}/{{ inventory_hostname }}/{{ user.username }}_id_{{ ssh_key_type }} ~/.ssh/
|
||||
|
||||
# Set correct permissions
|
||||
chmod 600 ~/.ssh/{{ user.username }}_id_{{ ssh_key_type }}
|
||||
|
||||
# SSH to server
|
||||
ssh -i ~/.ssh/{{ user.username }}_id_{{ ssh_key_type }} {{ user.username }}@{{ inventory_hostname }}
|
||||
```
|
||||
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
|
||||
## Security Notes
|
||||
|
||||
- Private keys are stored on the Ansible control node only
|
||||
- Public keys are deployed to the servers
|
||||
- Keep private keys secure and never commit to git
|
||||
- Rotate keys regularly (every 90 days recommended)
|
||||
|
||||
## Key Rotation
|
||||
|
||||
To rotate keys:
|
||||
1. Generate new keys by re-running the playbook
|
||||
2. Test new keys work
|
||||
3. Remove old keys from authorized_keys
|
||||
4. Delete old private keys securely
|
||||
mode: '0600'
|
||||
delegate_to: localhost
|
||||
run_once: false
|
||||
when: admin_users | selectattr('generate_keys', 'defined') | selectattr('generate_keys') | list | length > 0
|
||||
@@ -0,0 +1,31 @@
|
||||
---
|
||||
# SSH Users Role - Main Tasks
|
||||
|
||||
- name: Include user creation tasks
|
||||
ansible.builtin.include_tasks: create_users.yml
|
||||
when: admin_users | length > 0
|
||||
|
||||
- name: Include SSH key generation tasks
|
||||
ansible.builtin.include_tasks: generate_keys.yml
|
||||
when: admin_users | length > 0
|
||||
|
||||
- name: Include sudo configuration tasks
|
||||
ansible.builtin.include_tasks: sudo.yml
|
||||
|
||||
- name: Include root restrictions tasks
|
||||
ansible.builtin.include_tasks: root_restrictions.yml
|
||||
|
||||
- name: Include password policy tasks
|
||||
ansible.builtin.include_tasks: password_policy.yml
|
||||
|
||||
- name: Display user management summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "========================================="
|
||||
- "SSH User Management Complete"
|
||||
- "========================================="
|
||||
- "Admin users created: {{ admin_users | map(attribute='username') | list | join(', ') }}"
|
||||
- "Root SSH login: {{ 'DISABLED' if disable_root_login else 'ENABLED' }}"
|
||||
- "Sudo without password: {{ 'ENABLED' if sudo_nopasswd else 'DISABLED' }}"
|
||||
- "SSH keys saved to: {{ ssh_keys_local_dir }}"
|
||||
- "========================================="
|
||||
@@ -0,0 +1,91 @@
|
||||
---
|
||||
# Password Policy Configuration (CIS 5.4.x, 5.5.x)
|
||||
|
||||
- name: Set default password expiration (CIS 5.4.1)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^PASS_MAX_DAYS'
|
||||
line: "PASS_MAX_DAYS {{ password_max_days }}"
|
||||
state: present
|
||||
|
||||
- name: Set minimum password change days (CIS 5.4.2)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^PASS_MIN_DAYS'
|
||||
line: "PASS_MIN_DAYS {{ password_min_days }}"
|
||||
state: present
|
||||
|
||||
- name: Set password expiration warning (CIS 5.4.3)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^PASS_WARN_AGE'
|
||||
line: "PASS_WARN_AGE {{ password_warn_age }}"
|
||||
state: present
|
||||
|
||||
- name: Set default umask (CIS 5.4.4)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/login.defs
|
||||
regexp: '^UMASK'
|
||||
line: "UMASK {{ default_umask }}"
|
||||
state: present
|
||||
|
||||
- name: Configure PAM password quality requirements (CIS 5.5.1)
|
||||
ansible.builtin.apt:
|
||||
name: libpam-pwquality
|
||||
state: present
|
||||
|
||||
- name: Set password quality requirements
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/security/pwquality.conf
|
||||
regexp: "^{{ item.key }}"
|
||||
line: "{{ item.key }} = {{ item.value }}"
|
||||
state: present
|
||||
loop:
|
||||
- { key: 'minlen', value: '14' }
|
||||
- { key: 'dcredit', value: '-1' } # At least 1 digit
|
||||
- { key: 'ucredit', value: '-1' } # At least 1 uppercase
|
||||
- { key: 'lcredit', value: '-1' } # At least 1 lowercase
|
||||
- { key: 'ocredit', value: '-1' } # At least 1 special char
|
||||
- { key: 'minclass', value: '4' } # All 4 character classes
|
||||
|
||||
- name: Configure account lockout (CIS 5.5.2)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/common-auth
|
||||
line: "auth required pam_faillock.so preauth audit silent deny=5 unlock_time=900"
|
||||
insertbefore: '^auth\s+\[success=1 default=ignore\]\s+pam_unix.so'
|
||||
state: present
|
||||
|
||||
- name: Configure account lockout (part 2)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/common-auth
|
||||
line: "auth [default=die] pam_faillock.so authfail audit deny=5 unlock_time=900"
|
||||
insertafter: '^auth\s+\[success=1 default=ignore\]\s+pam_unix.so'
|
||||
state: present
|
||||
|
||||
- name: Configure account lockout (part 3)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/common-auth
|
||||
line: "auth sufficient pam_faillock.so authsucc audit deny=5 unlock_time=900"
|
||||
insertafter: '^auth\s+\[default=die\]\s+pam_faillock.so'
|
||||
state: present
|
||||
|
||||
- name: Configure password reuse prevention (CIS 5.5.3)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/common-password
|
||||
regexp: '^password\s+\[success=1 default=ignore\]\s+pam_unix.so'
|
||||
line: "password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass yescrypt remember=5"
|
||||
state: present
|
||||
|
||||
- name: Ensure password hashing algorithm is yescrypt (CIS 5.5.4)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/pam.d/common-password
|
||||
regexp: '^password\s+\[success=1 default=ignore\]\s+pam_unix.so'
|
||||
line: "password [success=1 default=ignore] pam_unix.so obscure use_authtok try_first_pass yescrypt"
|
||||
state: present
|
||||
|
||||
- name: Set inactive password lock (CIS 5.4.4)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/default/useradd
|
||||
regexp: '^INACTIVE='
|
||||
line: "INACTIVE={{ password_inactive_days }}"
|
||||
state: present
|
||||
@@ -0,0 +1,38 @@
|
||||
---
|
||||
# Root Account Restrictions
|
||||
|
||||
- name: Disable root SSH login
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/ssh/sshd_config
|
||||
regexp: '^PermitRootLogin'
|
||||
line: 'PermitRootLogin no'
|
||||
state: present
|
||||
validate: '/usr/sbin/sshd -t -f %s'
|
||||
when: disable_root_login
|
||||
notify: restart sshd
|
||||
|
||||
- name: Lock root account
|
||||
ansible.builtin.user:
|
||||
name: root
|
||||
password_lock: yes
|
||||
when: lock_root_account
|
||||
|
||||
- name: Ensure root group is GID 0 (CIS 5.4.5)
|
||||
ansible.builtin.group:
|
||||
name: root
|
||||
gid: 0
|
||||
state: present
|
||||
|
||||
- name: Verify root is the only UID 0 account
|
||||
ansible.builtin.shell: |
|
||||
awk -F: '($3 == 0) { print $1 }' /etc/passwd
|
||||
register: uid_zero_accounts
|
||||
changed_when: false
|
||||
failed_when: uid_zero_accounts.stdout_lines | length > 1
|
||||
|
||||
- name: Display root restrictions status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "Root SSH login: {{ 'DISABLED' if disable_root_login else 'ENABLED' }}"
|
||||
- "Root account: {{ 'LOCKED' if lock_root_account else 'UNLOCKED' }}"
|
||||
- "UID 0 accounts: {{ uid_zero_accounts.stdout_lines | join(', ') }}"
|
||||
@@ -0,0 +1,76 @@
|
||||
---
|
||||
# Sudo Configuration (CIS 5.3.x)
|
||||
|
||||
- name: Ensure sudo is installed
|
||||
ansible.builtin.apt:
|
||||
name: sudo
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Configure sudo to use pty (CIS 5.3.2)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/cis-hardening
|
||||
line: "Defaults use_pty"
|
||||
create: yes
|
||||
mode: '0440'
|
||||
validate: '/usr/sbin/visudo -cf %s'
|
||||
|
||||
- name: Configure sudo logfile (CIS 5.3.3)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/cis-hardening
|
||||
line: "Defaults logfile=\"/var/log/sudo.log\""
|
||||
create: yes
|
||||
mode: '0440'
|
||||
validate: '/usr/sbin/visudo -cf %s'
|
||||
|
||||
- name: Configure sudo timeout
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/cis-hardening
|
||||
line: "Defaults timestamp_timeout={{ sudo_timeout }}"
|
||||
create: yes
|
||||
mode: '0440'
|
||||
validate: '/usr/sbin/visudo -cf %s'
|
||||
|
||||
- name: Configure sudo password requirement (CIS 5.3.4)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/cis-hardening
|
||||
line: "Defaults !authenticate"
|
||||
state: "{{ 'present' if sudo_nopasswd else 'absent' }}"
|
||||
create: yes
|
||||
mode: '0440'
|
||||
validate: '/usr/sbin/visudo -cf %s'
|
||||
|
||||
- name: Allow sudo group to use sudo
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/sudoers.d/sudo-group
|
||||
line: "%sudo ALL=(ALL:ALL) {{ 'NOPASSWD:' if sudo_nopasswd else '' }}ALL"
|
||||
create: yes
|
||||
mode: '0440'
|
||||
validate: '/usr/sbin/visudo -cf %s'
|
||||
|
||||
- name: Create sudo log file
|
||||
ansible.builtin.file:
|
||||
path: /var/log/sudo.log
|
||||
state: touch
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
modification_time: preserve
|
||||
access_time: preserve
|
||||
|
||||
- name: Configure sudo log rotation
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/logrotate.d/sudo
|
||||
content: |
|
||||
/var/log/sudo.log {
|
||||
weekly
|
||||
rotate 4
|
||||
compress
|
||||
delaycompress
|
||||
missingok
|
||||
notifempty
|
||||
create 0600 root root
|
||||
}
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
@@ -0,0 +1,128 @@
|
||||
---
|
||||
# System Hardening Role - Default Variables
|
||||
|
||||
# SSH Configuration
|
||||
ssh_port: 22
|
||||
ssh_permit_root_login: "no"
|
||||
ssh_password_authentication: "no"
|
||||
ssh_pubkey_authentication: "yes"
|
||||
ssh_challenge_response_auth: "no"
|
||||
ssh_x11_forwarding: "no"
|
||||
ssh_max_auth_tries: 3
|
||||
ssh_client_alive_interval: 300
|
||||
ssh_client_alive_count_max: 2
|
||||
ssh_allowed_users: [] # List of users allowed to SSH
|
||||
ssh_listen_address: "0.0.0.0"
|
||||
|
||||
# Strong SSH ciphers and algorithms
|
||||
ssh_ciphers:
|
||||
- "chacha20-poly1305@openssh.com"
|
||||
- "aes256-gcm@openssh.com"
|
||||
- "aes128-gcm@openssh.com"
|
||||
ssh_macs:
|
||||
- "hmac-sha2-512-etm@openssh.com"
|
||||
- "hmac-sha2-256-etm@openssh.com"
|
||||
ssh_kex_algorithms:
|
||||
- "curve25519-sha256"
|
||||
- "curve25519-sha256@libssh.org"
|
||||
- "diffie-hellman-group-exchange-sha256"
|
||||
|
||||
# System packages
|
||||
hardening_install_packages:
|
||||
- ufw
|
||||
- fail2ban
|
||||
- unattended-upgrades
|
||||
- apt-listchanges
|
||||
- auditd
|
||||
- aide
|
||||
- rkhunter
|
||||
- lynis
|
||||
|
||||
hardening_remove_packages:
|
||||
- telnet
|
||||
- rsh-client
|
||||
- rsh-redone-client
|
||||
|
||||
# Automatic security updates
|
||||
unattended_upgrades_enabled: true
|
||||
unattended_upgrades_auto_reboot: false
|
||||
unattended_upgrades_auto_reboot_time: "03:00"
|
||||
|
||||
# Fail2ban configuration
|
||||
fail2ban_enabled: true
|
||||
fail2ban_bantime: 3600
|
||||
fail2ban_findtime: 600
|
||||
fail2ban_maxretry: 5
|
||||
fail2ban_destemail: "root@localhost"
|
||||
|
||||
# Sysctl hardening
|
||||
sysctl_config:
|
||||
# IP Forwarding (required for VPN)
|
||||
net.ipv4.ip_forward: 1
|
||||
|
||||
# Disable IPv6 (optional, set to 0 to enable)
|
||||
net.ipv6.conf.all.disable_ipv6: 1
|
||||
net.ipv6.conf.default.disable_ipv6: 1
|
||||
|
||||
# Protect against SYN flood attacks
|
||||
net.ipv4.tcp_syncookies: 1
|
||||
net.ipv4.tcp_syn_retries: 2
|
||||
net.ipv4.tcp_synack_retries: 2
|
||||
net.ipv4.tcp_max_syn_backlog: 4096
|
||||
|
||||
# Protect against IP spoofing
|
||||
net.ipv4.conf.all.rp_filter: 1
|
||||
net.ipv4.conf.default.rp_filter: 1
|
||||
|
||||
# Ignore ICMP redirects
|
||||
net.ipv4.conf.all.accept_redirects: 0
|
||||
net.ipv4.conf.default.accept_redirects: 0
|
||||
net.ipv4.conf.all.secure_redirects: 0
|
||||
net.ipv4.conf.default.secure_redirects: 0
|
||||
|
||||
# Do not send ICMP redirects
|
||||
net.ipv4.conf.all.send_redirects: 0
|
||||
net.ipv4.conf.default.send_redirects: 0
|
||||
|
||||
# Ignore ICMP ping requests
|
||||
net.ipv4.icmp_echo_ignore_all: 0
|
||||
net.ipv4.icmp_echo_ignore_broadcasts: 1
|
||||
|
||||
# Ignore bogus ICMP error responses
|
||||
net.ipv4.icmp_ignore_bogus_error_responses: 1
|
||||
|
||||
# Log martian packets
|
||||
net.ipv4.conf.all.log_martians: 1
|
||||
net.ipv4.conf.default.log_martians: 1
|
||||
|
||||
# Disable source packet routing
|
||||
net.ipv4.conf.all.accept_source_route: 0
|
||||
net.ipv4.conf.default.accept_source_route: 0
|
||||
|
||||
# Increase system file descriptor limit
|
||||
fs.file-max: 65535
|
||||
|
||||
# Protect against time-wait assassination
|
||||
net.ipv4.tcp_rfc1337: 1
|
||||
|
||||
# Kernel hardening
|
||||
kernel.dmesg_restrict: 1
|
||||
kernel.kptr_restrict: 2
|
||||
kernel.yama.ptrace_scope: 1
|
||||
|
||||
# Auditd configuration
|
||||
auditd_enabled: true
|
||||
auditd_rules:
|
||||
- "-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 actions"
|
||||
- "-w /var/log/auth.log -p wa -k auth"
|
||||
- "-w /var/log/faillog -p wa -k logins"
|
||||
- "-w /etc/ssh/sshd_config -p wa -k sshd"
|
||||
|
||||
# Timezone
|
||||
system_timezone: "UTC"
|
||||
|
||||
# Hostname
|
||||
system_hostname: "" # Leave empty to keep current hostname
|
||||
@@ -0,0 +1,19 @@
|
||||
---
|
||||
# CIS-Specific Variables for System Hardening
|
||||
|
||||
# AppArmor (CIS 1.3.x)
|
||||
apparmor_enabled: true
|
||||
apparmor_enforce_all: true
|
||||
|
||||
# Auditd (CIS 4.1.x)
|
||||
auditd_enabled: true
|
||||
auditd_max_log_file: 8 # MB
|
||||
|
||||
# Network (CIS 3.x)
|
||||
disable_ipv6: true # Set to false if IPv6 is needed
|
||||
|
||||
# Core dumps (CIS 1.5.1)
|
||||
disable_core_dumps: true
|
||||
|
||||
# Uncommon protocols (CIS 3.3.x)
|
||||
disable_uncommon_protocols: true
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
# System Hardening Role - Handlers
|
||||
|
||||
- name: restart sshd
|
||||
ansible.builtin.systemd:
|
||||
name: sshd
|
||||
state: restarted
|
||||
|
||||
- name: restart fail2ban
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: restarted
|
||||
|
||||
- name: restart auditd
|
||||
ansible.builtin.command: service auditd restart
|
||||
# Note: auditd requires special restart command
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: system_hardening
|
||||
author: Security Infrastructure Team
|
||||
description: Comprehensive Ubuntu 24.04 server hardening following security best practices
|
||||
company: Your Organization
|
||||
license: MIT
|
||||
min_ansible_version: "2.15"
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- noble # 24.04
|
||||
- jammy # 22.04
|
||||
galaxy_tags:
|
||||
- security
|
||||
- hardening
|
||||
- ssh
|
||||
- firewall
|
||||
- ubuntu
|
||||
|
||||
dependencies: []
|
||||
|
||||
collections:
|
||||
- community.general
|
||||
- ansible.posix
|
||||
@@ -0,0 +1,42 @@
|
||||
---
|
||||
# AppArmor Configuration (CIS 1.3.x)
|
||||
|
||||
- name: Install AppArmor packages (CIS 1.3.1)
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- apparmor
|
||||
- apparmor-utils
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Enable AppArmor service (CIS 1.3.2)
|
||||
ansible.builtin.service:
|
||||
name: apparmor
|
||||
state: started
|
||||
enabled: yes
|
||||
|
||||
- name: Check AppArmor status
|
||||
ansible.builtin.command: aa-status --json
|
||||
register: apparmor_status
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Parse AppArmor status
|
||||
ansible.builtin.set_fact:
|
||||
apparmor_json: "{{ apparmor_status.stdout | from_json }}"
|
||||
when: apparmor_status.rc == 0
|
||||
|
||||
- name: Set all AppArmor profiles to enforce mode (CIS 1.3.3)
|
||||
ansible.builtin.command: aa-enforce /etc/apparmor.d/*
|
||||
register: apparmor_enforce
|
||||
changed_when: "'Setting' in apparmor_enforce.stdout"
|
||||
failed_when: false
|
||||
when: apparmor_enforce_all | default(true)
|
||||
|
||||
- name: Display AppArmor status
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "AppArmor status: {{ apparmor_json.apparmor if apparmor_json is defined else 'unknown' }}"
|
||||
- "Profiles loaded: {{ apparmor_json.profiles | length if apparmor_json is defined and apparmor_json.profiles is defined else 0 }}"
|
||||
- "Profiles in enforce mode: {{ apparmor_json.profiles | selectattr('mode', 'equalto', 'enforce') | list | length if apparmor_json is defined and apparmor_json.profiles is defined else 0 }}"
|
||||
when: apparmor_json is defined
|
||||
@@ -0,0 +1,57 @@
|
||||
---
|
||||
# Auditd Configuration Tasks (CIS 4.1.x)
|
||||
|
||||
- name: Ensure auditd is installed (CIS 4.1.1)
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- auditd
|
||||
- audispd-plugins
|
||||
state: present
|
||||
|
||||
- name: Configure auditd max log file size (CIS 4.1.3)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/audit/auditd.conf
|
||||
regexp: '^max_log_file\s*='
|
||||
line: "max_log_file = {{ auditd_max_log_file }}"
|
||||
state: present
|
||||
|
||||
- name: Configure auditd log retention (CIS 4.1.4)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/audit/auditd.conf
|
||||
regexp: '^max_log_file_action\s*='
|
||||
line: "max_log_file_action = keep_logs"
|
||||
state: present
|
||||
|
||||
- name: Configure auditd space left action (CIS 4.1.5)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/audit/auditd.conf
|
||||
regexp: '^space_left_action\s*='
|
||||
line: "space_left_action = email"
|
||||
state: present
|
||||
|
||||
- name: Configure auditd admin space left action
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/audit/auditd.conf
|
||||
regexp: '^admin_space_left_action\s*='
|
||||
line: "admin_space_left_action = halt"
|
||||
state: present
|
||||
|
||||
- name: Deploy CIS-compliant audit rules
|
||||
ansible.builtin.template:
|
||||
src: audit.rules.j2
|
||||
dest: /etc/audit/rules.d/cis.rules
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0640'
|
||||
notify: restart auditd
|
||||
|
||||
- name: Load audit rules
|
||||
ansible.builtin.command: augenrules --load
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Ensure auditd is started and enabled (CIS 4.1.2)
|
||||
ansible.builtin.systemd:
|
||||
name: auditd
|
||||
state: started
|
||||
enabled: yes
|
||||
@@ -0,0 +1,39 @@
|
||||
---
|
||||
# Restrict Core Dumps (CIS 1.5.1)
|
||||
|
||||
- name: Disable core dumps via limits.conf
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/security/limits.conf
|
||||
line: "* hard core 0"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Disable core dumps via sysctl (already done in sysctl_cis.yml)
|
||||
ansible.posix.sysctl:
|
||||
name: fs.suid_dumpable
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
- name: Disable coredump in systemd
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/systemd/coredump.conf
|
||||
regexp: '^Storage='
|
||||
line: "Storage=none"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Disable coredump processing
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/systemd/coredump.conf
|
||||
regexp: '^ProcessSizeMax='
|
||||
line: "ProcessSizeMax=0"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
@@ -0,0 +1,49 @@
|
||||
---
|
||||
# Disable Uncommon Network Protocols (CIS 3.3.x)
|
||||
|
||||
- name: Disable DCCP protocol (CIS 3.3.1)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/modprobe.d/cis.conf
|
||||
line: "install dccp /bin/true"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Disable SCTP protocol (CIS 3.3.2)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/modprobe.d/cis.conf
|
||||
line: "install sctp /bin/true"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Disable RDS protocol (CIS 3.3.3)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/modprobe.d/cis.conf
|
||||
line: "install rds /bin/true"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Disable TIPC protocol (CIS 3.3.4)
|
||||
ansible.builtin.lineinfile:
|
||||
path: /etc/modprobe.d/cis.conf
|
||||
line: "install tipc /bin/true"
|
||||
create: yes
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Unload uncommon protocols if loaded
|
||||
community.general.modprobe:
|
||||
name: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- dccp
|
||||
- sctp
|
||||
- rds
|
||||
- tipc
|
||||
failed_when: false
|
||||
@@ -0,0 +1,22 @@
|
||||
---
|
||||
# Fail2ban Configuration Tasks
|
||||
|
||||
- name: Ensure fail2ban is installed
|
||||
ansible.builtin.apt:
|
||||
name: fail2ban
|
||||
state: present
|
||||
|
||||
- name: Configure fail2ban
|
||||
ansible.builtin.template:
|
||||
src: fail2ban.local.j2
|
||||
dest: /etc/fail2ban/jail.local
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
notify: restart fail2ban
|
||||
|
||||
- name: Ensure fail2ban is started and enabled
|
||||
ansible.builtin.systemd:
|
||||
name: fail2ban
|
||||
state: started
|
||||
enabled: yes
|
||||
@@ -0,0 +1,121 @@
|
||||
---
|
||||
# System Hardening Role - Main Tasks
|
||||
|
||||
- name: Set timezone
|
||||
community.general.timezone:
|
||||
name: "{{ system_timezone }}"
|
||||
when: system_timezone is defined and system_timezone != ""
|
||||
|
||||
- name: Set hostname
|
||||
ansible.builtin.hostname:
|
||||
name: "{{ system_hostname }}"
|
||||
when: system_hostname is defined and system_hostname != ""
|
||||
|
||||
- name: Update apt cache
|
||||
ansible.builtin.apt:
|
||||
update_cache: yes
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Upgrade all packages
|
||||
ansible.builtin.apt:
|
||||
upgrade: dist
|
||||
autoremove: yes
|
||||
autoclean: yes
|
||||
|
||||
- name: Install security packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ hardening_install_packages }}"
|
||||
state: present
|
||||
|
||||
- name: Remove insecure packages
|
||||
ansible.builtin.apt:
|
||||
name: "{{ hardening_remove_packages }}"
|
||||
state: absent
|
||||
purge: yes
|
||||
|
||||
- name: Configure SSH hardening
|
||||
ansible.builtin.include_tasks: ssh.yml
|
||||
|
||||
- name: Configure sysctl parameters (basic)
|
||||
ansible.builtin.include_tasks: sysctl.yml
|
||||
|
||||
- name: Configure CIS-compliant sysctl parameters
|
||||
ansible.builtin.include_tasks: sysctl_cis.yml
|
||||
|
||||
- name: Configure AppArmor (CIS 1.3.x)
|
||||
ansible.builtin.include_tasks: apparmor.yml
|
||||
when: apparmor_enabled | default(true)
|
||||
|
||||
- name: Configure fail2ban
|
||||
ansible.builtin.include_tasks: fail2ban.yml
|
||||
when: fail2ban_enabled | bool
|
||||
|
||||
- name: Configure auditd (CIS 4.1.x)
|
||||
ansible.builtin.include_tasks: audit.yml
|
||||
when: auditd_enabled | bool
|
||||
|
||||
- name: Configure unattended upgrades
|
||||
ansible.builtin.include_tasks: unattended_upgrades.yml
|
||||
when: unattended_upgrades_enabled | bool
|
||||
|
||||
- name: Disable uncommon network protocols (CIS 3.3.x)
|
||||
ansible.builtin.include_tasks: disable_protocols.yml
|
||||
|
||||
- name: Configure core dumps restriction (CIS 1.5.1)
|
||||
ansible.builtin.include_tasks: core_dumps.yml
|
||||
|
||||
- name: Disable unnecessary services
|
||||
ansible.builtin.systemd:
|
||||
name: "{{ item }}"
|
||||
state: stopped
|
||||
enabled: no
|
||||
loop:
|
||||
- avahi-daemon
|
||||
- cups
|
||||
- isc-dhcp-server
|
||||
- isc-dhcp-server6
|
||||
- rpcbind
|
||||
- rsync
|
||||
- snmpd
|
||||
failed_when: false # Don't fail if service doesn't exist
|
||||
|
||||
- name: Set secure file permissions (CIS 6.1.x)
|
||||
ansible.builtin.file:
|
||||
path: "{{ item.path }}"
|
||||
mode: "{{ item.mode }}"
|
||||
loop:
|
||||
- { path: '/etc/passwd', mode: '0644' }
|
||||
- { path: '/etc/shadow', mode: '0600' }
|
||||
- { path: '/etc/group', mode: '0644' }
|
||||
- { path: '/etc/gshadow', mode: '0600' }
|
||||
- { path: '/etc/ssh/sshd_config', mode: '0600' }
|
||||
|
||||
- name: Create security banners (CIS 1.4.x)
|
||||
ansible.builtin.copy:
|
||||
dest: "{{ item }}"
|
||||
content: |
|
||||
**************************************************************************
|
||||
* *
|
||||
* WARNING: Unauthorized access to this system is forbidden and will *
|
||||
* be prosecuted by law. By accessing this system, you agree that your *
|
||||
* actions may be monitored if unauthorized usage is suspected. *
|
||||
* *
|
||||
**************************************************************************
|
||||
mode: '0644'
|
||||
loop:
|
||||
- /etc/issue
|
||||
- /etc/issue.net
|
||||
- /etc/motd
|
||||
|
||||
- name: Display hardening summary
|
||||
ansible.builtin.debug:
|
||||
msg:
|
||||
- "========================================="
|
||||
- "System Hardening Complete"
|
||||
- "========================================="
|
||||
- "CIS Level 1 controls applied"
|
||||
- "AppArmor: {{ 'ENABLED' if apparmor_enabled | default(true) else 'DISABLED' }}"
|
||||
- "Auditd: {{ 'ENABLED' if auditd_enabled else 'DISABLED' }}"
|
||||
- "Fail2ban: {{ 'ENABLED' if fail2ban_enabled else 'DISABLED' }}"
|
||||
- "Unattended upgrades: {{ 'ENABLED' if unattended_upgrades_enabled else 'DISABLED' }}"
|
||||
- "========================================="
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
# SSH Hardening Tasks
|
||||
|
||||
- name: Backup original sshd_config
|
||||
ansible.builtin.copy:
|
||||
src: /etc/ssh/sshd_config
|
||||
dest: /etc/ssh/sshd_config.backup
|
||||
remote_src: yes
|
||||
force: no
|
||||
|
||||
- name: Configure SSH daemon
|
||||
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
|
||||
|
||||
- name: Ensure SSH directory exists for root
|
||||
ansible.builtin.file:
|
||||
path: /root/.ssh
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0700'
|
||||
|
||||
- name: Generate strong SSH host keys
|
||||
ansible.builtin.command: ssh-keygen -A
|
||||
args:
|
||||
creates: /etc/ssh/ssh_host_ed25519_key
|
||||
|
||||
- name: Remove weak SSH host keys
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: absent
|
||||
loop:
|
||||
- /etc/ssh/ssh_host_dsa_key
|
||||
- /etc/ssh/ssh_host_dsa_key.pub
|
||||
- /etc/ssh/ssh_host_ecdsa_key
|
||||
- /etc/ssh/ssh_host_ecdsa_key.pub
|
||||
|
||||
- name: Set permissions on SSH host keys
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
loop:
|
||||
- /etc/ssh/ssh_host_rsa_key
|
||||
- /etc/ssh/ssh_host_ed25519_key
|
||||
when: ansible_facts['os_family'] == "Debian"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
# Sysctl Hardening Tasks
|
||||
|
||||
- name: Apply sysctl hardening parameters
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item.key }}"
|
||||
value: "{{ item.value }}"
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop: "{{ sysctl_config | dict2items }}"
|
||||
when: sysctl_config is defined
|
||||
@@ -0,0 +1,157 @@
|
||||
---
|
||||
# CIS-Compliant Sysctl Parameters
|
||||
|
||||
# CIS 3.1.1 - Disable IP forwarding (unless VPN server needs it)
|
||||
- name: Disable IPv4 forwarding
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.ip_forward
|
||||
value: '1' # Enabled for VPN server
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
# CIS 3.1.2 - Disable packet redirect sending
|
||||
- name: Disable send packet redirects
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv4.conf.all.send_redirects
|
||||
- net.ipv4.conf.default.send_redirects
|
||||
|
||||
# CIS 3.2.1 - Do not accept source routed packets
|
||||
- name: Disable source routed packets
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv4.conf.all.accept_source_route
|
||||
- net.ipv4.conf.default.accept_source_route
|
||||
- net.ipv6.conf.all.accept_source_route
|
||||
- net.ipv6.conf.default.accept_source_route
|
||||
|
||||
# CIS 3.2.2 - Do not accept ICMP redirects
|
||||
- name: Disable ICMP redirects
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv4.conf.all.accept_redirects
|
||||
- net.ipv4.conf.default.accept_redirects
|
||||
- net.ipv6.conf.all.accept_redirects
|
||||
- net.ipv6.conf.default.accept_redirects
|
||||
|
||||
# CIS 3.2.3 - Do not accept secure ICMP redirects
|
||||
- name: Disable secure ICMP redirects
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv4.conf.all.secure_redirects
|
||||
- net.ipv4.conf.default.secure_redirects
|
||||
|
||||
# CIS 3.2.4 - Log suspicious packets
|
||||
- name: Enable suspicious packet logging
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv4.conf.all.log_martians
|
||||
- net.ipv4.conf.default.log_martians
|
||||
|
||||
# CIS 3.2.5 - Ignore broadcast ICMP requests
|
||||
- name: Ignore ICMP broadcast requests
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.icmp_echo_ignore_broadcasts
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
# CIS 3.2.6 - Ignore bogus ICMP responses
|
||||
- name: Ignore bogus ICMP error responses
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.icmp_ignore_bogus_error_responses
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
# CIS 3.2.7 - Enable reverse path filtering
|
||||
- name: Enable reverse path filtering
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv4.conf.all.rp_filter
|
||||
- net.ipv4.conf.default.rp_filter
|
||||
|
||||
# CIS 3.2.8 - Enable TCP SYN cookies
|
||||
- name: Enable TCP SYN cookies
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.tcp_syncookies
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
# CIS 3.2.9 - Do not accept IPv6 router advertisements
|
||||
- name: Disable IPv6 router advertisements
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv6.conf.all.accept_ra
|
||||
- net.ipv6.conf.default.accept_ra
|
||||
|
||||
# Additional hardening
|
||||
- name: Disable IPv6 (if not used)
|
||||
ansible.posix.sysctl:
|
||||
name: "{{ item }}"
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
loop:
|
||||
- net.ipv6.conf.all.disable_ipv6
|
||||
- net.ipv6.conf.default.disable_ipv6
|
||||
when: disable_ipv6 | default(false)
|
||||
|
||||
# CIS 1.5.2 - Enable ASLR
|
||||
- name: Enable address space layout randomization
|
||||
ansible.posix.sysctl:
|
||||
name: kernel.randomize_va_space
|
||||
value: '2'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
# CIS 1.5.1 - Restrict core dumps
|
||||
- name: Restrict core dumps
|
||||
ansible.posix.sysctl:
|
||||
name: fs.suid_dumpable
|
||||
value: '0'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
# Unattended Upgrades Configuration Tasks
|
||||
|
||||
- name: Ensure unattended-upgrades is installed
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- unattended-upgrades
|
||||
- apt-listchanges
|
||||
state: present
|
||||
|
||||
- name: Configure unattended-upgrades
|
||||
ansible.builtin.template:
|
||||
src: 50unattended-upgrades.j2
|
||||
dest: /etc/apt/apt.conf.d/50unattended-upgrades
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
|
||||
- name: Enable automatic updates
|
||||
ansible.builtin.template:
|
||||
src: 20auto-upgrades.j2
|
||||
dest: /etc/apt/apt.conf.d/20auto-upgrades
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
@@ -0,0 +1,4 @@
|
||||
APT::Periodic::Update-Package-Lists "1";
|
||||
APT::Periodic::Unattended-Upgrade "1";
|
||||
APT::Periodic::Download-Upgradeable-Packages "1";
|
||||
APT::Periodic::AutocleanInterval "7";
|
||||
@@ -0,0 +1,22 @@
|
||||
Unattended-Upgrade::Allowed-Origins {
|
||||
"${distro_id}:${distro_codename}";
|
||||
"${distro_id}:${distro_codename}-security";
|
||||
"${distro_id}ESMApps:${distro_codename}-apps-security";
|
||||
"${distro_id}ESM:${distro_codename}-infra-security";
|
||||
};
|
||||
|
||||
Unattended-Upgrade::Package-Blacklist {
|
||||
};
|
||||
|
||||
Unattended-Upgrade::DevRelease "false";
|
||||
Unattended-Upgrade::AutoFixInterruptedDpkg "true";
|
||||
Unattended-Upgrade::MinimalSteps "true";
|
||||
Unattended-Upgrade::InstallOnShutdown "false";
|
||||
Unattended-Upgrade::Mail "{{ fail2ban_destemail }}";
|
||||
Unattended-Upgrade::MailReport "on-change";
|
||||
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true";
|
||||
Unattended-Upgrade::Remove-New-Unused-Dependencies "true";
|
||||
Unattended-Upgrade::Remove-Unused-Dependencies "true";
|
||||
Unattended-Upgrade::Automatic-Reboot "{{ unattended_upgrades_auto_reboot | lower }}";
|
||||
Unattended-Upgrade::Automatic-Reboot-Time "{{ unattended_upgrades_auto_reboot_time }}";
|
||||
Unattended-Upgrade::SyslogEnable "true";
|
||||
@@ -0,0 +1,86 @@
|
||||
# CIS-Compliant Audit Rules for Ubuntu 24.04
|
||||
# Generated by Ansible - Do not edit manually
|
||||
|
||||
# Remove any existing rules
|
||||
-D
|
||||
|
||||
# Buffer Size
|
||||
-b 8192
|
||||
|
||||
# Failure Mode (0=silent 1=printk 2=panic)
|
||||
-f 1
|
||||
|
||||
# CIS 4.1.6 - Audit time changes
|
||||
-a always,exit -F arch=b64 -S adjtimex -S settimeofday -k time-change
|
||||
-a always,exit -F arch=b32 -S adjtimex -S settimeofday -S stime -k time-change
|
||||
-a always,exit -F arch=b64 -S clock_settime -k time-change
|
||||
-a always,exit -F arch=b32 -S clock_settime -k time-change
|
||||
-w /etc/localtime -p wa -k time-change
|
||||
|
||||
# CIS 4.1.7 - Audit user/group changes
|
||||
-w /etc/group -p wa -k identity
|
||||
-w /etc/passwd -p wa -k identity
|
||||
-w /etc/gshadow -p wa -k identity
|
||||
-w /etc/shadow -p wa -k identity
|
||||
-w /etc/security/opasswd -p wa -k identity
|
||||
|
||||
# CIS 4.1.8 - Audit network environment
|
||||
-a always,exit -F arch=b64 -S sethostname -S setdomainname -k system-locale
|
||||
-a always,exit -F arch=b32 -S sethostname -S setdomainname -k system-locale
|
||||
-w /etc/issue -p wa -k system-locale
|
||||
-w /etc/issue.net -p wa -k system-locale
|
||||
-w /etc/hosts -p wa -k system-locale
|
||||
-w /etc/network -p wa -k system-locale
|
||||
|
||||
# CIS 4.1.9 - Audit AppArmor changes
|
||||
-w /etc/apparmor/ -p wa -k MAC-policy
|
||||
-w /etc/apparmor.d/ -p wa -k MAC-policy
|
||||
|
||||
# CIS 4.1.10 - Audit login/logout events
|
||||
-w /var/log/faillog -p wa -k logins
|
||||
-w /var/log/lastlog -p wa -k logins
|
||||
-w /var/log/tallylog -p wa -k logins
|
||||
|
||||
# CIS 4.1.11 - Audit session initiation
|
||||
-w /var/run/utmp -p wa -k session
|
||||
-w /var/log/wtmp -p wa -k logins
|
||||
-w /var/log/btmp -p wa -k logins
|
||||
|
||||
# CIS 4.1.12 - Audit permission changes
|
||||
-a always,exit -F arch=b64 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-a always,exit -F arch=b32 -S chmod -S fchmod -S fchmodat -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-a always,exit -F arch=b64 -S chown -S fchown -S fchownat -S lchown -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-a always,exit -F arch=b32 -S chown -S fchown -S fchownat -S lchown -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-a always,exit -F arch=b64 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
-a always,exit -F arch=b32 -S setxattr -S lsetxattr -S fsetxattr -S removexattr -S lremovexattr -S fremovexattr -F auid>=1000 -F auid!=4294967295 -k perm_mod
|
||||
|
||||
# CIS 4.1.13 - Audit unsuccessful file access attempts
|
||||
-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access
|
||||
-a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EACCES -F auid>=1000 -F auid!=4294967295 -k access
|
||||
-a always,exit -F arch=b64 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access
|
||||
-a always,exit -F arch=b32 -S creat -S open -S openat -S truncate -S ftruncate -F exit=-EPERM -F auid>=1000 -F auid!=4294967295 -k access
|
||||
|
||||
# CIS 4.1.14 - Audit file deletion events
|
||||
-a always,exit -F arch=b64 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete
|
||||
-a always,exit -F arch=b32 -S unlink -S unlinkat -S rename -S renameat -F auid>=1000 -F auid!=4294967295 -k delete
|
||||
|
||||
# CIS 4.1.15 - Audit sudoers changes
|
||||
-w /etc/sudoers -p wa -k scope
|
||||
-w /etc/sudoers.d/ -p wa -k scope
|
||||
|
||||
# CIS 4.1.16 - Audit sudo usage
|
||||
-w /var/log/sudo.log -p wa -k actions
|
||||
|
||||
# CIS 4.1.17 - Audit kernel module loading/unloading
|
||||
-w /sbin/insmod -p x -k modules
|
||||
-w /sbin/rmmod -p x -k modules
|
||||
-w /sbin/modprobe -p x -k modules
|
||||
-a always,exit -F arch=b64 -S init_module -S delete_module -k modules
|
||||
|
||||
# Additional security-relevant events
|
||||
-w /etc/ssh/sshd_config -p wa -k sshd
|
||||
-w /etc/pam.d/ -p wa -k pam
|
||||
-w /etc/security/ -p wa -k security
|
||||
|
||||
# Make configuration immutable
|
||||
-e 2
|
||||
@@ -0,0 +1,14 @@
|
||||
[DEFAULT]
|
||||
bantime = {{ fail2ban_bantime }}
|
||||
findtime = {{ fail2ban_findtime }}
|
||||
maxretry = {{ fail2ban_maxretry }}
|
||||
destemail = {{ fail2ban_destemail }}
|
||||
sendername = Fail2Ban
|
||||
action = %(action_mwl)s
|
||||
|
||||
[sshd]
|
||||
enabled = true
|
||||
port = {{ ssh_port }}
|
||||
filter = sshd
|
||||
logpath = /var/log/auth.log
|
||||
maxretry = {{ fail2ban_maxretry }}
|
||||
@@ -0,0 +1,44 @@
|
||||
# Hardened SSH Configuration
|
||||
# Generated by Ansible - Do not edit manually
|
||||
|
||||
# Network
|
||||
Port {{ ssh_port }}
|
||||
AddressFamily inet
|
||||
ListenAddress {{ ssh_listen_address }}
|
||||
|
||||
# Authentication
|
||||
PermitRootLogin {{ ssh_permit_root_login }}
|
||||
PubkeyAuthentication {{ ssh_pubkey_authentication }}
|
||||
PasswordAuthentication {{ ssh_password_authentication }}
|
||||
ChallengeResponseAuthentication {{ ssh_challenge_response_auth }}
|
||||
UsePAM yes
|
||||
MaxAuthTries {{ ssh_max_auth_tries }}
|
||||
|
||||
{% if ssh_allowed_users | length > 0 %}
|
||||
AllowUsers {{ ssh_allowed_users | join(' ') }}
|
||||
{% endif %}
|
||||
|
||||
# Cryptography
|
||||
Ciphers {{ ssh_ciphers | join(',') }}
|
||||
MACs {{ ssh_macs | join(',') }}
|
||||
KexAlgorithms {{ ssh_kex_algorithms | join(',') }}
|
||||
|
||||
# Features
|
||||
X11Forwarding {{ ssh_x11_forwarding }}
|
||||
PrintMotd no
|
||||
PrintLastLog yes
|
||||
TCPKeepAlive yes
|
||||
PermitUserEnvironment no
|
||||
Compression no
|
||||
ClientAliveInterval {{ ssh_client_alive_interval }}
|
||||
ClientAliveCountMax {{ ssh_client_alive_count_max }}
|
||||
UseDNS no
|
||||
PermitTunnel no
|
||||
Banner /etc/issue.net
|
||||
|
||||
# Subsystems
|
||||
Subsystem sftp /usr/lib/openssh/sftp-server -f AUTHPRIV -l INFO
|
||||
|
||||
# Logging
|
||||
SyslogFacility AUTH
|
||||
LogLevel VERBOSE
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
# WireGuard Server Role - Default Variables
|
||||
|
||||
# WireGuard interface configuration
|
||||
wg_interface: wg0
|
||||
wg_port: 51820
|
||||
wg_network: "10.100.0.0/24"
|
||||
wg_server_ip: "10.100.0.1"
|
||||
|
||||
# DNS servers for VPN clients
|
||||
wg_dns_servers:
|
||||
- "1.1.1.1"
|
||||
- "1.0.0.1"
|
||||
|
||||
# WireGuard users/peers
|
||||
# Format:
|
||||
# wg_peers:
|
||||
# - name: user1
|
||||
# ip: 10.100.0.10
|
||||
# - name: user2
|
||||
# ip: 10.100.0.11
|
||||
wg_peers: []
|
||||
|
||||
# Automatic peer IP allocation
|
||||
wg_auto_allocate_ips: true
|
||||
wg_ip_start: 10 # Start allocating from 10.100.0.10
|
||||
|
||||
# Key management
|
||||
wg_keys_dir: "/etc/wireguard/keys"
|
||||
wg_config_dir: "/etc/wireguard"
|
||||
wg_client_configs_dir: "/root/wireguard-client-configs"
|
||||
|
||||
# Post-up and post-down rules for NAT
|
||||
wg_postup: "iptables -A FORWARD -i %i -j ACCEPT; iptables -A FORWARD -o %i -j ACCEPT; iptables -t nat -A POSTROUTING -o {{ ansible_default_ipv4.interface }} -j MASQUERADE"
|
||||
wg_postdown: "iptables -D FORWARD -i %i -j ACCEPT; iptables -D FORWARD -o %i -j ACCEPT; iptables -t nat -D POSTROUTING -o {{ ansible_default_ipv4.interface }} -j MASQUERADE"
|
||||
|
||||
# Keepalive
|
||||
wg_persistent_keepalive: 25
|
||||
|
||||
# MTU
|
||||
wg_mtu: 1420
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
# WireGuard Server Role - Handlers
|
||||
|
||||
- name: restart wireguard
|
||||
ansible.builtin.systemd:
|
||||
name: "wg-quick@{{ wg_interface }}"
|
||||
state: restarted
|
||||
@@ -0,0 +1,25 @@
|
||||
---
|
||||
galaxy_info:
|
||||
role_name: wireguard_server
|
||||
author: Security Infrastructure Team
|
||||
description: WireGuard VPN server installation and configuration
|
||||
company: Your Organization
|
||||
license: MIT
|
||||
min_ansible_version: "2.15"
|
||||
platforms:
|
||||
- name: Ubuntu
|
||||
versions:
|
||||
- noble # 24.04
|
||||
- jammy # 22.04
|
||||
galaxy_tags:
|
||||
- wireguard
|
||||
- vpn
|
||||
- security
|
||||
- networking
|
||||
|
||||
dependencies:
|
||||
- role: system_hardening
|
||||
|
||||
collections:
|
||||
- ansible.posix
|
||||
- ansible.utils
|
||||
@@ -0,0 +1,34 @@
|
||||
---
|
||||
# WireGuard Configuration Tasks
|
||||
|
||||
- name: Configure WireGuard server
|
||||
ansible.builtin.template:
|
||||
src: wg0.conf.j2
|
||||
dest: "{{ wg_config_dir }}/{{ wg_interface }}.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
notify: restart wireguard
|
||||
|
||||
- name: Enable IP forwarding (if not already enabled by sysctl)
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.ip_forward
|
||||
value: '1'
|
||||
state: present
|
||||
sysctl_set: yes
|
||||
reload: yes
|
||||
|
||||
- name: Enable WireGuard service
|
||||
ansible.builtin.systemd:
|
||||
name: "wg-quick@{{ wg_interface }}"
|
||||
enabled: yes
|
||||
state: started
|
||||
|
||||
- name: Get WireGuard service status
|
||||
ansible.builtin.systemd:
|
||||
name: "wg-quick@{{ wg_interface }}"
|
||||
register: wg_service_status
|
||||
|
||||
- name: Display WireGuard status
|
||||
ansible.builtin.debug:
|
||||
msg: "WireGuard service is {{ wg_service_status.status.ActiveState }}"
|
||||
@@ -0,0 +1,59 @@
|
||||
---
|
||||
# WireGuard Installation Tasks
|
||||
|
||||
- name: Install WireGuard
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- wireguard
|
||||
- wireguard-tools
|
||||
- qrencode
|
||||
state: present
|
||||
update_cache: yes
|
||||
|
||||
- name: Create WireGuard directories
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0700'
|
||||
loop:
|
||||
- "{{ wg_config_dir }}"
|
||||
- "{{ wg_keys_dir }}"
|
||||
- "{{ wg_client_configs_dir }}"
|
||||
|
||||
- name: Check if server private key exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ wg_keys_dir }}/server_private.key"
|
||||
register: server_private_key
|
||||
|
||||
- name: Generate server private key
|
||||
ansible.builtin.shell: wg genkey > {{ wg_keys_dir }}/server_private.key
|
||||
when: not server_private_key.stat.exists
|
||||
|
||||
- name: Set server private key permissions
|
||||
ansible.builtin.file:
|
||||
path: "{{ wg_keys_dir }}/server_private.key"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
|
||||
- name: Generate server public key
|
||||
ansible.builtin.shell: cat {{ wg_keys_dir }}/server_private.key | wg pubkey > {{ wg_keys_dir }}/server_public.key
|
||||
args:
|
||||
creates: "{{ wg_keys_dir }}/server_public.key"
|
||||
|
||||
- name: Read server private key
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ wg_keys_dir }}/server_private.key"
|
||||
register: server_private_key_content
|
||||
|
||||
- name: Read server public key
|
||||
ansible.builtin.slurp:
|
||||
src: "{{ wg_keys_dir }}/server_public.key"
|
||||
register: server_public_key_content
|
||||
|
||||
- name: Set server keys as facts
|
||||
ansible.builtin.set_fact:
|
||||
wg_server_private_key: "{{ server_private_key_content['content'] | b64decode | trim }}"
|
||||
wg_server_public_key: "{{ server_public_key_content['content'] | b64decode | trim }}"
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
# WireGuard Server Role - Main Tasks
|
||||
|
||||
- name: Include installation tasks
|
||||
ansible.builtin.include_tasks: install.yml
|
||||
|
||||
- name: Include configuration tasks
|
||||
ansible.builtin.include_tasks: configure.yml
|
||||
|
||||
- name: Include user management tasks
|
||||
ansible.builtin.include_tasks: users.yml
|
||||
when: wg_peers | length > 0
|
||||
@@ -0,0 +1,73 @@
|
||||
---
|
||||
# WireGuard User Management Tasks
|
||||
|
||||
- name: Auto-allocate IPs if enabled
|
||||
ansible.builtin.set_fact:
|
||||
wg_peers_with_ips: "{{ wg_peers_with_ips | default([]) + [item | combine({'ip': wg_network | ansible.utils.ipaddr(wg_ip_start + idx) | ansible.utils.ipaddr('address')})] }}"
|
||||
loop: "{{ wg_peers }}"
|
||||
loop_control:
|
||||
index_var: idx
|
||||
when:
|
||||
- wg_auto_allocate_ips | bool
|
||||
- item.ip is not defined
|
||||
|
||||
- name: Use provided IPs if auto-allocation disabled
|
||||
ansible.builtin.set_fact:
|
||||
wg_peers_with_ips: "{{ wg_peers }}"
|
||||
when: not (wg_auto_allocate_ips | bool)
|
||||
|
||||
- name: Generate client private keys
|
||||
ansible.builtin.shell: wg genkey > {{ wg_keys_dir }}/{{ item.name }}_private.key
|
||||
args:
|
||||
creates: "{{ wg_keys_dir }}/{{ item.name }}_private.key"
|
||||
loop: "{{ wg_peers_with_ips }}"
|
||||
|
||||
- name: Set client private key permissions
|
||||
ansible.builtin.file:
|
||||
path: "{{ wg_keys_dir }}/{{ item.name }}_private.key"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
loop: "{{ wg_peers_with_ips }}"
|
||||
|
||||
- name: Generate client public keys
|
||||
ansible.builtin.shell: cat {{ wg_keys_dir }}/{{ item.name }}_private.key | wg pubkey > {{ wg_keys_dir }}/{{ item.name }}_public.key
|
||||
args:
|
||||
creates: "{{ wg_keys_dir }}/{{ item.name }}_public.key"
|
||||
loop: "{{ wg_peers_with_ips }}"
|
||||
|
||||
- name: Read client keys
|
||||
ansible.builtin.shell: |
|
||||
echo "private=$(cat {{ wg_keys_dir }}/{{ item.name }}_private.key)"
|
||||
echo "public=$(cat {{ wg_keys_dir }}/{{ item.name }}_public.key)"
|
||||
register: client_keys
|
||||
loop: "{{ wg_peers_with_ips }}"
|
||||
changed_when: false
|
||||
|
||||
- name: Generate client configurations
|
||||
ansible.builtin.template:
|
||||
src: client.conf.j2
|
||||
dest: "{{ wg_client_configs_dir }}/{{ item.item.name }}.conf"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0600'
|
||||
loop: "{{ client_keys.results }}"
|
||||
vars:
|
||||
client_name: "{{ item.item.name }}"
|
||||
client_ip: "{{ item.item.ip }}"
|
||||
client_private_key: "{{ item.stdout_lines[0].split('=')[1] }}"
|
||||
client_public_key: "{{ item.stdout_lines[1].split('=')[1] }}"
|
||||
|
||||
- name: Generate QR codes for mobile clients
|
||||
ansible.builtin.shell: qrencode -t ansiutf8 < {{ wg_client_configs_dir }}/{{ item.name }}.conf > {{ wg_client_configs_dir }}/{{ item.name }}_qr.txt
|
||||
args:
|
||||
creates: "{{ wg_client_configs_dir }}/{{ item.name }}_qr.txt"
|
||||
loop: "{{ wg_peers_with_ips }}"
|
||||
|
||||
- name: Create summary file
|
||||
ansible.builtin.template:
|
||||
src: summary.md.j2
|
||||
dest: "{{ wg_client_configs_dir }}/README.md"
|
||||
owner: root
|
||||
group: root
|
||||
mode: '0644'
|
||||
@@ -0,0 +1,11 @@
|
||||
[Interface]
|
||||
PrivateKey = {{ client_private_key }}
|
||||
Address = {{ client_ip }}/{{ wg_network | ansible.utils.ipaddr('prefix') }}
|
||||
DNS = {{ wg_dns_servers | join(', ') }}
|
||||
MTU = {{ wg_mtu }}
|
||||
|
||||
[Peer]
|
||||
PublicKey = {{ wg_server_public_key }}
|
||||
Endpoint = {{ ansible_default_ipv4.address }}:{{ wg_port }}
|
||||
AllowedIPs = 0.0.0.0/0, ::/0
|
||||
PersistentKeepalive = {{ wg_persistent_keepalive }}
|
||||
@@ -0,0 +1,49 @@
|
||||
# WireGuard VPN Client Configurations
|
||||
|
||||
**Server**: {{ inventory_hostname }}
|
||||
**Server IP**: {{ ansible_default_ipv4.address }}
|
||||
**VPN Network**: {{ wg_network }}
|
||||
**Server Public Key**: {{ wg_server_public_key }}
|
||||
|
||||
## Client Configurations
|
||||
|
||||
{% for peer in wg_peers_with_ips | default([]) %}
|
||||
### {{ peer.name }}
|
||||
- **IP Address**: {{ peer.ip }}
|
||||
- **Config File**: `{{ peer.name }}.conf`
|
||||
- **QR Code**: `{{ peer.name }}_qr.txt`
|
||||
|
||||
{% endfor %}
|
||||
|
||||
## Installation Instructions
|
||||
|
||||
### Desktop (Linux/macOS/Windows)
|
||||
|
||||
1. Install WireGuard: https://www.wireguard.com/install/
|
||||
2. Copy the `.conf` file to your device
|
||||
3. Import configuration:
|
||||
- Linux: `sudo wg-quick up /path/to/config.conf`
|
||||
- macOS/Windows: Import via WireGuard GUI
|
||||
4. Connect
|
||||
|
||||
### Mobile (iOS/Android)
|
||||
|
||||
1. Install WireGuard app from App Store/Play Store
|
||||
2. View QR code: `cat <username>_qr.txt`
|
||||
3. Scan QR code in WireGuard app
|
||||
4. Connect
|
||||
|
||||
## Testing
|
||||
|
||||
After connecting, verify your IP:
|
||||
```bash
|
||||
curl https://ifconfig.me
|
||||
```
|
||||
|
||||
Should show: {{ ansible_default_ipv4.address }}
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
- Ensure port {{ wg_port }}/udp is open in firewall
|
||||
- Check server status: `sudo wg show`
|
||||
- Check logs: `sudo journalctl -u wg-quick@{{ wg_interface }}`
|
||||
@@ -0,0 +1,17 @@
|
||||
[Interface]
|
||||
Address = {{ wg_server_ip }}/{{ wg_network | ansible.utils.ipaddr('prefix') }}
|
||||
ListenPort = {{ wg_port }}
|
||||
PrivateKey = {{ wg_server_private_key }}
|
||||
MTU = {{ wg_mtu }}
|
||||
|
||||
PostUp = {{ wg_postup }}
|
||||
PostDown = {{ wg_postdown }}
|
||||
|
||||
{% for peer in wg_peers_with_ips | default([]) %}
|
||||
# {{ peer.name }}
|
||||
[Peer]
|
||||
PublicKey = {{ lookup('file', wg_keys_dir + '/' + peer.name + '_public.key') }}
|
||||
AllowedIPs = {{ peer.ip }}/32
|
||||
PersistentKeepalive = {{ wg_persistent_keepalive }}
|
||||
|
||||
{% endfor %}
|
||||
Reference in New Issue
Block a user