2650913050
App-Level: - Security-Header (secureHeaders) global; /images/* mit strikter CSP+sandbox → bösartiges SVG kann kein JS im Origin ausführen - Body-Limit 256 KB auf /api/*; Login-Rate-Limit (10/5min) gegen Brute-Force - Upload: 8-MB-Limit + Format-Verifikation (sharp-Metadaten, SVG/GIF-Signatur) - Comment-Längenlimit (10k) gegen DB-Bloat - DB-Fehler nicht mehr roh ausliefern (serverError-Helper) - Profil-PUT koalesziert Hugo-Builds (kein Build-Sturm) Infra: - Container läuft non-root (USER node, uid 1000) + Proxmox-Repo-chown - Ports binden per Default auf 127.0.0.1 (BIND_ADDR-Escape-Hatch) - Kong-CORS auf SITE_URL beschränkt statt "*" - README: Härtungs- + Migrationshinweise Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
195 lines
7.3 KiB
Bash
Executable File
195 lines
7.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
#
|
|
# OPENBUREAU — All-in-One LXC für Proxmox VE (Supabase-Kern + CMS).
|
|
#
|
|
# AUSFÜHREN AUF DEM PROXMOX-HOST (nicht im Container), als root:
|
|
# bash create-openbureau-lxc.sh
|
|
#
|
|
# Legt einen unprivileged Debian-12-LXC an (Docker-fähig: nesting + keyctl),
|
|
# installiert Docker, zieht das Repo, generiert alle Secrets (POSTGRES_PASSWORD,
|
|
# JWT_SECRET, ANON_KEY, SERVICE_ROLE_KEY) und befüllt die .env. Optional baut
|
|
# und startet es den Stack direkt.
|
|
|
|
set -euo pipefail
|
|
|
|
############################ CONFIG ############################
|
|
CTID="${CTID:-$(pvesh get /cluster/nextid)}"
|
|
HOSTNAME="openbureau"
|
|
|
|
# Storage
|
|
TEMPLATE_STORAGE="local"
|
|
ROOTFS_STORAGE="local-lvm"
|
|
DISK_GB="20" # Supabase + CMS
|
|
|
|
# Ressourcen
|
|
RAM_MB="4096"
|
|
SWAP_MB="1024"
|
|
CORES="2"
|
|
|
|
# Netzwerk
|
|
BRIDGE="vmbr0"
|
|
IP="dhcp" # "dhcp" ODER statisch z.B. "192.168.1.50/24"
|
|
GATEWAY="" # nur bei statischer IP
|
|
|
|
# Zugang
|
|
SSH_PUBKEY_FILE="${SSH_PUBKEY_FILE:-$HOME/.ssh/id_ed25519.pub}"
|
|
ROOT_PASSWORD=""
|
|
|
|
# Repo ist öffentlich → Clone braucht KEIN Token.
|
|
# GIT_TOKEN nur setzen, wenn das CMS später per GIT_PUBLISH nach Gitea
|
|
# zurückschreiben soll (git push braucht Auth). Format: "tokenname:tokenwert".
|
|
GIT_TOKEN="${GIT_TOKEN:-}"
|
|
REPO_HOST="git.kgva.ch/karim/OPENBUREAU.git"
|
|
APP_DIR="/opt/openbureau"
|
|
|
|
# Admin (sieht/bearbeitet ALLE Beiträge). Wird auch als erster Login-User vorgeschlagen.
|
|
ADMIN_EMAIL="${ADMIN_EMAIL:-karim@gabrielevarano.ch}"
|
|
|
|
# Stack nach dem Setup direkt bauen + starten?
|
|
COMPOSE_UP="true"
|
|
##################################################################
|
|
|
|
say() { echo -e "\n\033[1;36m▸ $*\033[0m"; }
|
|
|
|
# --- 0. Interaktiv nachfragen (Enter = Default) --------------------------
|
|
# Übersprungen, wenn kein Terminal (z.B. CI) — dann gelten die Defaults oben
|
|
# bzw. vorab gesetzte Umgebungsvariablen.
|
|
if [ -t 0 ]; then
|
|
echo "OPENBUREAU LXC-Setup — Enter übernimmt den Default in [Klammern]."
|
|
read -rp " Storage für die Disk [${ROOTFS_STORAGE}]: " _x; ROOTFS_STORAGE="${_x:-$ROOTFS_STORAGE}"
|
|
read -rp " Netzwerk-Bridge [${BRIDGE}]: " _x; BRIDGE="${_x:-$BRIDGE}"
|
|
read -rp " IP (dhcp | x.x.x.x/24) [${IP}]: " _x; IP="${_x:-$IP}"
|
|
[ "$IP" != "dhcp" ] && { read -rp " Gateway: " GATEWAY; }
|
|
read -rp " Admin-E-Mail [${ADMIN_EMAIL}]: " _x; ADMIN_EMAIL="${_x:-$ADMIN_EMAIL}"
|
|
fi
|
|
|
|
# --- 1. Template sicherstellen -------------------------------------------
|
|
say "Suche aktuelles Debian-12-Template…"
|
|
pveam update >/dev/null || true
|
|
TEMPLATE="$(pveam available --section system \
|
|
| awk '/debian-12-standard/{print $2}' | sort -V | tail -1)"
|
|
[ -n "$TEMPLATE" ] || { echo "Kein debian-12-Template gefunden."; exit 1; }
|
|
if ! pveam list "$TEMPLATE_STORAGE" | grep -q "$TEMPLATE"; then
|
|
say "Lade Template $TEMPLATE…"
|
|
pveam download "$TEMPLATE_STORAGE" "$TEMPLATE"
|
|
fi
|
|
TEMPLATE_REF="${TEMPLATE_STORAGE}:vztmpl/${TEMPLATE}"
|
|
|
|
# --- 2. Netzwerk ----------------------------------------------------------
|
|
if [ "$IP" = "dhcp" ]; then
|
|
NET="name=eth0,bridge=${BRIDGE},ip=dhcp"
|
|
else
|
|
[ -n "$GATEWAY" ] || { echo "Statische IP, aber GATEWAY leer."; exit 1; }
|
|
NET="name=eth0,bridge=${BRIDGE},ip=${IP},gw=${GATEWAY}"
|
|
fi
|
|
|
|
# --- 3. Container erstellen ----------------------------------------------
|
|
say "Erstelle LXC $CTID ($HOSTNAME)…"
|
|
CREATE_ARGS=(
|
|
"$CTID" "$TEMPLATE_REF"
|
|
--hostname "$HOSTNAME"
|
|
--cores "$CORES" --memory "$RAM_MB" --swap "$SWAP_MB"
|
|
--rootfs "${ROOTFS_STORAGE}:${DISK_GB}"
|
|
--net0 "$NET"
|
|
--unprivileged 1
|
|
--features "nesting=1,keyctl=1"
|
|
--onboot 1
|
|
)
|
|
[ -n "$ROOT_PASSWORD" ] && CREATE_ARGS+=(--password "$ROOT_PASSWORD")
|
|
[ -f "$SSH_PUBKEY_FILE" ] && CREATE_ARGS+=(--ssh-public-keys "$SSH_PUBKEY_FILE")
|
|
pct create "${CREATE_ARGS[@]}"
|
|
|
|
say "Starte Container…"
|
|
pct start "$CTID"
|
|
sleep 5
|
|
|
|
# Clone-URL (mit Token, falls gesetzt)
|
|
if [ -n "$GIT_TOKEN" ]; then
|
|
REPO_URL="https://${GIT_TOKEN}@${REPO_HOST}"
|
|
else
|
|
REPO_URL="https://${REPO_HOST}"
|
|
fi
|
|
|
|
# --- 4. Provisionierung im Container -------------------------------------
|
|
say "Installiere Docker + Git, ziehe Repo, generiere Secrets…"
|
|
pct exec "$CTID" -- bash -euo pipefail -c "
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
apt-get update -qq
|
|
apt-get install -y -qq ca-certificates curl git openssl >/dev/null
|
|
curl -fsSL https://get.docker.com | sh >/dev/null
|
|
systemctl enable --now docker
|
|
|
|
if [ ! -d '${APP_DIR}/.git' ]; then
|
|
git clone --quiet '${REPO_URL}' '${APP_DIR}' || {
|
|
echo 'WARN: Clone fehlgeschlagen (Token nötig?). Setup hier gestoppt.'; exit 0; }
|
|
fi
|
|
|
|
cd '${APP_DIR}/cms'
|
|
if [ ! -f .env ]; then
|
|
cp .env.example .env
|
|
|
|
# Secrets generieren
|
|
PW=\$(openssl rand -hex 32)
|
|
JWT=\$(openssl rand -hex 32)
|
|
sed -i \"s|^POSTGRES_PASSWORD=.*|POSTGRES_PASSWORD=\${PW}|\" .env
|
|
sed -i \"s|^JWT_SECRET=.*|JWT_SECRET=\${JWT}|\" .env
|
|
|
|
# ANON_KEY + SERVICE_ROLE_KEY via Wegwerf-Node-Container ableiten
|
|
KEYS=\$(docker run --rm -v \"\$PWD\":/w -w /w node:20-alpine \
|
|
node scripts/generate-keys.mjs \"\$JWT\" 2>/dev/null)
|
|
ANON=\$(echo \"\$KEYS\" | sed -n 's/^ANON_KEY=//p')
|
|
SVC=\$(echo \"\$KEYS\" | sed -n 's/^SERVICE_ROLE_KEY=//p')
|
|
sed -i \"s|^ANON_KEY=.*|ANON_KEY=\${ANON}|\" .env
|
|
sed -i \"s|^SERVICE_ROLE_KEY=.*|SERVICE_ROLE_KEY=\${SVC}|\" .env
|
|
|
|
# URLs auf die Container-IP setzen
|
|
HOSTIP=\$(hostname -I | awk '{print \$1}')
|
|
sed -i \"s|^SITE_URL=.*|SITE_URL=http://\${HOSTIP}:8080|\" .env
|
|
sed -i \"s|^API_EXTERNAL_URL=.*|API_EXTERNAL_URL=http://\${HOSTIP}:8000|\" .env
|
|
sed -i \"s|^ADMIN_EMAILS=.*|ADMIN_EMAILS=${ADMIN_EMAIL}|\" .env
|
|
# Out-of-box LAN-Direktzugriff (kein Reverse-Proxy) → auf allen Interfaces
|
|
# lauschen. Für Domain/HTTPS hinter Proxy: BIND_ADDR=127.0.0.1 setzen.
|
|
sed -i \"s|^BIND_ADDR=.*|BIND_ADDR=0.0.0.0|\" .env
|
|
# CORS auf die Browser-Origin (= SITE_URL) festnageln statt „*\".
|
|
sed -i \"s|__CORS_ORIGIN__|http://\${HOSTIP}:8080|g\" kong.yml
|
|
echo 'OK: .env generiert.'
|
|
fi
|
|
|
|
# Der CMS-Container läuft als non-root (uid 1000). Das gemountete Repo muss
|
|
# ihm gehören, damit Hugo public/ bauen und content/ schreiben kann.
|
|
chown -R 1000:1000 '${APP_DIR}'
|
|
|
|
if [ '${COMPOSE_UP}' = 'true' ]; then
|
|
echo '→ Baue + starte Stack (dauert beim ersten Mal ein paar Minuten)…'
|
|
docker compose up -d --build
|
|
fi
|
|
"
|
|
|
|
# --- 5. Abschluss --------------------------------------------------------
|
|
IPADDR="$(pct exec "$CTID" -- hostname -I 2>/dev/null | awk '{print $1}')"
|
|
say "Fertig. LXC $CTID läuft${IPADDR:+ unter $IPADDR}."
|
|
|
|
cat <<EOF
|
|
|
|
Admin: http://${IPADDR:-<ip>}:8080/admin/
|
|
Live: http://${IPADDR:-<ip>}:8080/
|
|
Supabase: http://${IPADDR:-<ip>}:8000 (nur API-Gateway, keine Web-UI — / gibt 404, ist normal)
|
|
|
|
Login-User anlegen (im Container, nach dem Start):
|
|
pct enter ${CTID}
|
|
cd ${APP_DIR}/cms
|
|
source .env
|
|
curl -s -X POST "http://localhost:8000/auth/v1/admin/users" \\
|
|
-H "apikey: \$SERVICE_ROLE_KEY" \\
|
|
-H "Authorization: Bearer \$SERVICE_ROLE_KEY" \\
|
|
-H "Content-Type: application/json" \\
|
|
-d '{"email":"${ADMIN_EMAIL}","password":"DEIN-PASSWORT","email_confirm":true}'
|
|
|
|
Hinweise:
|
|
• :8000 ist das Supabase-API-Gateway (Kong), keine Web-Oberfläche.
|
|
Das Admin-Login (:8080/admin/) spricht im Hintergrund damit.
|
|
• Für Domain/HTTPS: SITE_URL + API_EXTERNAL_URL in .env auf die
|
|
öffentliche Adresse setzen und 'docker compose up -d --build' neu.
|
|
• Logs: pct enter ${CTID}; cd ${APP_DIR}/cms; docker compose logs -f
|
|
EOF
|