feat(infra): use beanflows_service for supervisor
- 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>
This commit is contained in:
@@ -1,74 +1,161 @@
|
||||
#!/bin/bash
|
||||
# One-time server setup: create data directories, generate age keypair.
|
||||
# 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
|
||||
|
||||
REPO_DIR="/opt/materia"
|
||||
AGE_KEY_FILE="$REPO_DIR/age-key.txt"
|
||||
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"
|
||||
|
||||
if [ "$EUID" -ne 0 ]; then
|
||||
echo "ERROR: This script must be run as root"
|
||||
exit 1
|
||||
[ "$(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
|
||||
|
||||
# ── Create data directories ────────────────────────────────
|
||||
echo "--- Creating data directories ---"
|
||||
mkdir -p /data/materia/landing
|
||||
mkdir -p "$REPO_DIR"
|
||||
echo "Data dir: /data/materia"
|
||||
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 ───────────────────────────────────────────────────────────────────────
|
||||
|
||||
# ── Install age ────────────────────────────────────────────
|
||||
echo "--- Installing age ---"
|
||||
ARCH=$(uname -m)
|
||||
case "$ARCH" in
|
||||
x86_64) ARCH_AGE="amd64" ;;
|
||||
aarch64) ARCH_AGE="arm64" ;;
|
||||
*) echo "Unsupported architecture: $ARCH"; exit 1 ;;
|
||||
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; then
|
||||
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_AGE}" -o /tmp/age.tar.gz
|
||||
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
|
||||
echo "age installed to /usr/local/bin"
|
||||
log "age installed."
|
||||
fi
|
||||
|
||||
# ── Generate age keypair ───────────────────────────────────
|
||||
echo "--- Setting up age keypair ---"
|
||||
if [ -f "$AGE_KEY_FILE" ]; then
|
||||
echo "Keypair already exists at $AGE_KEY_FILE — skipping generation"
|
||||
# ── 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
|
||||
age-keygen -o "$AGE_KEY_FILE" 2>/dev/null
|
||||
chmod 600 "$AGE_KEY_FILE"
|
||||
echo "Generated: $AGE_KEY_FILE"
|
||||
log "Age keypair already exists — skipping."
|
||||
fi
|
||||
|
||||
AGE_PUB=$(grep "public key:" "$AGE_KEY_FILE" | awk '{print $NF}')
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
|
||||
DEPLOY_PUB=$(cat "${DEPLOY_KEY}.pub")
|
||||
AGE_PUB=$(grep "public key:" "${AGE_KEY_FILE}" | awk '{print $NF}')
|
||||
|
||||
echo ""
|
||||
echo "=================================================================="
|
||||
echo " Server age public key:"
|
||||
echo ""
|
||||
echo " $AGE_PUB"
|
||||
echo " SSH deploy key (add to GitLab → Settings → Deploy Keys):"
|
||||
echo ""
|
||||
echo " Add this key to .sops.yaml on your workstation:"
|
||||
echo " ${DEPLOY_PUB}"
|
||||
echo ""
|
||||
echo " creation_rules:"
|
||||
echo " - path_regex: \\.env\\.(dev|prod)\\.sops\$"
|
||||
echo " age: >-"
|
||||
echo " <dev-key>"
|
||||
echo " + $AGE_PUB"
|
||||
echo " Server age public key (add to .sops.yaml):"
|
||||
echo ""
|
||||
echo " Then re-encrypt the prod secrets file:"
|
||||
echo " sops updatekeys .env.prod.sops"
|
||||
echo " git add .sops.yaml .env.prod.sops && git commit -m 'chore: add server age key'"
|
||||
echo " git push"
|
||||
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 " Then run infra/bootstrap_supervisor.sh to complete setup."
|
||||
echo "=================================================================="
|
||||
|
||||
Reference in New Issue
Block a user