Skip to content

Instantly share code, notes, and snippets.

@amosbastian
Last active July 18, 2025 20:18
Show Gist options
  • Save amosbastian/8cc3efd809cc244b4213bfc46b5e74f3 to your computer and use it in GitHub Desktop.
Save amosbastian/8cc3efd809cc244b4213bfc46b5e74f3 to your computer and use it in GitHub Desktop.
hardening.sh
#!/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