--- - name: Security Hardening hosts: arch-vps become: yes gather_facts: yes tasks: # ============================================ # System Updates and Package Security # ============================================ - name: Update package database pacman: update_cache: yes - name: Upgrade all packages to latest versions pacman: upgrade: yes register: package_upgrade - name: Display upgrade results debug: msg: "{{ package_upgrade.packages | length }} packages were upgraded" when: package_upgrade.packages is defined # ============================================ # Kernel/Module Version Check and Reboot # ============================================ - name: Get current running kernel version command: uname -r register: current_kernel changed_when: false - name: Get latest available kernel modules version shell: ls /lib/modules/ | sort -V | tail -1 register: latest_modules changed_when: false - name: Display kernel versions for debugging debug: msg: - "Running kernel: {{ current_kernel.stdout }}" - "Latest modules: {{ latest_modules.stdout }}" - name: Determine if reboot is needed set_fact: reboot_needed: "{{ current_kernel.stdout != latest_modules.stdout }}" - name: Reboot system if kernel/module mismatch detected reboot: reboot_timeout: 120 test_command: uptime when: reboot_needed | bool - name: Wait for system to be fully ready after reboot wait_for_connection: delay: 15 timeout: 300 when: reboot_needed | bool # ============================================ # SSH Hardening # ============================================ - name: Harden SSH configuration lineinfile: path: /etc/ssh/sshd_config regexp: "{{ item.regexp }}" line: "{{ item.line }}" backup: no loop: - { regexp: '^#?PasswordAuthentication', line: 'PasswordAuthentication no' } - { regexp: '^#?PermitRootLogin', line: 'PermitRootLogin prohibit-password' } - { regexp: '^#?PubkeyAuthentication', line: 'PubkeyAuthentication yes' } - { regexp: '^#?AuthorizedKeysFile', line: 'AuthorizedKeysFile .ssh/authorized_keys' } - { regexp: '^#?PermitEmptyPasswords', line: 'PermitEmptyPasswords no' } - { regexp: '^#?ChallengeResponseAuthentication', line: 'ChallengeResponseAuthentication no' } - { regexp: '^#?UsePAM', line: 'UsePAM no' } - { regexp: '^#?X11Forwarding', line: 'X11Forwarding no' } - { regexp: '^#?MaxAuthTries', line: 'MaxAuthTries 3' } - { regexp: '^#?ClientAliveInterval', line: 'ClientAliveInterval 300' } - { regexp: '^#?ClientAliveCountMax', line: 'ClientAliveCountMax 2' } register: ssh_config_changed - name: Test SSH configuration syntax command: sshd -t changed_when: false failed_when: false register: ssh_test - name: Fail if SSH configuration is invalid fail: msg: "SSH configuration test failed: {{ ssh_test.stderr }}" when: ssh_test.rc != 0 - name: Restart SSH service if configuration changed systemd: name: sshd state: restarted when: ssh_config_changed.changed - name: Wait for SSH service to be available wait_for: port: 22 host: "{{ ansible_host }}" delay: 2 timeout: 30 delegate_to: localhost become: no when: ssh_config_changed.changed - name: Test SSH connection with new configuration ping: when: ssh_config_changed.changed # ============================================ # nftables Firewall Configuration # ============================================ - name: Install nftables pacman: name: nftables state: present - name: Create nftables rules directory file: path: /etc/nftables.d state: directory mode: '0755' - name: Create base nftables configuration copy: content: | #!/usr/sbin/nft -f # Flush existing rules for clean slate flush ruleset # Main firewall table - Rick-Infra Security # Architecture: Base rules -> Service rules -> Drop rule table inet filter { chain input { type filter hook input priority 0; policy drop; # ====================================== # Base Infrastructure Rules # ====================================== # Allow loopback interface iif "lo" accept # Allow established and related connections ct state established,related accept # Allow SSH (port 22) - Infrastructure access tcp dport 22 ct state new accept # Allow HTTP and HTTPS for Caddy reverse proxy tcp dport { 80, 443 } ct state new accept # Allow ping with rate limiting icmp type echo-request limit rate 1/second accept icmpv6 type echo-request limit rate 1/second accept } chain forward { type filter hook forward priority 0; policy drop; } chain output { type filter hook output priority 0; policy accept; } } dest: /etc/nftables.conf mode: '0755' backup: yes register: nft_config_changed - name: Create nftables drop rule (loaded last) copy: content: | # Final drop rule - Rick-Infra Security # This file is loaded LAST to drop all unmatched traffic # Service-specific rules in /etc/nftables.d/ are loaded before this add rule inet filter input counter drop comment "Drop all other traffic" dest: /etc/nftables.d/99-drop.nft mode: '0644' backup: yes register: nft_drop_changed - name: Create nftables loader script copy: content: | #!/usr/sbin/nft -f # Rick-Infra nftables loader # Loads rules in correct order: base -> services -> drop # Load base infrastructure rules include "/etc/nftables.conf" # Load service-specific rules (00-98 range) include "/etc/nftables.d/[0-8]*.nft" # Load final drop rule (99-drop.nft) include "/etc/nftables.d/99-drop.nft" dest: /etc/nftables-load.conf mode: '0755' backup: yes register: nft_loader_changed - name: Test nftables configuration syntax command: nft -c -f /etc/nftables-load.conf changed_when: false failed_when: false register: nft_test - name: Fail if nftables configuration is invalid fail: msg: "nftables configuration test failed: {{ nft_test.stderr }}" when: nft_test.rc != 0 - name: Update nftables systemd service to use loader lineinfile: path: /usr/lib/systemd/system/nftables.service regexp: '^ExecStart=' line: 'ExecStart=/usr/sbin/nft -f /etc/nftables-load.conf' backup: yes register: nft_service_changed - name: Reload systemd daemon if service changed systemd: daemon_reload: yes when: nft_service_changed.changed - name: Flush existing nftables rules before applying new configuration command: nft flush ruleset failed_when: false changed_when: false when: nft_config_changed.changed or nft_drop_changed.changed or nft_loader_changed.changed - name: Enable and start nftables service systemd: name: nftables enabled: yes state: restarted when: nft_config_changed.changed or nft_drop_changed.changed or nft_loader_changed.changed or nft_service_changed.changed - name: Wait for nftables to be active pause: seconds: 3 when: nft_config_changed.changed - name: Test SSH connection after firewall activation wait_for: port: 22 host: "{{ ansible_host }}" timeout: 15 delegate_to: localhost become: no when: nft_config_changed.changed - name: Verify nftables rules are loaded command: nft list ruleset register: nft_rules changed_when: false - name: Display active firewall rules debug: var: nft_rules.stdout_lines # ============================================ # Fail2ban Setup # ============================================ - name: Install fail2ban pacman: name: fail2ban state: present - name: Create fail2ban local configuration copy: content: | [DEFAULT] # Ban hosts for 1 hour (3600 seconds) bantime = 3600 banaction = nftables banaction_allports = nftables[type=allports] # A host is banned if it has generated "maxretry" failures during "findtime" findtime = 600 maxretry = 3 [sshd] enabled = true port = ssh filter = sshd logpath = /var/log/auth.log maxretry = 3 bantime = 3600 dest: /etc/fail2ban/jail.local backup: yes notify: restart fail2ban - name: Enable and start fail2ban service systemd: name: fail2ban enabled: yes state: started # ============================================ # Kernel Network Hardening # ============================================ - name: Apply kernel network security settings sysctl: name: "{{ item.name }}" value: "{{ item.value }}" state: present sysctl_file: /etc/sysctl.d/99-security.conf reload: yes loop: # Disable IP forwarding - { name: 'net.ipv4.ip_forward', value: '0' } - { name: 'net.ipv6.conf.all.forwarding', value: '0' } # Disable source routing - { name: 'net.ipv4.conf.all.accept_source_route', value: '0' } - { name: 'net.ipv6.conf.all.accept_source_route', value: '0' } # Disable ICMP redirects - { name: 'net.ipv4.conf.all.accept_redirects', value: '0' } - { name: 'net.ipv6.conf.all.accept_redirects', value: '0' } - { name: 'net.ipv4.conf.all.send_redirects', value: '0' } # Enable syn flood protection - { name: 'net.ipv4.tcp_syncookies', value: '1' } # Ignore ping requests - { name: 'net.ipv4.icmp_echo_ignore_all', value: '0' } # Log suspicious packets - { name: 'net.ipv4.conf.all.log_martians', value: '1' } # Disable IPv6 if not needed - { name: 'net.ipv6.conf.all.disable_ipv6', value: '0' } - { name: 'net.ipv6.conf.default.disable_ipv6', value: '0' } handlers: - name: restart fail2ban systemd: name: fail2ban state: restarted