- materia-supervisor.service: User=root → User=beanflows_service, add PATH so uv (~/.local/bin) is found without a login shell - setup_server.sh: full rewrite — creates beanflows_service (nologin), generates SSH deploy key + age keypair as service user at XDG path (~/.config/sops/age/keys.txt), installs age/sops/rclone as root, prints both public keys + numbered next-step instructions - bootstrap_supervisor.sh: full rewrite — removes GITLAB_READ_TOKEN requirement, clones via SSH as service user, installs uv as service user, decrypts with SOPS auto-discovery, uv sync as service user, systemctl as root - web/deploy.sh: remove self-contained sops/age install + keypair generation; replace with simple sops check (exit if missing) and SOPS auto-discovery decrypt (no explicit key file needed) - infra/readme.md: update architecture diagram for beanflows_service paths, update setup steps to match new scripts Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
162 lines
6.3 KiB
Bash
162 lines
6.3 KiB
Bash
#!/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" <<EOF
|
|
Host gitlab.com
|
|
IdentityFile ${DEPLOY_KEY}
|
|
IdentitiesOnly yes
|
|
EOF
|
|
chmod 600 "${SSH_DIR}/config"
|
|
|
|
sudo -u "${SERVICE_USER}" bash -c \
|
|
"ssh-keyscan -H gitlab.com >> ${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 " <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 "=================================================================="
|