Server-setup-script/install_server_setup

460 lines
14 KiB
Bash

#!/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-<timestamp>.txt (0600) or GPG-encrypted if root key exists
#
# ENV (optional):
# USERS_SPEC=<http(s)://... | /path/to/users.csv> # 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/urandom | head -c22
fi
}
validate_pubkey() {
local key="$1"
[[ -z "${key// /}" ]] && return 1
if command -v ssh-keygen >/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 "$@"