Initial commit
This commit is contained in:
21
README.md
Normal file
21
README.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Rick's Infra
|
||||||
|
## Arch Linux VPS
|
||||||
|
### Ansible
|
||||||
|
Infrastructure as code for setting up new instance.
|
||||||
|
- [ ] Security
|
||||||
|
- [ ] SSH
|
||||||
|
- [ ] Firewall
|
||||||
|
- [ ] Fail2ban
|
||||||
|
- [ ] Kernel hardening
|
||||||
|
- [ ] Base packages
|
||||||
|
- [ ] Monitoring/Logging
|
||||||
|
- [ ] Backup
|
||||||
|
|
||||||
|
### Services
|
||||||
|
Services are managed by serviced
|
||||||
|
|
||||||
|
#### Caddy
|
||||||
|
Reverse proxy.
|
||||||
|
|
||||||
|
### Containers
|
||||||
|
Containers are managed by rootless Podman.
|
||||||
7
docs/security-hardening.md
Normal file
7
docs/security-hardening.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
# Securing the VPS
|
||||||
|
## Network Security
|
||||||
|
- **SSH Hardening**: Password authentication disabled, root login disabled, key-only authentication
|
||||||
|
- **Firewall Configuration**: UFW with deny-all incoming, allow-all outgoing defaults
|
||||||
|
- **Fail2ban**: SSH brute-force protection with configurable ban times
|
||||||
|
- **Kernel Network Hardening**: IP forwarding disabled, source routing blocked, ICMP redirects disabled
|
||||||
|
|
||||||
16
docs/setup-guide.md
Normal file
16
docs/setup-guide.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# Setup guide
|
||||||
|
|
||||||
|
## Get a VPS with Arch Linux OS
|
||||||
|
- We are using [Hostinger](https://hostinger.com)
|
||||||
|
- Find it's IP in the Hostinger Dashboard
|
||||||
|
|
||||||
|
## Initial Setup
|
||||||
|
- Test manual sign in: ```ssh root@<VPS_IP>```
|
||||||
|
- Copy SSH key:
|
||||||
|
```bash
|
||||||
|
# From Workstation
|
||||||
|
ssh-copy-id -i ~/.ssh/id_ed25519.pub root@<VPS_IP>
|
||||||
|
```
|
||||||
|
- Add host to Ansible inventory
|
||||||
|
- Test connection `ansible -i inventory/hosts.yml arch-vps -m ping`
|
||||||
|
- ```ansible-playbook -i inventory/hosts/yml site.yml```
|
||||||
11
inventory/hosts.yml
Normal file
11
inventory/hosts.yml
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
---
|
||||||
|
all:
|
||||||
|
children:
|
||||||
|
production:
|
||||||
|
hosts:
|
||||||
|
arch-vps:
|
||||||
|
ansible_host: 69.62.119.31
|
||||||
|
ansible_user: root
|
||||||
|
|
||||||
|
vars:
|
||||||
|
ansible_python_interpreter: /usr/bin/python3
|
||||||
337
playbooks/security.yml
Normal file
337
playbooks/security.yml
Normal file
@@ -0,0 +1,337 @@
|
|||||||
|
---
|
||||||
|
- 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: Test if nftables modules are available
|
||||||
|
command: nft list ruleset
|
||||||
|
register: nft_test_prereq
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Determine if reboot is needed
|
||||||
|
set_fact:
|
||||||
|
reboot_needed: "{{ current_kernel.stdout != latest_modules.stdout or nft_test_prereq.rc != 0 }}"
|
||||||
|
|
||||||
|
- name: Reboot system if kernel/module mismatch detected
|
||||||
|
reboot:
|
||||||
|
reboot_timeout: 60
|
||||||
|
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
|
||||||
|
|
||||||
|
- name: Verify nftables is now available after reboot
|
||||||
|
command: nft list ruleset
|
||||||
|
register: nft_post_reboot
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
|
||||||
|
- name: Display post-reboot nftables status
|
||||||
|
debug:
|
||||||
|
msg: "nftables availability after reboot: {{ 'Working' if nft_post_reboot.rc == 0 else 'Failed' }}"
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 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 configuration
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
#!/usr/sbin/nft -f
|
||||||
|
|
||||||
|
# Main firewall table
|
||||||
|
table inet filter {
|
||||||
|
chain input {
|
||||||
|
type filter hook input priority 0; policy drop;
|
||||||
|
|
||||||
|
# Allow loopback interface
|
||||||
|
iif "lo" accept
|
||||||
|
|
||||||
|
# Allow established and related connections
|
||||||
|
ct state established,related accept
|
||||||
|
|
||||||
|
# Allow SSH (port 22)
|
||||||
|
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
|
||||||
|
|
||||||
|
# Log and drop everything else
|
||||||
|
counter drop
|
||||||
|
}
|
||||||
|
|
||||||
|
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: Test nftables configuration syntax
|
||||||
|
command: nft -c -f /etc/nftables.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: Flush existing nftables rules before applying new configuration
|
||||||
|
command: nft flush ruleset
|
||||||
|
failed_when: false
|
||||||
|
changed_when: false
|
||||||
|
when: nft_config_changed.changed
|
||||||
|
|
||||||
|
- name: Create firewall rollback safety script
|
||||||
|
copy:
|
||||||
|
content: |
|
||||||
|
#!/bin/bash
|
||||||
|
# Safety rollback script - automatically disables firewall after 5 minutes
|
||||||
|
echo "$(date): Starting 5-minute firewall safety timer"
|
||||||
|
sleep 300
|
||||||
|
echo "$(date): Safety timer expired, disabling firewall"
|
||||||
|
nft flush ruleset
|
||||||
|
systemctl stop nftables
|
||||||
|
rm -f /tmp/nft-rollback.sh
|
||||||
|
dest: /tmp/nft-rollback.sh
|
||||||
|
mode: '0755'
|
||||||
|
when: nft_config_changed.changed
|
||||||
|
|
||||||
|
- name: Start rollback safety timer in background
|
||||||
|
shell: nohup /tmp/nft-rollback.sh >> /tmp/nft-rollback.log 2>&1 &
|
||||||
|
when: nft_config_changed.changed
|
||||||
|
|
||||||
|
- name: Enable and start nftables service
|
||||||
|
systemd:
|
||||||
|
name: nftables
|
||||||
|
enabled: yes
|
||||||
|
state: restarted
|
||||||
|
when: nft_config_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: Cancel rollback timer if SSH connection works
|
||||||
|
shell: pkill -f nft-rollback.sh || true
|
||||||
|
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
|
||||||
2
roles/caddy/README.md
Normal file
2
roles/caddy/README.md
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Install and configure caddy
|
||||||
|
Installs and configures caddy.
|
||||||
Reference in New Issue
Block a user