diff --git a/script b/script index e69de29..89de165 100644 --- a/script +++ b/script @@ -0,0 +1,460 @@ +#!/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 "$@"