#!/usr/bin/env bash # shellcheck disable=SC2162 set -euo pipefail # Debian 13 (Trixie) provisioning — one-link + users-file capable (repo-safe) # - Optional Docker # - Preload users from CSV/URL (USERS_SPEC) + interactive add # - Per-user SSH key install (validated), sudo group add, NOPASSWD (confirm per group) # - Root SSH key (optional) + root password rotation (saved, not printed) # - Special "Niko" helper (full name: Niko Andreopoulos) if you want it # - SSH hardening (after verifying second session) # - UFW + Fail2Ban # - Credentials saved to /root/credentials-.txt (0600) or GPG-encrypted if root key exists # # ENV (optional): # USERS_SPEC= # Users list to preload # ROOT_SSH_PUBKEY="ssh-ed25519 AAAA... root@host" # NIKO_SSH_PUBKEY="ssh-ed25519 AAAA... niko@host" # # SAFE FOR PUBLIC REPO: # - No secrets printed to stdout # - Do NOT commit real keys or credential outputs ################################ # Helpers ################################ prompt_yn() { local q="$1"; local def="${2:-Y}"; local ans prompt case "$def" in Y|y) prompt=" [Y/n] ";; N|n) prompt=" [y/N] ";; *) prompt=" [y/n] ";; esac while true; do read -r -p "$q$prompt" ans || true ans="${ans:-$def}" case "$ans" in Y|y|yes|YES) return 0;; N|n|no|NO) return 1;; *) echo "Please answer y or n.";; esac done } require_root() { if [[ "$(id -u)" -ne 0 ]]; then echo "Please run as root (use sudo)." >&2 exit 1 fi } gen_password() { if command -v openssl >/dev/null 2>&1; then openssl rand -base64 24 | tr -d '/+=' | cut -c1-22 else tr -dc 'A-Za-z0-9' /dev/null 2>&1; then ssh-keygen -l -f - <<<"$key" >/dev/null 2>&1 || return 1 fi return 0 } ensure_pkg_tools() { apt-get update -y apt-get install -y ca-certificates curl gnupg lsb-release openssh-client } ################################ # Docker (optional) ################################ install_docker() { echo "Installing Docker from the official repository..." apt-get remove -y docker docker-engine docker.io containerd runc || true install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/debian/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg chmod a+r /etc/apt/keyrings/docker.gpg echo "Docker GPG key fingerprint:" gpg --show-keys --with-colons /etc/apt/keyrings/docker.gpg | awk -F: '/^fpr:/ {print $10; exit}' if ! prompt_yn "Does this match the official Docker docs?" Y; then echo "Aborting per user choice." exit 1 fi . /etc/os-release CODENAME="${VERSION_CODENAME:-trixie}" echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/debian $CODENAME stable" \ > /etc/apt/sources.list.d/docker.list apt-get update -y apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin systemctl enable --now docker echo "Docker installed and started." } ################################ # State + credential file ################################ declare -A GENERATED_PASSWORDS # username->password declare -A INSTALLED_PUBKEYS # username->yes declare -a CREATED_USERS declare -A GROUP_CONFIRMED # group->confirmed NOPASSWD CRED_FILE="" cred_file_init() { local ts; ts="$(date +%F-%H%M%S)" if command -v gpg >/dev/null 2>&1 && gpg --list-keys root >/dev/null 2>&1; then CRED_FILE="/root/credentials-${ts}.txt.gpg" : > /root/credentials-tmp.txt chmod 600 /root/credentials-tmp.txt else CRED_FILE="/root/credentials-${ts}.txt" : > "$CRED_FILE" chmod 600 "$CRED_FILE" fi } cred_append() { local line="$1" if [[ "$CRED_FILE" == *.gpg ]]; then printf "%s\n" "$line" >> /root/credentials-tmp.txt else printf "%s\n" "$line" >> "$CRED_FILE" fi } cred_file_finalize() { if [[ "$CRED_FILE" == *.gpg ]]; then gpg --yes --batch -r root -e /root/credentials-tmp.txt mv /root/credentials-tmp.txt.gpg "$CRED_FILE" rm -f /root/credentials-tmp.txt chmod 600 "$CRED_FILE" echo "Encrypted credentials written to: $CRED_FILE" else echo "Credentials written to: $CRED_FILE" fi } ################################ # Users & SSH ################################ create_user_if_needed() { local username="$1"; local gecos="$2" if id "$username" &>/dev/null; then echo "User '$username' already exists." else adduser --disabled-password --gecos "$gecos" "$username" CREATED_USERS+=("$username") echo "Created user: $username" fi } set_user_password() { local username="$1" local pw; pw="$(gen_password)" GENERATED_PASSWORDS["$username"]="$pw" echo "${username}:${pw}" | chpasswd cred_append "user ${username} password: ${pw}" } install_user_key() { local username="$1"; local pubkey="$2" if ! validate_pubkey "$pubkey"; then echo "Invalid SSH key for '$username'; skipping." return fi local homedir; homedir=$(getent passwd "$username" | cut -d: -f6) install -d -m 700 -o "$username" -g "$username" "$homedir/.ssh" touch "$homedir/.ssh/authorized_keys" chmod 600 "$homedir/.ssh/authorized_keys" chown "$username:$username" "$homedir/.ssh/authorized_keys" if ! grep -Fxq "$pubkey" "$homedir/.ssh/authorized_keys"; then echo "$pubkey" >> "$homedir/.ssh/authorized_keys" chown "$username:$username" "$homedir/.ssh/authorized_keys" INSTALLED_PUBKEYS["$username"]="yes" echo "Installed SSH key for $username" else echo "Key already present for $username" fi } add_user_to_group_nopasswd() { local username="$1"; local group="$2" [[ -z "${group// /}" ]] && return 0 if ! getent group "$group" >/dev/null; then groupadd "$group" echo "Group '$group' created." fi # Create sudoers drop-in once per group (ask/confirm once) if [[ -z "${GROUP_CONFIRMED[$group]:-}" ]]; then echo "About to grant NOPASSWD:ALL to group '$group'." if prompt_yn "Proceed with passwordless sudo for '$group'?" N; then local sudoers_file="/etc/sudoers.d/${group}-nopasswd" echo "%${group} ALL=(ALL) NOPASSWD:ALL" > "$sudoers_file" chmod 440 "$sudoers_file" visudo -cf "$sudoers_file" >/dev/null GROUP_CONFIRMED[$group]="yes" echo "NOPASSWD sudo enabled for group '$group'." else GROUP_CONFIRMED[$group]="no" echo "Skipped NOPASSWD for group '$group'." fi fi usermod -aG "$group" "$username" echo "Added '$username' to '$group'." } ################################ # SSH hardening / firewall ################################ harden_ssh() { local sshd="/etc/ssh/sshd_config"; local changed=0 echo "Open a SECOND SSH session now and confirm key login works." if ! prompt_yn "Proceed to SSH hardening?" Y; then echo "Skipping SSH hardening." return fi if prompt_yn "Disable root SSH login?" Y; then if grep -qE '^\s*PermitRootLogin' "$sshd"; then sed -ri 's/^\s*PermitRootLogin.*/PermitRootLogin no/' "$sshd" else echo "PermitRootLogin no" >> "$sshd" fi changed=1 fi if prompt_yn "Disable SSH password authentication (key-only)?" Y; then if grep -qE '^\s*PasswordAuthentication' "$sshd"; then sed -ri 's/^\s*PasswordAuthentication.*/PasswordAuthentication no/' "$sshd" else echo "PasswordAuthentication no" >> "$sshd" fi changed=1 fi if prompt_yn "Change SSH port from 22?" N; then read -r -p "New port (1-65535): " newport if [[ "$newport" =~ ^[0-9]+$ ]] && (( newport >= 1 && newport <= 65535 )); then if grep -qE '^\s*Port' "$sshd"; then sed -ri "s/^\s*Port.*/Port $newport/" "$sshd" else echo "Port $newport" >> "$sshd" fi changed=1 echo "SSH port set to $newport." else echo "Invalid port; skipping." fi fi (( changed )) && { systemctl reload ssh || systemctl reload sshd || true; echo "sshd reloaded."; } } setup_ufw() { apt-get install -y ufw ufw default deny incoming ufw default allow outgoing local ssh_port="22" if grep -qE '^\s*Port\s+[0-9]+' /etc/ssh/sshd_config; then ssh_port=$(awk '/^\s*Port/ {print $2; exit}' /etc/ssh/sshd_config) fi ufw allow "$ssh_port"/tcp if prompt_yn "Enable UFW now?" Y; then yes | ufw enable ufw status else echo "UFW not enabled." fi } setup_fail2ban() { apt-get install -y fail2ban systemctl enable --now fail2ban mkdir -p /etc/fail2ban/jail.d cat > /etc/fail2ban/jail.d/ssh.local <<'EOF' [sshd] enabled = true mode = aggressive port = ssh filter = sshd backend = systemd maxretry = 5 findtime = 10m bantime = 1h EOF systemctl restart fail2ban echo "Fail2Ban installed and enabled." } ################################ # Preload: users file ################################ fetch_users_spec() { local spec="$1" [[ -z "${spec:-}" ]] && return 1 if [[ "$spec" =~ ^https?:// ]]; then curl -fsSL "$spec" else cat "$spec" fi } process_users_line() { # Simple CSV: username,Full Name,ssh-key,group # Note: no commas inside fields; lines starting with # are ignored local line="$1" [[ -z "${line// /}" ]] && return 0 [[ "$line" =~ ^# ]] && return 0 IFS=',' read -r username fullname pubkey group <<< "$line" username="$(echo -n "${username:-}" | xargs)" fullname="$(echo -n "${fullname:-}" | xargs)" pubkey="$(echo -n "${pubkey:-}" | sed 's/^[ \t]*//;s/[ \t]*$//')" group="$(echo -n "${group:-}" | xargs)" [[ -z "$username" ]] && { echo "Skipping line (no username): $line"; return 0; } create_user_if_needed "$username" "$fullname" set_user_password "$username" [[ -n "$pubkey" ]] && install_user_key "$username" "$pubkey" [[ -n "$group" ]] && add_user_to_group_nopasswd "$username" "$group" } preload_users_from_spec() { local spec="$1" echo "Preloading users from: $spec" local content content="$(fetch_users_spec "$spec")" || { echo "Failed to read USERS_SPEC '$spec'"; return 1; } while IFS= read -r line; do process_users_line "$line" done <<< "$content" } ################################ # Summary (no secrets printed) ################################ print_summary() { echo echo "========== SUMMARY (no passwords shown) ==========" echo "Users created:" if ((${#CREATED_USERS[@]})); then printf ' - %s\n' "${CREATED_USERS[@]}" else echo " - none" fi echo "SSH keys installed for:" if ((${#INSTALLED_PUBKEYS[@]})); then for u in "${!INSTALLED_PUBKEYS[@]}"; do echo " - $u"; done else echo " - none" fi echo "Sudoers drop-ins:" ls -1 /etc/sudoers.d || true echo "SSH settings:" grep -E '^(PermitRootLogin|PasswordAuthentication|Port)\s' /etc/ssh/sshd_config || true echo "UFW:" ufw status || echo "UFW not installed or inactive." echo "Fail2Ban:" systemctl is-active fail2ban >/dev/null 2>&1 && echo "active" || echo "inactive" echo "Docker:" systemctl is-active docker >/dev/null 2>&1 && echo "active" || echo "not installed/active" echo "Credentials saved to: $CRED_FILE" echo "==================================================" } ################################ # Main ################################ main() { require_root ensure_pkg_tools cred_file_init # Docker if prompt_yn "Install Docker?" N; then install_docker fi # Root SSH key local ROOT_KEY="${ROOT_SSH_PUBKEY:-}" if [[ -z "$ROOT_KEY" ]] && prompt_yn "Install a public SSH key for root?" Y; then echo "Paste root SSH public key (single line):" read ROOT_KEY || true fi if [[ -n "${ROOT_KEY// /}" ]] && validate_pubkey "$ROOT_KEY"; then install -d -m 700 /root/.ssh touch /root/.ssh/authorized_keys chmod 600 /root/.ssh/authorized_keys if ! grep -Fxq "$ROOT_KEY" /root/.ssh/authorized_keys 2>/dev/null; then echo "$ROOT_KEY" >> /root/.ssh/authorized_keys INSTALLED_PUBKEYS["root"]="yes" echo "Installed root SSH key." else echo "Root SSH key already present." fi fi # Root password if prompt_yn "Generate and set a new root password? (saved to credentials file)" Y; then local rpw; rpw="$(gen_password)" GENERATED_PASSWORDS["root"]="$rpw" echo "root:${rpw}" | chpasswd cred_append "root password: ${rpw}" else cred_append "root password: (unchanged)" fi # Niko (optional) if prompt_yn "Create user 'Niko' (full: 'Niko Andreopoulos') and assign your key?" Y; then create_user_if_needed "Niko" "Niko Andreopoulos" set_user_password "Niko" local NIKO_KEY="${NIKO_SSH_PUBKEY:-}" if [[ -z "$NIKO_KEY" ]] && prompt_yn "Paste an SSH public key for Niko?" Y; then echo "Paste SSH public key for Niko (single line):" read NIKO_KEY || true fi [[ -n "${NIKO_KEY// /}" ]] && install_user_key "Niko" "$NIKO_KEY" fi # Preload from USERS_SPEC if [[ -n "${USERS_SPEC:-}" ]]; then preload_users_from_spec "$USERS_SPEC" || true fi # Interactive additional users if prompt_yn "Would you like to add additional users now?" Y; then echo "Enter a comma-separated list (e.g., dev1,dev2), or leave empty to skip:" read others || true if [[ -n "${others// /}" ]]; then IFS=',' read -r -a arr <<< "$others" for u in "${arr[@]}"; do u="$(echo "$u" | xargs)"; [[ -z "$u" ]] && continue read -r -p "Full name for '$u' (optional): " fullname || true create_user_if_needed "$u" "${fullname:-}" set_user_password "$u" if prompt_yn "Add an SSH public key for '$u'?" Y; then echo "Paste SSH public key for $u (single line):" read key || true [[ -n "${key// /}" ]] && install_user_key "$u" "$key" fi if prompt_yn "Add '$u' to a sudo group?" N; then read -r -p "Enter sudo group name: " gname || true [[ -n "${gname// /}" ]] && add_user_to_group_nopasswd "$u" "$gname" fi done fi fi # Security if prompt_yn "Apply SSH hardening (after verifying second session)?" Y; then harden_ssh fi if prompt_yn "Install & configure UFW?" Y; then setup_ufw fi if prompt_yn "Install & enable Fail2Ban?" Y; then setup_fail2ban fi cred_file_finalize print_summary echo "Done." } main "$@"