460 lines
14 KiB
Bash
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 "$@"
|