#!/bin/bash # One-time server setup: create service user, 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. Generates ed25519 SSH deploy key for GitLab read access # 4. Installs age + sops + rclone to /usr/local/bin (as root) # 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}" log "User OK." # ── 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}" log "Directories OK." # ── 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 sudo -u "${SERVICE_USER}" bash -c "cat > ${SSH_DIR}/config" <> ${SSH_DIR}/known_hosts 2>/dev/null; \ sort -u ${SSH_DIR}/known_hosts -o ${SSH_DIR}/known_hosts" chmod 644 "${SSH_DIR}/known_hosts" log "SSH deploy key OK." # ── 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 # ── 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 " " 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 "=================================================================="