From 7a038b534f0e9f988eeab673dd61089a2e26629c Mon Sep 17 00:00:00 2001 From: beatzaplenty Date: Thu, 23 Jul 2026 10:02:26 +1000 Subject: [PATCH] add Linux admin user setup as part of node hardening New script setup-linux-admin-user.sh creates the Linux system user, installs an SSH authorized key, and adds the user to the sudo group. Integrated into bootstrap.sh before harden-ssh.sh so key-based access is in place before password authentication is disabled. bootstrap.sh now accepts ADMIN_USER and ADMIN_SSH_KEY env vars to run user setup and setup-admin-sudo.sh automatically at the right point. audit.sh checks that at least one non-root user has an authorized key. docs/04-security-hardening.md updated with the new steps and ordering. Co-Authored-By: Claude Sonnet 4.6 --- docs/04-security-hardening.md | 27 ++++++---- scripts/audit.sh | 16 ++++++ scripts/bootstrap.sh | 51 +++++++++++++------ scripts/setup-linux-admin-user.sh | 82 +++++++++++++++++++++++++++++++ 4 files changed, 151 insertions(+), 25 deletions(-) create mode 100755 scripts/setup-linux-admin-user.sh diff --git a/docs/04-security-hardening.md b/docs/04-security-hardening.md index 0b6c355..995d54d 100644 --- a/docs/04-security-hardening.md +++ b/docs/04-security-hardening.md @@ -16,24 +16,31 @@ Then run `scripts/audit.sh` to verify. Order matters (matches | # | Item | Script | Manual step required? | |---|------|--------|------------------------| | 1 | Remove enterprise repos, switch to no-subscription | `switch-to-no-subscription-repo.sh` | no | -| 2 | SSH: key-only root login + fail2ban | `harden-ssh.sh` | no (requires an `authorized_keys` already in place — script warns if missing) | -| 3 | Unattended security upgrades, no auto-reboot | `setup-unattended-upgrades.sh` | no | -| 4 | PVE firewall, default-deny, mgmt-only SSH/8006 | `deploy-firewall.sh` | needs `MGMT_CIDR` set | -| 5 | Disable subscription nag (cosmetic) | `disable-subscription-nag.sh` | no | -| 6 | Named PVE admin user, Administrator role | `create-admin-user.sh ` | yes — pick the username, change the generated password on first login | -| 7 | 2FA/TOTP on that user and `root@pam` | — | yes — web UI only: Datacenter → Permissions → Two Factor, or user menu → TFA | -| 8 | Verify everything above | `audit.sh` | no | +| 2 | Linux admin user with SSH key + sudo access | `setup-linux-admin-user.sh ` | yes — choose a username and provide your SSH public key. Run this **before** SSH hardening or pass `ADMIN_USER`/`ADMIN_SSH_KEY` to `bootstrap.sh` so it runs automatically at the right point | +| 3 | Passwordless sudo for pvesh/qm/pct | `setup-admin-sudo.sh ` | yes — same username as step 2 | +| 4 | SSH: key-only root login + fail2ban | `harden-ssh.sh` | no — step 2 ensures authorized_keys are in place first | +| 5 | Unattended security upgrades, no auto-reboot | `setup-unattended-upgrades.sh` | no | +| 6 | PVE firewall, default-deny, mgmt-only SSH/8006 | `deploy-firewall.sh` | needs `MGMT_CIDR` set | +| 7 | Disable subscription nag (cosmetic) | `disable-subscription-nag.sh` | no | +| 8 | Named PVE admin user, Administrator role | `create-admin-user.sh ` | yes — pick the username, change the generated password on first login | +| 9 | 2FA/TOTP on that user and `root@pam` | — | yes — web UI only: Datacenter → Permissions → Two Factor, or user menu → TFA | +| 10 | Verify everything above | `audit.sh` | no | ## Linux/SSH layer +- A named Linux system user (created by `setup-linux-admin-user.sh`) with an + SSH authorized key and `sudo` group membership is the primary SSH login + account. Root SSH is locked to key-only after `harden-ssh.sh` runs; the + Linux admin user is how you get shell access day-to-day without using root. +- `setup-admin-sudo.sh` adds a narrower, password-free sudoers rule for the + Proxmox management tools (`pvesh`, `qm`, `pct`) specifically — needed for + non-interactive automation scripts that SSH in and run these tools without a + TTY. - `PermitRootLogin prohibit-password` in `sshd_config` — root can only log in via SSH key, never password. Kills most brute-force attempts. - fail2ban jail for SSH on top of that. - Restrict SSH to the management VLAN/trusted IPs via the Proxmox firewall (see `03-networking.md`) rather than exposing broadly. -- A separate Linux sudo user isn't strictly required for day-to-day PVE - admin (the PVE permission system below governs that), but worth adding - if multiple people SSH into the box directly, for accountability. ## PVE/web layer (the one that actually matters day-to-day) diff --git a/scripts/audit.sh b/scripts/audit.sh index a4d293b..f06c500 100755 --- a/scripts/audit.sh +++ b/scripts/audit.sh @@ -69,6 +69,22 @@ if [ -f /var/run/reboot-required ]; then audit_warn "a reboot is pending (/var/run/reboot-required) - schedule one" fi +# --- Linux admin user with SSH key (for non-root SSH login) --- +LINUX_ADMIN_OK=0 +for auth_file in /home/*/.ssh/authorized_keys; do + [ -f "$auth_file" ] || continue + # Must have at least one non-comment, non-empty key line. + if grep -qE '^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) ' "$auth_file" 2>/dev/null; then + LINUX_ADMIN_OK=1 + break + fi +done +if [ "$LINUX_ADMIN_OK" -eq 1 ]; then + audit_pass "a non-root Linux user has an SSH authorized key" +else + audit_fail "no non-root Linux user has an authorized SSH key (run setup-linux-admin-user.sh)" +fi + # --- named admin user (not just root@pam) --- if pveum user list --output-format json 2>/dev/null | grep -q '"userid":"[^"]*@pve"'; then audit_pass "a named @pve admin user exists (root@pam is not the only account)" diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index f50861e..9b82a05 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -2,11 +2,15 @@ # Stage 1 base config + hardening, end to end, for a single fresh PVE host. # Runs the individual scripts in order. Idempotent - safe to re-run. # -# Does NOT create the named admin user (needs a username decision) - run -# create-admin-user.sh separately afterwards. Run audit.sh at the end to -# verify. +# If ADMIN_USER and ADMIN_SSH_KEY are set, a Linux system user is created +# with SSH key access and sudo before SSH hardening runs - so key-based +# login is in place before password auth is disabled. If they are not set, +# a reminder is printed at the end to run setup-linux-admin-user.sh manually +# (but do this BEFORE disconnecting, since password auth will be disabled). # -# Usage: MGMT_CIDR=192.168.2.0/24 ./bootstrap.sh +# Usage: +# MGMT_CIDR=192.168.2.0/24 ./bootstrap.sh +# MGMT_CIDR=192.168.2.0/24 ADMIN_USER=wayne ADMIN_SSH_KEY="ssh-ed25519 ..." ./bootstrap.sh set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" @@ -19,28 +23,45 @@ if [ -z "${MGMT_CIDR:-}" ]; then exit 1 fi -echo "=== 1/5: remove enterprise repos, switch to no-subscription ===" +STEP=0 +next_step() { STEP=$((STEP + 1)); echo; echo "=== ${STEP}: $* ==="; } + +next_step "remove enterprise repos, switch to no-subscription" "${SCRIPT_DIR}/switch-to-no-subscription-repo.sh" -echo -echo "=== 2/5: SSH hardening (key-only root login + fail2ban) ===" +# Create the Linux admin user before SSH hardening so that authorized_keys +# is in place before password auth is disabled. +if [ -n "${ADMIN_USER:-}" ] && [ -n "${ADMIN_SSH_KEY:-}" ]; then + next_step "Linux admin user '${ADMIN_USER}' + SSH key + sudo group" + "${SCRIPT_DIR}/setup-linux-admin-user.sh" "$ADMIN_USER" "$ADMIN_SSH_KEY" + + next_step "passwordless sudo for pvesh/qm/pct (${ADMIN_USER})" + "${SCRIPT_DIR}/setup-admin-sudo.sh" "$ADMIN_USER" +else + echo + echo "WARNING: ADMIN_USER / ADMIN_SSH_KEY not set -- skipping Linux user setup." + echo " Run setup-linux-admin-user.sh and setup-admin-sudo.sh BEFORE disconnecting" + echo " from this session, since the next step disables password authentication." +fi + +next_step "SSH hardening (key-only root login + fail2ban)" "${SCRIPT_DIR}/harden-ssh.sh" -echo -echo "=== 3/5: unattended security upgrades ===" +next_step "unattended security upgrades" "${SCRIPT_DIR}/setup-unattended-upgrades.sh" -echo -echo "=== 4/5: PVE firewall (mgmt-only SSH/8006) ===" +next_step "PVE firewall (mgmt-only SSH/8006)" MGMT_CIDR="$MGMT_CIDR" "${SCRIPT_DIR}/deploy-firewall.sh" -echo -echo "=== 5/5: disable subscription nag (cosmetic) ===" +next_step "disable subscription nag (cosmetic)" "${SCRIPT_DIR}/disable-subscription-nag.sh" echo echo "=== Base hardening applied. Remaining manual/deliberate steps: ===" -echo " - ${SCRIPT_DIR}/create-admin-user.sh " -echo " - ${SCRIPT_DIR}/setup-admin-sudo.sh (passwordless sudo for pvesh/qm/pct)" +if [ -z "${ADMIN_USER:-}" ]; then + echo " - ${SCRIPT_DIR}/setup-linux-admin-user.sh " + echo " - ${SCRIPT_DIR}/setup-admin-sudo.sh (NOPASSWD for pvesh/qm/pct)" +fi +echo " - ${SCRIPT_DIR}/create-admin-user.sh (PVE web UI account)" echo " - Enable 2FA/TOTP for that user and root@pam via the web UI" echo " - ${SCRIPT_DIR}/audit.sh (verify everything above)" diff --git a/scripts/setup-linux-admin-user.sh b/scripts/setup-linux-admin-user.sh new file mode 100755 index 0000000..a920299 --- /dev/null +++ b/scripts/setup-linux-admin-user.sh @@ -0,0 +1,82 @@ +#!/bin/bash +# Create a Linux system user for SSH access and sudo, and install their +# authorized SSH public key. Run this before harden-ssh.sh so that +# key-based access is in place before password authentication is disabled. +# +# Idempotent - safe to re-run. Run as root on the PVE host. +# +# Usage: +# ./setup-linux-admin-user.sh +# ./setup-linux-admin-user.sh --key-file +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "${SCRIPT_DIR}/lib/common.sh" +require_root + +USERNAME="${1:-}" +if [ -z "$USERNAME" ]; then + echo "Usage: $0 " >&2 + echo " $0 --key-file " >&2 + exit 1 +fi + +shift +SSH_KEY="" +if [ "${1:-}" = "--key-file" ]; then + KEY_FILE="${2:-}" + [ -z "$KEY_FILE" ] && { echo "ERROR: --key-file requires a path." >&2; exit 1; } + [ -f "$KEY_FILE" ] || { echo "ERROR: key file not found: $KEY_FILE" >&2; exit 1; } + SSH_KEY="$(cat "$KEY_FILE")" +else + SSH_KEY="${1:-}" +fi + +if [ -z "$SSH_KEY" ]; then + echo "ERROR: an SSH public key is required (key string or --key-file )." >&2 + exit 1 +fi + +if ! echo "$SSH_KEY" | grep -qE '^(ssh-rsa|ssh-ed25519|ecdsa-sha2-nistp[0-9]+) [A-Za-z0-9+/=]'; then + echo "ERROR: argument doesn't look like a valid SSH public key." >&2 + exit 1 +fi + +# --- create Linux user if missing --- +if id "$USERNAME" >/dev/null 2>&1; then + echo "User '${USERNAME}' already exists - skipping useradd." +else + useradd --create-home --shell /bin/bash "$USERNAME" + echo "Created Linux user '${USERNAME}'." +fi + +# --- sudo group membership --- +if id -nG "$USERNAME" | grep -qw sudo; then + echo "User '${USERNAME}' is already in the sudo group." +else + usermod --append --groups sudo "$USERNAME" + echo "Added '${USERNAME}' to the sudo group." +fi + +# --- install authorized SSH key --- +HOME_DIR="$(getent passwd "$USERNAME" | cut -d: -f6)" +SSH_DIR="${HOME_DIR}/.ssh" +AUTH_FILE="${SSH_DIR}/authorized_keys" + +mkdir -p "$SSH_DIR" +chmod 700 "$SSH_DIR" +touch "$AUTH_FILE" +chmod 600 "$AUTH_FILE" +chown -R "${USERNAME}:${USERNAME}" "$SSH_DIR" + +if grep -qF "$SSH_KEY" "$AUTH_FILE" 2>/dev/null; then + echo "SSH key is already present in ${AUTH_FILE}." +else + echo "$SSH_KEY" >> "$AUTH_FILE" + echo "Installed SSH key in ${AUTH_FILE}." +fi + +echo +echo "Linux user '${USERNAME}' is ready for SSH key-based login with sudo access." +echo "Next: run setup-admin-sudo.sh ${USERNAME} to grant NOPASSWD for pvesh/qm/pct."