Skip to content

Instantly share code, notes, and snippets.

@toolittlecakes
Last active March 20, 2026 07:51
Show Gist options
  • Select an option

  • Save toolittlecakes/eb9e1ddaf5daac9aacce72a01fb43c1e to your computer and use it in GitHub Desktop.

Select an option

Save toolittlecakes/eb9e1ddaf5daac9aacce72a01fb43c1e to your computer and use it in GitHub Desktop.
#!/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