Last active
July 18, 2025 20:18
-
-
Save amosbastian/8cc3efd809cc244b4213bfc46b5e74f3 to your computer and use it in GitHub Desktop.
hardening.sh
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/bin/bash | |
# Hetzner Server Hardening Script for Debian 12 | |
# Run as root after SSH'ing in: ./harden.sh | |
set -euo pipefail | |
# Colors for output | |
RED='\033[0;31m' | |
GREEN='\033[0;32m' | |
YELLOW='\033[1;33m' | |
NC='\033[0m' # No Color | |
log() { | |
echo -e "${GREEN}[$(date +'%Y-%m-%d %H:%M:%S')] $1${NC}" | |
} | |
warn() { | |
echo -e "${YELLOW}[$(date +'%Y-%m-%d %H:%M:%S')] WARNING: $1${NC}" | |
} | |
error() { | |
echo -e "${RED}[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $1${NC}" | |
exit 1 | |
} | |
# Check if running as root | |
if [[ $EUID -ne 0 ]]; then | |
error "This script must be run as root" | |
fi | |
NEW_USER="admin" | |
log "Starting Hetzner server hardening for Debian 12..." | |
# Update system and install essentials | |
log "Updating system and installing essentials..." | |
apt-get update && apt-get install -y fail2ban sudo ansible curl wget git vim htop unattended-upgrades ufw python3-systemd libpam-modules libpam-modules | |
# Create admin user | |
log "Creating user '$NEW_USER'..." | |
if ! id "$NEW_USER" &>/dev/null; then | |
useradd -m -s /bin/bash "$NEW_USER" | |
usermod -aG sudo "$NEW_USER" | |
echo "$NEW_USER ALL=(ALL) NOPASSWD:ALL" > "/etc/sudoers.d/$NEW_USER" | |
log "User '$NEW_USER' created and added to sudo group" | |
else | |
log "User '$NEW_USER' already exists" | |
fi | |
# Copy SSH keys from root to new user | |
log "Setting up SSH for $NEW_USER user..." | |
mkdir -p "/home/$NEW_USER/.ssh" | |
if [ -f /root/.ssh/authorized_keys ]; then | |
cp /root/.ssh/authorized_keys "/home/$NEW_USER/.ssh/" | |
chmod 700 "/home/$NEW_USER/.ssh" | |
chmod 600 "/home/$NEW_USER/.ssh/authorized_keys" | |
chown -R "$NEW_USER:$NEW_USER" "/home/$NEW_USER/.ssh" | |
log "SSH keys copied from root to $NEW_USER" | |
else | |
warn "No authorized_keys found in /root/.ssh/ - you'll need to add your public key manually" | |
fi | |
# Configure UFW before running Ansible | |
log "Configuring UFW firewall..." | |
ufw --force reset | |
ufw default deny incoming | |
ufw default allow outgoing | |
ufw allow ssh | |
ufw --force enable | |
log "UFW configured and enabled with default deny policy" | |
# Install DevSec hardening collection | |
log "Installing DevSec hardening collection..." | |
ansible-galaxy collection install devsec.hardening | |
# Create hardening playbook | |
log "Creating hardening playbook..." | |
cat > hardening_playbook.yml << 'EOF' | |
--- | |
- hosts: localhost | |
connection: local | |
become: yes | |
collections: | |
- devsec.hardening | |
vars: | |
ssh_allow_users: "admin" | |
ssh_allow_groups: "admin" | |
# Debian 12 specific SSH configuration with PAM enabled | |
ssh_server_ports: ['22'] | |
ssh_use_pam: true | |
ssh_challenge_response_authentication: false | |
ssh_gss_api_authentication: false | |
ssh_x11_forwarding: false | |
ssh_max_auth_tries: 3 | |
ssh_client_alive_interval: 600 | |
ssh_client_alive_count_max: 3 | |
ssh_compression: false | |
ssh_use_dns: false | |
ssh_permit_tunnel: "no" | |
ssh_print_motd: false | |
ssh_password_authentication: false | |
ssh_permit_root_login: "no" | |
ssh_permit_empty_passwords: false | |
# Aggressive fail2ban configuration for better protection | |
fail2ban_jail_local: | | |
[DEFAULT] | |
# Debian 12 uses systemd journal by default | |
backend = systemd | |
# Aggressive mode settings | |
bantime = 86400 | |
findtime = 300 | |
maxretry = 2 | |
[sshd] | |
enabled = true | |
port = {{ ssh_server_ports | first | default('22') }} | |
filter = sshd | |
backend = systemd | |
maxretry = 2 | |
findtime = 300 | |
bantime = 86400 | |
ignoreip = 127.0.0.1/8 ::1 | |
pre_tasks: | |
- name: Update package cache | |
apt: | |
update_cache: yes | |
cache_valid_time: 3600 | |
- name: Install required packages | |
apt: | |
name: | |
- fail2ban | |
- ufw | |
- unattended-upgrades | |
- apt-listchanges | |
- libpam-modules | |
state: present | |
- name: Ensure fail2ban directories exist | |
file: | |
path: "{{ item }}" | |
state: directory | |
owner: root | |
group: root | |
mode: '0755' | |
loop: | |
- /etc/fail2ban | |
- /etc/fail2ban/jail.d | |
roles: | |
- devsec.hardening.os_hardening | |
- devsec.hardening.ssh_hardening | |
tasks: | |
- name: Ensure SSH uses PAM | |
lineinfile: | |
path: /etc/ssh/sshd_config | |
regexp: '^#?UsePAM' | |
line: 'UsePAM yes' | |
backup: yes | |
notify: | |
- Restart SSH | |
- name: Configure Fail2Ban with aggressive settings | |
copy: | |
dest: /etc/fail2ban/jail.local | |
content: "{{ fail2ban_jail_local }}" | |
owner: root | |
group: root | |
mode: '0644' | |
notify: | |
- Restart Fail2Ban | |
- name: Enable and start Fail2Ban service | |
systemd: | |
name: fail2ban | |
state: started | |
enabled: yes | |
daemon_reload: yes | |
- name: Ensure SSH service is enabled and running | |
systemd: | |
name: ssh | |
state: started | |
enabled: yes | |
daemon_reload: yes | |
- name: Verify UFW is enabled and configured | |
ufw: | |
state: enabled | |
- name: Configure automatic security updates | |
copy: | |
dest: /etc/apt/apt.conf.d/50unattended-upgrades | |
content: | | |
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::Remove-Unused-Kernel-Packages "true"; | |
Unattended-Upgrade::Remove-New-Unused-Dependencies "true"; | |
Unattended-Upgrade::Remove-Unused-Dependencies "true"; | |
Unattended-Upgrade::Automatic-Reboot "false"; | |
owner: root | |
group: root | |
mode: '0644' | |
- name: Enable automatic updates | |
copy: | |
dest: /etc/apt/apt.conf.d/20auto-upgrades | |
content: | | |
APT::Periodic::Update-Package-Lists "1"; | |
APT::Periodic::Unattended-Upgrade "1"; | |
owner: root | |
group: root | |
mode: '0644' | |
- name: Reload SSH service | |
systemd: | |
name: ssh | |
state: reloaded | |
daemon_reload: yes | |
handlers: | |
- name: Restart Fail2Ban | |
systemd: | |
name: fail2ban | |
state: restarted | |
daemon_reload: yes | |
- name: Restart SSH | |
systemd: | |
name: ssh | |
state: restarted | |
daemon_reload: yes | |
EOF | |
# Install Docker | |
log "Installing Docker..." | |
apt-get install -y ca-certificates curl | |
install -m 0755 -d /etc/apt/keyrings | |
curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc | |
chmod a+r /etc/apt/keyrings/docker.asc | |
# Add the repository to Apt sources: | |
echo \ | |
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian \ | |
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \ | |
tee /etc/apt/sources.list.d/docker.list > /dev/null | |
apt-get update | |
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
# Add admin to docker group | |
usermod -aG docker "$NEW_USER" | |
# Run the hardening playbook | |
log "Running DevSec hardening playbook..." | |
ansible-playbook hardening_playbook.yml | |
# Check services status | |
log "Checking services status..." | |
systemctl status ssh --no-pager | |
systemctl status fail2ban --no-pager | |
ufw status verbose | |
log "Server hardening completed successfully!" | |
warn "IMPORTANT: Test SSH connection as '$NEW_USER' user before logging out!" | |
log "Your server is now hardened with DevSec playbook, fail2ban (aggressive mode), UFW firewall, and Docker installed." |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment