#!/usr/bin/env bash # # hardened-deploy.sh - One-shot hardening script for a fresh Ubuntu 22.04/24.04 VPS. # # Run as root over SSH immediately after first login, BEFORE deploying the app: # ssh root@ 'bash -s' < deploy/hardening.sh # # What it does (in order): # 1. Refuses to run unless you are root # 2. Refuses to run if a deploy user already exists (prevents double-run) # 3. Creates a dedicated non-root user with sudo privileges # 4. Disables root SSH login # 5. Disables SSH password authentication (key-based only) # 6. Cleans up unnecessary SSH configuration directives # 7. Configures firewall rules (UFW) - SSH + app ports only # 8. Adds brute-force protection (fail2ban) # 9. Validates sshd_config and reloads the SSH service # 10. Prints SSH log monitoring instructions # set -euo pipefail # ── Configuration ────────────────────────────────────────────────────────── # The non-root user that will own SSH and the deployment. DEPLOY_USER="${DEPLOY_USER:-deploy}" # Ports to open in the firewall besides SSH (space separated). APP_PORTS="${APP_PORTS:-8080}" # SSH port. Change to a non-standard port if you want. SSH_PORT="${SSH_PORT:-22}" # Directory where the deploy user's authorized_keys will be placed. DEPLOY_HOME="/home/${DEPLOY_USER}" # ── Colours for output ───────────────────────────────────────────────────── GREEN='\033[0;32m'; YELLOW='\033[1;33m'; RED='\033[0;31m'; NC='\033[0m' info() { echo -e "${GREEN}[+]${NC} $*"; } warn() { echo -e "${YELLOW}[!]${NC} $*"; } die() { echo -e "${RED}[x]${NC} $*"; exit 1; } # ── Preconditions ────────────────────────────────────────────────────────── [ "$(id -u)" -eq 0 ] || die "Must run as root. Re-run with: sudo bash deploy/hardening.sh" if id "${DEPLOY_USER}" >/dev/null 2>&1; then die "User '${DEPLOY_USER}' already exists. Refusing to re-run hardening (it is not idempotent)." fi if [ ! -f /etc/os-release ]; then die "Cannot detect OS. This script targets Ubuntu 22.04/24.04." fi . /etc/os-release case "${VERSION_ID:-}" in 22.04|24.04) : ;; *) warn "Untested OS: ${PRETTY_NAME:-unknown}. Proceeding anyway." ;; esac # ── 1. Update system packages ────────────────────────────────────────────── info "Updating package lists and upgrading system packages" export DEBIAN_FRONTEND=noninteractive apt-get update -qq apt-get upgrade -y -qq apt-get install -y -qq ufw fail2ban unattended-upgrades apt-listchanges >/dev/null # ── 2. Create the deploy user ────────────────────────────────────────────── info "Creating non-root user '${DEPLOY_USER}' with sudo privileges" useradd --create-home --shell /bin/bash --groups sudo "${DEPLOY_USER}" mkdir -p "${DEPLOY_HOME}/.ssh" chmod 700 "${DEPLOY_HOME}/.ssh" # ── 3. Install your SSH key ──────────────────────────────────────────────── # If you run this via `ssh root@ip 'bash -s'`, the root key may not exist on disk. if [ -f /root/.ssh/authorized_keys ]; then info "Copying root authorized_keys to ${DEPLOY_USER}" cp /root/.ssh/authorized_keys "${DEPLOY_HOME}/.ssh/authorized_keys" else warn "No /root/.ssh/authorized_keys found. Add your key manually:" warn " ssh-copy-id ${DEPLOY_USER}@ (or) " warn " echo 'ssh-ed25519 AAAA... your@email' | sudo tee ${DEPLOY_HOME}/.ssh/authorized_keys" fi chown -R "${DEPLOY_USER}:${DEPLOY_USER}" "${DEPLOY_HOME}/.ssh" chmod 600 "${DEPLOY_HOME}/.ssh/authorized_keys" # ── 4. Back up + rewrite sshd_config ─────────────────────────────────────── info "Hardening SSH configuration" cp /etc/ssh/sshd_config "/etc/ssh/sshd_config.bak.$(date +%Y%m%d%H%M%S)" cat > /etc/ssh/sshd_config < /etc/fail2ban/jail.local </dev/null || true # ── 8. Automatic security updates ────────────────────────────────────────── info "Enabling unattended security upgrades" cat > /etc/apt/apt.conf.d/50unattended-upgrades.local < 2. Only after that login works, exit this session and deploy the app. 3. To watch for brute-force attempts: sudo tail -f /var/log/auth.log | grep -E 'Failed|Invalid user' sudo fail2ban-client status sshd ════════════════════════════════════════════════════════════════════ SUMMARY