Last active
March 20, 2026 07:51
-
-
Save toolittlecakes/eb9e1ddaf5daac9aacce72a01fb43c1e to your computer and use it in GitHub Desktop.
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
| #!/usr/bin/env bash | |
| set -euo pipefail | |
| # Non-interactive VPS bootstrap EXCEPT the initial SSH password prompt. | |
| # - No root password stored anywhere. | |
| # - Generates an SSH key locally. | |
| # - Configures ~/.ssh/config. | |
| # - SSH'es to root@VPS_IP (you type the password once) and hardens the server. | |
| usage() { | |
| cat <<'EOF' | |
| Usage: | |
| setup_vps.sh --ip <VPS_IP> --name <NAME> | |
| Notes: | |
| - You will be prompted once for the current root password. | |
| - NAME is used as the Linux username and as the Host alias in ~/.ssh/config. | |
| EOF | |
| } | |
| VPS_IP="" | |
| NAME="" | |
| while [[ $# -gt 0 ]]; do | |
| case "$1" in | |
| --ip) | |
| VPS_IP="${2:-}"; shift 2 ;; | |
| --name) | |
| NAME="${2:-}"; shift 2 ;; | |
| -h|--help) | |
| usage; exit 0 ;; | |
| *) | |
| echo "Unknown argument: $1"; usage; exit 2 ;; | |
| esac | |
| done | |
| if [[ -z "$VPS_IP" || -z "$NAME" ]]; then | |
| echo "Missing required arguments." | |
| usage | |
| exit 2 | |
| fi | |
| # Basic input validation (prevents command injection via ssh env assignments) | |
| if ! [[ "$NAME" =~ ^[a-z_][a-z0-9_-]{0,31}$ ]]; then | |
| echo "Invalid --name. Use: [a-z_][a-z0-9_-]{0,31}" | |
| exit 2 | |
| fi | |
| for bin in ssh ssh-keygen ssh-keyscan grep cat chmod mkdir touch base64; do | |
| command -v "$bin" >/dev/null || { echo "Missing dependency: $bin"; exit 1; } | |
| done | |
| KEY_PATH="${KEY_PATH:-$HOME/.ssh/${NAME}_ed25519}" | |
| mkdir -p "$HOME/.ssh" | |
| chmod 700 "$HOME/.ssh" | |
| if [[ -f "$KEY_PATH" ]]; then | |
| echo "Using existing SSH key: $KEY_PATH" | |
| chmod 600 "$KEY_PATH" || true | |
| if [[ ! -f "${KEY_PATH}.pub" ]]; then | |
| echo "Public key missing; regenerating: ${KEY_PATH}.pub" | |
| ssh-keygen -y -f "$KEY_PATH" > "${KEY_PATH}.pub" | |
| fi | |
| chmod 644 "${KEY_PATH}.pub" || true | |
| else | |
| echo "Generating SSH key: $KEY_PATH" | |
| ssh-keygen -t ed25519 -a 64 -f "$KEY_PATH" -N "" -C "${NAME}@${VPS_IP}" | |
| chmod 600 "$KEY_PATH" | |
| chmod 644 "${KEY_PATH}.pub" | |
| fi | |
| # Preload known_hosts to avoid interactive host-key prompts. | |
| touch "$HOME/.ssh/known_hosts" | |
| chmod 600 "$HOME/.ssh/known_hosts" | |
| ssh-keyscan -p 22 -H "$VPS_IP" >> "$HOME/.ssh/known_hosts" 2>/dev/null || true | |
| # Update ~/.ssh/config (append if block doesn't exist) | |
| SSH_CONFIG="$HOME/.ssh/config" | |
| touch "$SSH_CONFIG" | |
| chmod 600 "$SSH_CONFIG" | |
| if ! grep -qE "^[[:space:]]*Host[[:space:]]+$NAME([[:space:]]|\$)" "$SSH_CONFIG"; then | |
| cat >> "$SSH_CONFIG" <<EOF | |
| Host $NAME | |
| HostName $VPS_IP | |
| User $NAME | |
| IdentityFile $KEY_PATH | |
| IdentitiesOnly yes | |
| ForwardAgent no | |
| ServerAliveInterval 30 | |
| ServerAliveCountMax 3 | |
| EOF | |
| echo "Added SSH config host entry: $NAME" | |
| else | |
| echo "SSH config already has a Host entry for: $NAME (leaving as-is)" | |
| fi | |
| # Public key contains spaces; pass it safely as base64. | |
| PUBKEY_B64="$(base64 < "${KEY_PATH}.pub" | tr -d '\n')" | |
| echo "Connecting to root@$VPS_IP (you will be prompted for the root password once)" | |
| echo "Running server setup..." | |
| ssh \ | |
| -o StrictHostKeyChecking=accept-new \ | |
| root@"$VPS_IP" \ | |
| NAME="$NAME" PUBKEY_B64="$PUBKEY_B64" bash -se <<'REMOTE' | |
| set -euo pipefail | |
| PUBKEY="$(printf '%s' "$PUBKEY_B64" | base64 -d)" | |
| export DEBIAN_FRONTEND=noninteractive | |
| export NEEDRESTART_MODE=a | |
| APT_NO_PROMPT=( | |
| -y | |
| -o Dpkg::Use-Pty=0 | |
| -o Dpkg::Options::="--force-confdef" | |
| -o Dpkg::Options::="--force-confold" | |
| ) | |
| apt-get update -y | |
| apt-get "${APT_NO_PROMPT[@]}" install sudo ufw fail2ban unattended-upgrades | |
| if ! id -u "$NAME" >/dev/null 2>&1; then | |
| adduser --disabled-password --gecos "" "$NAME" | |
| fi | |
| # Lock local password (no local password auth for this user) | |
| passwd -l "$NAME" >/dev/null 2>&1 || true | |
| # Sudo without password (risk accepted) | |
| install -d -m 0755 /etc/sudoers.d | |
| SUDO_FILE="/etc/sudoers.d/$NAME" | |
| cat > "$SUDO_FILE" <<EOF | |
| $NAME ALL=(ALL) NOPASSWD:ALL | |
| EOF | |
| chmod 0440 "$SUDO_FILE" | |
| # authorized_keys | |
| HOME_DIR="$(getent passwd "$NAME" | cut -d: -f6)" | |
| install -d -m 0700 -o "$NAME" -g "$NAME" "$HOME_DIR/.ssh" | |
| AUTH_KEYS="$HOME_DIR/.ssh/authorized_keys" | |
| touch "$AUTH_KEYS" | |
| chown "$NAME:$NAME" "$AUTH_KEYS" | |
| chmod 0600 "$AUTH_KEYS" | |
| grep -qxF "$PUBKEY" "$AUTH_KEYS" || printf '%s\n' "$PUBKEY" >> "$AUTH_KEYS" | |
| # SSH hardening via drop-in config (avoid editing main file) | |
| install -d -m 0755 /etc/ssh/sshd_config.d | |
| cat > /etc/ssh/sshd_config.d/99-hardening.conf <<'EOF' | |
| PermitRootLogin no | |
| PasswordAuthentication no | |
| KbdInteractiveAuthentication no | |
| PubkeyAuthentication yes | |
| AllowAgentForwarding no | |
| X11Forwarding no | |
| EOF | |
| sshd -t | |
| systemctl restart ssh | |
| # Firewall | |
| ufw allow 22/tcp | |
| ufw --force enable | |
| # Unattended upgrades (minimal) | |
| cat > /etc/apt/apt.conf.d/20auto-upgrades <<'EOF' | |
| APT::Periodic::Update-Package-Lists "1"; | |
| APT::Periodic::Unattended-Upgrade "1"; | |
| EOF | |
| systemctl enable --now unattended-upgrades >/dev/null 2>&1 || true | |
| # fail2ban | |
| install -d -m 0755 /etc/fail2ban/jail.d | |
| cat > /etc/fail2ban/jail.d/sshd.local <<'EOF' | |
| [sshd] | |
| enabled = true | |
| backend = systemd | |
| maxretry = 5 | |
| findtime = 10m | |
| bantime = 1h | |
| EOF | |
| systemctl enable --now fail2ban | |
| echo "SERVER_SETUP_OK" | |
| REMOTE | |
| echo "Verifying key-based login via ssh config alias: $NAME" | |
| ssh -o BatchMode=yes "$NAME" 'whoami; sudo -n true; echo OK' | |
| echo "Verifying root SSH is disabled (should fail)" | |
| if ssh -o BatchMode=yes root@"$VPS_IP" 'echo SHOULD_NOT_WORK' 2>/dev/null; then | |
| echo "ERROR: root SSH login still works (unexpected)" | |
| exit 1 | |
| fi | |
| echo "DONE" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment