|
#!/usr/bin/env bash |
|
|
|
# ============================================================================ |
|
# VPS Hardening Script |
|
# ============================================================================ |
|
# Description: Automates initial VPS security setup including non-root user, |
|
# SSH hardening, firewall configuration, automatic security updates, |
|
# and additional security tools installation. |
|
# |
|
# Usage: sudo bash vps-hardening.sh |
|
# ============================================================================ |
|
|
|
set -euo pipefail # Exit on error, undefined variable, pipe failure |
|
IFS=$'\n\t' |
|
|
|
# Colors for output |
|
RED='\033[0;31m' |
|
GREEN='\033[0;32m' |
|
YELLOW='\033[1;33m' |
|
BLUE='\033[0;34m' |
|
NC='\033[0m' # No Color |
|
|
|
# Script configuration |
|
SCRIPT_NAME="$(basename "$0")" |
|
LOG_FILE="/var/log/vps-hardening-$(date +%Y%m%d-%H%M%S).log" |
|
CONFIG_DIR="$HOME/.vps-hardening" |
|
STATE_FILE="$CONFIG_DIR/setup-state.conf" |
|
|
|
# ============================================================================ |
|
# Helper Functions |
|
# ============================================================================ |
|
|
|
log() { |
|
echo -e "${GREEN}[$(date '+%Y-%m-%d %H:%M:%S')] $1${NC}" | tee -a "$LOG_FILE" |
|
} |
|
|
|
warn() { |
|
echo -e "${YELLOW}[WARNING] $1${NC}" | tee -a "$LOG_FILE" |
|
} |
|
|
|
error() { |
|
echo -e "${RED}[ERROR] $1${NC}" | tee -a "$LOG_FILE" |
|
exit 1 |
|
} |
|
|
|
info() { |
|
echo -e "${BLUE}[INFO] $1${NC}" | tee -a "$LOG_FILE" |
|
} |
|
|
|
confirm() { |
|
local prompt="$1" |
|
local default="${2:-n}" |
|
local response |
|
|
|
if [[ "$default" == "y" ]]; then |
|
prompt="$prompt [Y/n]: " |
|
else |
|
prompt="$prompt [y/N]: " |
|
fi |
|
|
|
read -r -p "$prompt" response |
|
response=${response:-$default} |
|
|
|
[[ "$response" =~ ^[Yy]$ ]] |
|
} |
|
|
|
check_root() { |
|
if [[ $EUID -ne 0 ]]; then |
|
error "This script must be run as root. Use: sudo bash $SCRIPT_NAME" |
|
fi |
|
} |
|
|
|
check_os() { |
|
if [[ -f /etc/os-release ]]; then |
|
. /etc/os-release |
|
if [[ "$ID" != "ubuntu" ]]; then |
|
warn "This script is optimized for Ubuntu. Detected OS: $ID" |
|
if ! confirm "Continue anyway?" "n"; then |
|
exit 0 |
|
fi |
|
fi |
|
else |
|
warn "Cannot detect OS version. Proceed with caution." |
|
fi |
|
} |
|
|
|
save_state() { |
|
mkdir -p "$CONFIG_DIR" |
|
cat > "$STATE_FILE" << EOF |
|
# VPS Hardening Setup State |
|
# Last run: $(date) |
|
SSH_USER="${SSH_USER:-}" |
|
SSH_CONFIGURED="${SSH_CONFIGURED:-false}" |
|
FIREWALL_CONFIGURED="${FIREWALL_CONFIGURED:-false}" |
|
AUTO_UPDATES_CONFIGURED="${AUTO_UPDATES_CONFIGURED:-false}" |
|
FAIL2BAN_INSTALLED="${FAIL2BAN_INSTALLED:-false}" |
|
EOF |
|
} |
|
|
|
load_state() { |
|
if [[ -f "$STATE_FILE" ]]; then |
|
source "$STATE_FILE" |
|
info "Found previous setup state from: $(grep "Last run:" "$STATE_FILE" | cut -d: -f2-)" |
|
fi |
|
} |
|
|
|
# ============================================================================ |
|
# Main Setup Functions |
|
# ============================================================================ |
|
|
|
create_nonroot_user() { |
|
log "=== Step 1: Creating non-root user ===" |
|
|
|
# Check if we already created a user |
|
if [[ -n "${SSH_USER:-}" ]] && id "$SSH_USER" &>/dev/null; then |
|
info "User $SSH_USER already exists. Skipping user creation." |
|
return 0 |
|
fi |
|
|
|
local username="" |
|
while [[ -z "$username" ]]; do |
|
read -r -p "Enter username for the new non-root user: " username |
|
if [[ -z "$username" ]]; then |
|
warn "Username cannot be empty" |
|
elif id "$username" &>/dev/null; then |
|
warn "User $username already exists. Choose another name or press Ctrl+C to exit." |
|
username="" |
|
fi |
|
done |
|
|
|
# Create user with home directory |
|
adduser --gecos "" "$username" |
|
|
|
# Add to sudo group |
|
usermod -aG sudo "$username" |
|
|
|
# Verify user creation |
|
if id "$username" &>/dev/null; then |
|
log "User $username created successfully and added to sudo group" |
|
SSH_USER="$username" |
|
save_state |
|
else |
|
error "Failed to create user $username" |
|
fi |
|
} |
|
|
|
setup_ssh_for_user() { |
|
log "=== Step 2: Setting up SSH for new user ===" |
|
|
|
if [[ -z "${SSH_USER:-}" ]]; then |
|
error "No SSH user defined. Please create a user first." |
|
fi |
|
|
|
# Check if SSH is already configured |
|
if [[ "${SSH_CONFIGURED:-false}" == "true" ]]; then |
|
if confirm "SSH appears to be already configured. Reconfigure?" "n"; then |
|
SSH_CONFIGURED="false" |
|
else |
|
info "Skipping SSH configuration" |
|
return 0 |
|
fi |
|
fi |
|
|
|
# Switch to new user context for SSH setup |
|
local user_home |
|
user_home=$(eval echo "~$SSH_USER") |
|
|
|
# Create .ssh directory |
|
sudo -u "$SSH_USER" mkdir -p "$user_home/.ssh" |
|
|
|
# Setup SSH key |
|
if [[ ! -f "$user_home/.ssh/authorized_keys" ]]; then |
|
info "Please paste your public SSH key (from ~/.ssh/id_rsa.pub or equivalent)" |
|
info "Enter the key and press Ctrl+D when done (multi-line paste supported):" |
|
|
|
local pubkey="" |
|
while IFS= read -r line; do |
|
pubkey+="$line"$'\n' |
|
done |
|
|
|
if [[ -n "$pubkey" ]]; then |
|
echo "$pubkey" | sudo -u "$SSH_USER" tee "$user_home/.ssh/authorized_keys" > /dev/null |
|
else |
|
error "No SSH key provided. Cannot continue without SSH key access." |
|
fi |
|
else |
|
info "Authorized keys file already exists. Current keys:" |
|
cat "$user_home/.ssh/authorized_keys" |
|
if ! confirm "Replace existing SSH key(s)?" "n"; then |
|
info "Keeping existing SSH keys" |
|
else |
|
info "Please paste your new public SSH key and press Ctrl+D when done:" |
|
local newkey="" |
|
while IFS= read -r line; do |
|
newkey+="$line"$'\n' |
|
done |
|
echo "$newkey" | sudo -u "$SSH_USER" tee "$user_home/.ssh/authorized_keys" > /dev/null |
|
fi |
|
fi |
|
|
|
# Set correct permissions |
|
sudo -u "$SSH_USER" chmod 700 "$user_home/.ssh" |
|
sudo -u "$SSH_USER" chmod 600 "$user_home/.ssh/authorized_keys" |
|
|
|
# Test SSH configuration |
|
info "Testing SSH configuration..." |
|
local ip |
|
ip=$(curl -s ifconfig.me || echo "your_server_ip") |
|
|
|
warn "IMPORTANT: Before proceeding, test SSH access in a NEW terminal:" |
|
echo -e "${YELLOW}ssh $SSH_USER@$ip${NC}" |
|
echo "" |
|
|
|
if ! confirm "Have you successfully connected via SSH in another terminal?" "n"; then |
|
warn "Please test SSH connection before continuing. This is critical!" |
|
if confirm "Continue without testing? (NOT RECOMMENDED)" "n"; then |
|
warn "Proceeding at your own risk..." |
|
else |
|
info "Exiting. Please test SSH connection and run script again." |
|
exit 0 |
|
fi |
|
fi |
|
|
|
SSH_CONFIGURED="true" |
|
save_state |
|
log "SSH configured successfully for user $SSH_USER" |
|
} |
|
|
|
disable_root_password_auth() { |
|
log "=== Step 3: Disabling root login and password authentication ===" |
|
|
|
local sshd_config="/etc/ssh/sshd_config" |
|
local backup_file="${sshd_config}.backup-$(date +%Y%m%d-%H%M%S)" |
|
|
|
# Backup SSH config |
|
cp "$sshd_config" "$backup_file" |
|
info "SSH configuration backed up to $backup_file" |
|
|
|
# Disable root login |
|
if grep -q "^PermitRootLogin" "$sshd_config"; then |
|
sed -i 's/^PermitRootLogin.*/PermitRootLogin no/' "$sshd_config" |
|
else |
|
echo "PermitRootLogin no" >> "$sshd_config" |
|
fi |
|
|
|
# Disable password authentication |
|
if grep -q "^PasswordAuthentication" "$sshd_config"; then |
|
sed -i 's/^PasswordAuthentication.*/PasswordAuthentication no/' "$sshd_config" |
|
else |
|
echo "PasswordAuthentication no" >> "$sshd_config" |
|
fi |
|
|
|
# Ensure challenge-response authentication is also disabled |
|
if grep -q "^ChallengeResponseAuthentication" "$sshd_config"; then |
|
sed -i 's/^ChallengeResponseAuthentication.*/ChallengeResponseAuthentication no/' "$sshd_config" |
|
else |
|
echo "ChallengeResponseAuthentication no" >> "$sshd_config" |
|
fi |
|
|
|
# Test SSH configuration before reloading |
|
sshd -t || error "Invalid SSH configuration. Please check $sshd_config" |
|
|
|
# Reload SSH service |
|
systemctl reload sshd || systemctl reload ssh |
|
|
|
log "Root login and password authentication disabled successfully" |
|
} |
|
|
|
configure_firewall() { |
|
log "=== Step 4: Configuring UFW firewall ===" |
|
|
|
if [[ "${FIREWALL_CONFIGURED:-false}" == "true" ]]; then |
|
if ! confirm "Firewall appears to be configured. Reconfigure?" "n"; then |
|
info "Skipping firewall configuration" |
|
sudo ufw status verbose |
|
return 0 |
|
fi |
|
fi |
|
|
|
# Check if UFW is installed |
|
if ! command -v ufw &> /dev/null; then |
|
info "Installing UFW..." |
|
apt-get update -qq |
|
apt-get install -y ufw |
|
fi |
|
|
|
# Reset UFW if requested |
|
if sudo ufw status | grep -q "active"; then |
|
warn "UFW is currently active" |
|
if confirm "Reset UFW to default settings?" "n"; then |
|
sudo ufw --force reset |
|
fi |
|
fi |
|
|
|
# Set default policies |
|
sudo ufw default deny incoming |
|
sudo ufw default allow outgoing |
|
|
|
# Allow essential services |
|
info "Allowing essential services:" |
|
sudo ufw allow ssh |
|
sudo ufw allow http |
|
sudo ufw allow https |
|
|
|
# Ask about additional ports |
|
if confirm "Do you want to allow any additional ports?" "n"; then |
|
while true; do |
|
read -r -p "Enter port number to allow (or 'done' to finish): " port |
|
if [[ "$port" == "done" ]]; then |
|
break |
|
elif [[ "$port" =~ ^[0-9]+$ ]] && [[ "$port" -ge 1 ]] && [[ "$port" -le 65535 ]]; then |
|
read -r -p "TCP only? (y/n): " tcp_only |
|
if [[ "$tcp_only" =~ ^[Yy]$ ]]; then |
|
sudo ufw allow "$port"/tcp |
|
else |
|
sudo ufw allow "$port" |
|
fi |
|
else |
|
warn "Invalid port number" |
|
fi |
|
done |
|
fi |
|
|
|
# Enable firewall |
|
warn "About to enable firewall. This may close current SSH connection if SSH isn't allowed." |
|
if confirm "Enable UFW now?" "y"; then |
|
echo "y" | sudo ufw enable |
|
sudo ufw status verbose |
|
FIREWALL_CONFIGURED="true" |
|
save_state |
|
log "Firewall configured successfully" |
|
else |
|
warn "Firewall not enabled. Security compromised." |
|
fi |
|
} |
|
|
|
setup_auto_updates() { |
|
log "=== Step 5: Enabling automatic security updates ===" |
|
|
|
if [[ "${AUTO_UPDATES_CONFIGURED:-false}" == "true" ]]; then |
|
if ! confirm "Automatic updates already configured. Reconfigure?" "n"; then |
|
info "Skipping automatic updates configuration" |
|
apt-config dump APT::Periodic::Unattended-Upgrade |
|
return 0 |
|
fi |
|
fi |
|
|
|
# Install unattended-upgrades if not present |
|
if ! dpkg -l | grep -q unattended-upgrades; then |
|
info "Installing unattended-upgrades..." |
|
apt-get update -qq |
|
apt-get install -y unattended-upgrades |
|
fi |
|
|
|
# Configure unattended-upgrades |
|
cat > /etc/apt/apt.conf.d/50unattended-upgrades << 'EOF' |
|
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::DevRelease "false"; |
|
Unattended-Upgrade::AutoFixInterruptedDpkg "true"; |
|
Unattended-Upgrade::MinimalSteps "true"; |
|
Unattended-Upgrade::Remove-Unused-Kernel-Packages "true"; |
|
Unattended-Upgrade::Remove-Unused-Dependencies "true"; |
|
Unattended-Upgrade::Automatic-Reboot "false"; |
|
Unattended-Upgrade::Automatic-Reboot-Time "02:00"; |
|
EOF |
|
|
|
# Enable automatic updates |
|
cat > /etc/apt/apt.conf.d/20auto-upgrades << 'EOF' |
|
APT::Periodic::Update-Package-Lists "1"; |
|
APT::Periodic::Download-Upgradeable-Packages "1"; |
|
APT::Periodic::AutocleanInterval "7"; |
|
APT::Periodic::Unattended-Upgrade "1"; |
|
EOF |
|
|
|
# Apply configuration |
|
dpkg-reconfigure -f noninteractive unattended-upgrades |
|
|
|
# Verify configuration |
|
if apt-config dump APT::Periodic::Unattended-Upgrade | grep -q "1"; then |
|
AUTO_UPDATES_CONFIGURED="true" |
|
save_state |
|
log "Automatic security updates enabled successfully" |
|
else |
|
error "Failed to configure automatic updates" |
|
fi |
|
} |
|
|
|
install_security_tools() { |
|
log "=== Step 6: Installing additional security tools ===" |
|
|
|
local tools_to_install=() |
|
|
|
# Check and install fail2ban |
|
if ! dpkg -l | grep -q fail2ban; then |
|
if confirm "Install fail2ban for brute-force protection?" "y"; then |
|
tools_to_install+=("fail2ban") |
|
fi |
|
else |
|
info "fail2ban is already installed" |
|
FAIL2BAN_INSTALLED="true" |
|
fi |
|
|
|
# Check and install rsyslog |
|
if ! dpkg -l | grep -q rsyslog; then |
|
if confirm "Install rsyslog for system logging?" "y"; then |
|
tools_to_install+=("rsyslog") |
|
fi |
|
else |
|
info "rsyslog is already installed" |
|
fi |
|
|
|
# Additional recommended tools |
|
if confirm "Install additional useful tools (htop, curl, wget, git, vim)?" "n"; then |
|
tools_to_install+=(htop curl wget git vim) |
|
fi |
|
|
|
if [[ ${#tools_to_install[@]} -gt 0 ]]; then |
|
info "Installing: ${tools_to_install[*]}" |
|
apt-get update -qq |
|
apt-get install -y "${tools_to_install[@]}" |
|
|
|
# Configure fail2ban if installed |
|
if [[ " ${tools_to_install[*]} " =~ " fail2ban " ]] || [[ "${FAIL2BAN_INSTALLED:-false}" == "true" ]]; then |
|
configure_fail2ban |
|
FAIL2BAN_INSTALLED="true" |
|
fi |
|
|
|
log "Additional tools installed successfully" |
|
else |
|
info "No additional tools selected for installation" |
|
fi |
|
|
|
save_state |
|
} |
|
|
|
configure_fail2ban() { |
|
log "Configuring fail2ban..." |
|
|
|
# Create local configuration |
|
cat > /etc/fail2ban/jail.local << 'EOF' |
|
[DEFAULT] |
|
bantime = 3600 |
|
findtime = 600 |
|
maxretry = 5 |
|
destemail = root@localhost |
|
action = %(action_mwl)s |
|
|
|
[sshd] |
|
enabled = true |
|
port = ssh |
|
filter = sshd |
|
logpath = /var/log/auth.log |
|
maxretry = 3 |
|
bantime = 86400 |
|
EOF |
|
|
|
systemctl restart fail2ban |
|
systemctl enable fail2ban |
|
|
|
log "fail2ban configured and started" |
|
} |
|
|
|
verify_setup() { |
|
log "=== Final Verification ===" |
|
info "Running final checks..." |
|
|
|
# Check SSH configuration |
|
echo -e "\n${BLUE}1. SSH Configuration:${NC}" |
|
sshd -T | grep -E "permitrootlogin|passwordauthentication|challengeresponseauthentication" | grep -v "^#" | sed 's/^/ /' |
|
|
|
# Check firewall |
|
echo -e "\n${BLUE}2. Firewall Status:${NC}" |
|
if command -v ufw &> /dev/null; then |
|
sudo ufw status | sed 's/^/ /' |
|
fi |
|
|
|
# Check automatic updates |
|
echo -e "\n${BLUE}3. Automatic Updates:${NC}" |
|
apt-config dump APT::Periodic::Unattended-Upgrade | sed 's/^/ /' |
|
|
|
# Check fail2ban |
|
echo -e "\n${BLUE}4. fail2ban Status:${NC}" |
|
if command -v fail2ban-client &> /dev/null; then |
|
fail2ban-client status sshd | sed 's/^/ /' 2>/dev/null || echo " fail2ban is installed but not active for sshd" |
|
fi |
|
|
|
# Summary |
|
echo -e "\n${GREEN}=== Setup Summary ===${NC}" |
|
echo -e " Non-root user: ${SSH_USER:-Not configured}" |
|
echo -e " SSH key auth: Enabled" |
|
echo -e " Root login: Disabled" |
|
echo -e " Password auth: Disabled" |
|
echo -e " Firewall: ${FIREWALL_CONFIGURED:-false}" |
|
echo -e " Auto updates: ${AUTO_UPDATES_CONFIGURED:-false}" |
|
echo -e " fail2ban: ${FAIL2BAN_INSTALLED:-false}" |
|
echo -e "\n${YELLOW}Log file saved to: $LOG_FILE${NC}" |
|
echo -e "${YELLOW}Configuration state saved to: $STATE_FILE${NC}" |
|
} |
|
|
|
# ============================================================================ |
|
# Main Script Execution |
|
# ============================================================================ |
|
|
|
main() { |
|
echo -e "${GREEN}" |
|
echo "╔═══════════════════════════════════════════════════════════════╗" |
|
echo "║ VPS Hardening Setup ║" |
|
echo "║ Automated Security Script ║" |
|
echo "╚═══════════════════════════════════════════════════════════════╝" |
|
echo -e "${NC}" |
|
|
|
# Pre-flight checks |
|
check_root |
|
check_os |
|
load_state |
|
|
|
# Ensure system is updated |
|
info "Updating package lists..." |
|
apt-get update -qq |
|
|
|
# Main setup sequence |
|
create_nonroot_user |
|
setup_ssh_for_user |
|
disable_root_password_auth |
|
configure_firewall |
|
setup_auto_updates |
|
install_security_tools |
|
|
|
# Final verification |
|
verify_setup |
|
|
|
echo -e "\n${GREEN}✓ VPS hardening completed successfully!${NC}" |
|
echo -e "${YELLOW}Please keep this session open and test SSH in a new terminal:${NC}" |
|
echo -e " ssh ${SSH_USER}@$(curl -s ifconfig.me 2>/dev/null || echo 'your-server-ip')" |
|
echo -e "\n${BLUE}Remember to:${NC}" |
|
echo -e " 1. Save your SSH key in a safe place" |
|
echo -e " 2. Regularly update your system: sudo apt update && sudo apt upgrade" |
|
echo -e " 3. Monitor fail2ban status: sudo fail2ban-client status sshd" |
|
echo -e " 4. Check firewall rules: sudo ufw status verbose" |
|
} |
|
|
|
# Run main function |
|
main "$@" |