Add K3s dev deployment setup for single-node VPS

Mirrors the prod deploy-k3s/ setup but runs all services in-cluster
on a single node: PostgreSQL (replaces Neon), MinIO S3-compatible
storage (replaces B2), Redis, API, worker, and admin.

Includes fully automated setup scripts (00-init through 04-verify),
server hardening (SSH, fail2ban, ufw), Let's Encrypt TLS via Traefik,
network policies, RBAC, and security contexts matching prod.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-03-30 21:30:39 -05:00
parent 00fd674b56
commit 34553f3bec
52 changed files with 5319 additions and 0 deletions

View File

@@ -0,0 +1,146 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=_config.sh
source "${SCRIPT_DIR}/_config.sh"
log() { printf '[setup] %s\n' "$*"; }
die() { printf '[setup][error] %s\n' "$*" >&2; exit 1; }
# --- Local prerequisites ---
command -v kubectl >/dev/null 2>&1 || die "Missing locally: kubectl (https://kubernetes.io/docs/tasks/tools/)"
# --- Server connection ---
SERVER_HOST="$(cfg_require server.host "Server IP or SSH alias")"
SERVER_USER="$(cfg server.user)"
SERVER_USER="${SERVER_USER:-root}"
SSH_KEY="$(cfg server.ssh_key | sed "s|~|${HOME}|g")"
SSH_OPTS=()
if [[ -n "${SSH_KEY}" && -f "${SSH_KEY}" ]]; then
SSH_OPTS+=(-i "${SSH_KEY}")
fi
SSH_OPTS+=(-o StrictHostKeyChecking=accept-new)
ssh_cmd() {
ssh "${SSH_OPTS[@]}" "${SERVER_USER}@${SERVER_HOST}" "$@"
}
log "Testing SSH connection to ${SERVER_USER}@${SERVER_HOST}..."
ssh_cmd "echo 'SSH connection OK'" || die "Cannot SSH into ${SERVER_HOST}"
# --- Server prerequisites ---
log "Setting up server prerequisites..."
ssh_cmd 'bash -s' <<'REMOTE_SETUP'
set -euo pipefail
log() { printf '[setup][remote] %s\n' "$*"; }
# --- System updates ---
log "Updating system packages..."
export DEBIAN_FRONTEND=noninteractive
apt-get update -qq
apt-get upgrade -y -qq
# --- SSH hardening ---
log "Hardening SSH..."
sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sed -i 's/^#\?PermitRootLogin.*/PermitRootLogin prohibit-password/' /etc/ssh/sshd_config
systemctl reload sshd 2>/dev/null || systemctl reload ssh 2>/dev/null || true
# --- fail2ban ---
if ! command -v fail2ban-client >/dev/null 2>&1; then
log "Installing fail2ban..."
apt-get install -y -qq fail2ban
systemctl enable --now fail2ban
else
log "fail2ban already installed"
fi
# --- Unattended security upgrades ---
if ! dpkg -l | grep -q unattended-upgrades; then
log "Installing unattended-upgrades..."
apt-get install -y -qq unattended-upgrades
dpkg-reconfigure -plow unattended-upgrades
else
log "unattended-upgrades already installed"
fi
# --- Firewall (ufw) ---
if command -v ufw >/dev/null 2>&1; then
log "Configuring firewall..."
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp # SSH
ufw allow 443/tcp # HTTPS (Traefik)
ufw allow 6443/tcp # K3s API
ufw allow 80/tcp # HTTP (Let's Encrypt ACME challenge)
ufw --force enable
else
log "Installing ufw..."
apt-get install -y -qq ufw
ufw default deny incoming
ufw default allow outgoing
ufw allow 22/tcp
ufw allow 443/tcp
ufw allow 6443/tcp
ufw allow 80/tcp
ufw --force enable
fi
log "Server prerequisites complete."
REMOTE_SETUP
# --- Install K3s ---
log "Installing K3s on ${SERVER_HOST}..."
log " This takes about 1-2 minutes."
ssh_cmd "curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC='server --secrets-encryption' sh -"
# --- Wait for K3s to be ready ---
log "Waiting for K3s to be ready..."
ssh_cmd "until kubectl get nodes >/dev/null 2>&1; do sleep 2; done"
# --- Copy kubeconfig ---
KUBECONFIG_PATH="${DEPLOY_DIR}/kubeconfig"
log "Copying kubeconfig..."
ssh_cmd "sudo cat /etc/rancher/k3s/k3s.yaml" > "${KUBECONFIG_PATH}"
# Replace 127.0.0.1 with the server's actual IP/hostname
# If SERVER_HOST is an SSH alias, resolve the actual IP
ACTUAL_HOST="${SERVER_HOST}"
if ! echo "${SERVER_HOST}" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then
# Try to resolve from SSH config
RESOLVED="$(ssh -G "${SERVER_HOST}" 2>/dev/null | awk '/^hostname / {print $2}')"
if [[ -n "${RESOLVED}" && "${RESOLVED}" != "${SERVER_HOST}" ]]; then
ACTUAL_HOST="${RESOLVED}"
fi
fi
sed -i.bak "s|https://127.0.0.1:6443|https://${ACTUAL_HOST}:6443|g" "${KUBECONFIG_PATH}"
rm -f "${KUBECONFIG_PATH}.bak"
chmod 600 "${KUBECONFIG_PATH}"
# --- Verify ---
export KUBECONFIG="${KUBECONFIG_PATH}"
log "Verifying cluster..."
kubectl get nodes
log ""
log "K3s installed successfully on ${SERVER_HOST}."
log "Server hardened: SSH key-only, fail2ban, ufw firewall, unattended-upgrades."
log ""
log "Next steps:"
log " export KUBECONFIG=${KUBECONFIG_PATH}"
log " kubectl get nodes"
log " ./scripts/02-setup-secrets.sh"