- setup_server.sh: add git/curl/ca-certificates apt install, add uv install as service user, fix SSH config write (root + chown vs sudo heredoc), remove noise log lines after set -e makes them redundant - bootstrap_supervisor.sh: remove all tool installs (apt, uv, sops, age) — setup_server.sh is now the single source of truth; strip to ~45 lines: age-key check, clone/fetch, tag checkout, decrypt, uv sync, systemd enable - readme.md: update step 1 and step 3 descriptions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
173 lines
7.0 KiB
Bash
173 lines
7.0 KiB
Bash
#!/bin/bash
|
|
# One-time server setup: create service user, install tools, SSH deploy key, age keypair.
|
|
# Run as root on a fresh Hetzner server before running bootstrap_supervisor.sh.
|
|
#
|
|
# Usage:
|
|
# bash infra/setup_server.sh
|
|
#
|
|
# What it does:
|
|
# 1. Creates beanflows_service user (nologin) + adds to docker group
|
|
# 2. Creates /opt/materia + /data/materia/landing with correct ownership
|
|
# 3. Installs git, curl, age, sops, rclone, uv
|
|
# 4. Generates ed25519 SSH deploy key for GitLab read access
|
|
# 5. Generates age keypair at ~/.config/sops/age/keys.txt (as service user)
|
|
# 6. Prints both public keys + numbered next-step instructions
|
|
|
|
set -euo pipefail
|
|
|
|
SERVICE_USER="beanflows_service"
|
|
APP_DIR="/opt/materia"
|
|
DATA_DIR="/data/materia"
|
|
SSH_DIR="/home/${SERVICE_USER}/.ssh"
|
|
DEPLOY_KEY="${SSH_DIR}/materia_deploy"
|
|
SOPS_AGE_DIR="/home/${SERVICE_USER}/.config/sops/age"
|
|
|
|
[ "$(id -u)" = "0" ] || { echo "ERROR: Run as root: sudo bash infra/setup_server.sh"; exit 1; }
|
|
|
|
log() { echo "$(date '+%H:%M:%S') ==> $*"; }
|
|
|
|
# ── Service user ──────────────────────────────────────────────────────────────
|
|
|
|
log "Creating service user ${SERVICE_USER}..."
|
|
if ! id "${SERVICE_USER}" >/dev/null 2>&1; then
|
|
useradd --system --create-home --shell /usr/sbin/nologin "${SERVICE_USER}"
|
|
fi
|
|
usermod -aG docker "${SERVICE_USER}"
|
|
|
|
# ── Directories ───────────────────────────────────────────────────────────────
|
|
|
|
log "Creating directories..."
|
|
mkdir -p "${APP_DIR}" "${DATA_DIR}/landing"
|
|
chown "${SERVICE_USER}:${SERVICE_USER}" "${APP_DIR}"
|
|
chown -R "${SERVICE_USER}:${SERVICE_USER}" "${DATA_DIR}"
|
|
|
|
# ── System tools ──────────────────────────────────────────────────────────────
|
|
|
|
log "Installing system tools..."
|
|
apt-get update -q
|
|
apt-get install -y -q git curl ca-certificates
|
|
|
|
# ── SSH deploy key ────────────────────────────────────────────────────────────
|
|
|
|
log "Setting up SSH deploy key..."
|
|
sudo -u "${SERVICE_USER}" mkdir -p "${SSH_DIR}"
|
|
chmod 700 "${SSH_DIR}"
|
|
|
|
if [ ! -f "${DEPLOY_KEY}" ]; then
|
|
sudo -u "${SERVICE_USER}" ssh-keygen -t ed25519 \
|
|
-f "${DEPLOY_KEY}" -N "" -C "materia-deploy"
|
|
fi
|
|
|
|
cat > "${SSH_DIR}/config" <<EOF
|
|
Host gitlab.com
|
|
IdentityFile ${DEPLOY_KEY}
|
|
IdentitiesOnly yes
|
|
EOF
|
|
chown "${SERVICE_USER}:${SERVICE_USER}" "${SSH_DIR}/config"
|
|
chmod 600 "${SSH_DIR}/config"
|
|
|
|
ssh-keyscan -H gitlab.com >> "${SSH_DIR}/known_hosts" 2>/dev/null
|
|
sort -u "${SSH_DIR}/known_hosts" -o "${SSH_DIR}/known_hosts"
|
|
chown "${SERVICE_USER}:${SERVICE_USER}" "${SSH_DIR}/known_hosts"
|
|
chmod 644 "${SSH_DIR}/known_hosts"
|
|
|
|
# ── age ───────────────────────────────────────────────────────────────────────
|
|
|
|
ARCH=$(uname -m)
|
|
case "${ARCH}" in
|
|
x86_64) ARCH_TAG="amd64" ;;
|
|
aarch64) ARCH_TAG="arm64" ;;
|
|
*) echo "Unsupported architecture: ${ARCH}"; exit 1 ;;
|
|
esac
|
|
|
|
if ! command -v age-keygen >/dev/null 2>&1; then
|
|
log "Installing age..."
|
|
AGE_VERSION="v1.3.1"
|
|
curl -fsSL "https://dl.filippo.io/age/${AGE_VERSION}?for=linux/${ARCH_TAG}" -o /tmp/age.tar.gz
|
|
tar -xzf /tmp/age.tar.gz -C /usr/local/bin --strip-components=1 age/age age/age-keygen
|
|
chmod +x /usr/local/bin/age /usr/local/bin/age-keygen
|
|
rm /tmp/age.tar.gz
|
|
log "age installed."
|
|
fi
|
|
|
|
# ── sops ──────────────────────────────────────────────────────────────────────
|
|
|
|
if ! command -v sops >/dev/null 2>&1; then
|
|
log "Installing sops..."
|
|
SOPS_VERSION="v3.12.1"
|
|
curl -fsSL \
|
|
"https://github.com/getsops/sops/releases/download/${SOPS_VERSION}/sops-${SOPS_VERSION}.linux.${ARCH_TAG}" \
|
|
-o /usr/local/bin/sops
|
|
chmod +x /usr/local/bin/sops
|
|
log "sops installed."
|
|
fi
|
|
|
|
# ── rclone ────────────────────────────────────────────────────────────────────
|
|
|
|
if ! command -v rclone >/dev/null 2>&1; then
|
|
log "Installing rclone..."
|
|
curl -fsSL https://rclone.org/install.sh | bash
|
|
log "rclone installed."
|
|
fi
|
|
|
|
# ── age keypair (as service user, XDG path auto-discovered by SOPS) ───────────
|
|
|
|
log "Generating age keypair for ${SERVICE_USER}..."
|
|
AGE_KEY_FILE="${SOPS_AGE_DIR}/keys.txt"
|
|
if [ ! -f "${AGE_KEY_FILE}" ]; then
|
|
sudo -u "${SERVICE_USER}" mkdir -p "${SOPS_AGE_DIR}"
|
|
sudo -u "${SERVICE_USER}" age-keygen -o "${AGE_KEY_FILE}"
|
|
chmod 600 "${AGE_KEY_FILE}"
|
|
log "Age keypair generated at ${AGE_KEY_FILE}"
|
|
else
|
|
log "Age keypair already exists — skipping."
|
|
fi
|
|
|
|
# ── uv (installed as service user) ────────────────────────────────────────────
|
|
|
|
if [ ! -f "/home/${SERVICE_USER}/.local/bin/uv" ]; then
|
|
log "Installing uv..."
|
|
sudo -u "${SERVICE_USER}" bash -c 'curl -LsSf https://astral.sh/uv/install.sh | sh'
|
|
fi
|
|
|
|
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
|
|
DEPLOY_PUB=$(cat "${DEPLOY_KEY}.pub")
|
|
AGE_PUB=$(grep "public key:" "${AGE_KEY_FILE}" | awk '{print $NF}')
|
|
|
|
echo ""
|
|
echo "=================================================================="
|
|
echo ""
|
|
echo " SSH deploy key (add to GitLab → Settings → Deploy Keys):"
|
|
echo ""
|
|
echo " ${DEPLOY_PUB}"
|
|
echo ""
|
|
echo " Server age public key (add to .sops.yaml):"
|
|
echo ""
|
|
echo " ${AGE_PUB}"
|
|
echo ""
|
|
echo "=================================================================="
|
|
echo ""
|
|
echo " Next steps:"
|
|
echo ""
|
|
echo " 1. Add the SSH deploy key to GitLab:"
|
|
echo " → Repository Settings → Deploy Keys → Add key (read-only)"
|
|
echo ""
|
|
echo " 2. Add the age public key to .sops.yaml on your workstation:"
|
|
echo " creation_rules:"
|
|
echo " - path_regex: \\.env\\.(dev|prod)\\.sops\$"
|
|
echo " age: >-"
|
|
echo " <dev-key>"
|
|
echo " + ${AGE_PUB}"
|
|
echo ""
|
|
echo " 3. Re-encrypt prod secrets to include the server key:"
|
|
echo " sops updatekeys .env.prod.sops"
|
|
echo " git add .sops.yaml .env.prod.sops"
|
|
echo " git commit -m 'chore: add server age key'"
|
|
echo " git push"
|
|
echo ""
|
|
echo " 4. Run bootstrap:"
|
|
echo " bash infra/bootstrap_supervisor.sh"
|
|
echo ""
|
|
echo "=================================================================="
|