Files
patrickandClaude Sonnet 5 02038866a9
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
OPS-12: systemd haertung und db rollentrennung (F-15)
10 bisher ungehaertete Archive-Backup/Reconcile/Scrub-Oneshot-Vorlagen
(deploy/systemd/) bekommen die etablierten Sandbox-Direktiven
(NoNewPrivileges, ProtectSystem=strict, ProtectHome, PrivateTmp, ggf.
ReadWritePaths). Drei Vorlagen ohne Dateisystemzugriff (reconcile,
retention-notify, scrub) bekommen bewusst keine ReadWritePaths,
begruendet dokumentiert (S3-kompatible Storage-Abstraktion statt
lokaler Pfade).

Neue, getrennte Postgres-Laufzeit-Rolle (nexarch_tenant_runtime, OHNE
CREATEDB) statt der bisher gemeinsam genutzten CREATEDB-Rolle fuer
NEXARCH_TENANT_DSN_TEMPLATE. Provisioner.Provision() berechtigt die
neue Rolle je frisch angelegter Tenant-DB (GRANT CONNECT + Schema-
Rechte inkl. Default-Privileges), Tenant.DBDSN wird jetzt mit dieser
Rolle statt der CREATEDB-Rolle gebaut. Neue Umgebungsvariable
NEXARCH_TENANT_RUNTIME_DSN_TEMPLATE, deploy/install.sh legt die Rolle
automatisch an.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-26 22:34:51 +02:00

218 lines
11 KiB
Bash
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env bash
# NEXARCH Core – Installer/Deployer fuer eine Debian/systemd-Zielumgebung.
#
# Baut alle Core-Binaries (cmd/core + die *-devserver), legt Postgres-Rolle
# und Registry-Datenbank an (falls nicht vorhanden), wendet die
# Registry-Migrationen an, generiert Secrets (falls nicht vorhanden) und
# installiert+startet jeden Dienst als eigenen systemd-Service.
#
# Idempotent: mehrfacher Aufruf ist sicher — vorhandene Rolle/DB/Secrets
# werden nicht ueberschrieben, Migrationen nur angewendet, wenn die
# Registry-DB noch leer ist (siehe Abschnitt "Migrationen" unten).
#
# Aufruf (als root): ./deploy/install.sh
#
# Ueberschreibbar per Umgebungsvariable:
# NEXARCH_INSTALL_DIR (Default: /opt/nexarch-core)
# NEXARCH_PG_ROLE (Default: nexarch_core)
# NEXARCH_PG_REGISTRY_ROLE (Default: nexarch_registry)
# NEXARCH_PG_REGISTRY_DB (Default: nexarch_registry)
# NEXARCH_BRANCH (Default: aktueller Branch dieses Checkouts)
#
# TEN-09 (Registry-Haertung): es gibt zwei getrennte Postgres-Rollen statt
# einer gemeinsamen Superuser-aehnlichen Rolle (der Keycloak-Fehler, den
# das Ticket ausdruecklich vermeiden soll):
# - NEXARCH_PG_ROLE (nexarch_core): hat CREATEDB, wird NUR fuer
# NEXARCH_ADMIN_DSN/NEXARCH_TENANT_DSN_TEMPLATE genutzt (Tenant-DB-
# Provisioning/Zugriff, TEN-01). Diese Rolle wird NIEMALS fuer die
# Registry-Verbindung selbst verwendet.
# - NEXARCH_PG_REGISTRY_ROLE (nexarch_registry): Eigentuemerin der
# Registry-Datenbank, OHNE CREATEDB, wird fuer NEXARCH_REGISTRY_DSN
# genutzt. Da neu angelegte Tenant-Datenbanken PUBLIC das CONNECT-Recht
# explizit entziehen (siehe internal/tenant/provisioner.go), hat diese
# Rolle keinerlei Zugriff auf Tenant-Datenbanken (Akzeptanzkriterium 1).
set -euo pipefail
INSTALL_DIR="${NEXARCH_INSTALL_DIR:-/opt/nexarch-core}"
PG_ROLE="${NEXARCH_PG_ROLE:-nexarch_core}"
PG_REGISTRY_ROLE="${NEXARCH_PG_REGISTRY_ROLE:-nexarch_registry}"
PG_TENANT_RUNTIME_ROLE="${NEXARCH_PG_TENANT_RUNTIME_ROLE:-nexarch_tenant_runtime}"
PG_DB="${NEXARCH_PG_REGISTRY_DB:-nexarch_registry}"
ENV_FILE=/etc/nexarch/core.env
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
if [ "$(id -u)" -ne 0 ]; then
echo "Bitte als root ausfuehren (legt Systembenutzer, systemd-Units und Postgres-Rolle an)." >&2
exit 1
fi
echo "== 1. Systembenutzer =="
if ! id nexarch >/dev/null 2>&1; then
useradd --system --no-create-home --shell /usr/sbin/nologin nexarch
echo "Benutzer 'nexarch' angelegt."
else
echo "Benutzer 'nexarch' existiert bereits."
fi
echo "== 2. Postgres-Rollen und Registry-Datenbank =="
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname = '${PG_ROLE}'" | grep -q 1; then
PG_PASSWORD="$(openssl rand -hex 24)"
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "CREATE ROLE ${PG_ROLE} WITH LOGIN PASSWORD '${PG_PASSWORD}';"
echo "Rolle '${PG_ROLE}' angelegt."
else
echo "Rolle '${PG_ROLE}' existiert bereits — Passwort wird NICHT neu gesetzt (siehe ${ENV_FILE} fuer das vorhandene)."
PG_PASSWORD=""
fi
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname = '${PG_REGISTRY_ROLE}'" | grep -q 1; then
PG_REGISTRY_PASSWORD="$(openssl rand -hex 24)"
# Bewusst OHNE CREATEDB — diese Rolle darf ausschliesslich die Registry-DB
# nutzen, nie eine Tenant-Datenbank anlegen oder verwalten (Akzeptanz-
# kriterium 1 / "bekannte Fehler vermeiden": keine gemeinsame Rolle mit
# Tenant-Datenbankzugriff).
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "CREATE ROLE ${PG_REGISTRY_ROLE} WITH LOGIN PASSWORD '${PG_REGISTRY_PASSWORD}' NOCREATEDB;"
echo "Rolle '${PG_REGISTRY_ROLE}' angelegt."
else
echo "Rolle '${PG_REGISTRY_ROLE}' existiert bereits — Passwort wird NICHT neu gesetzt (siehe ${ENV_FILE} fuer das vorhandene)."
PG_REGISTRY_PASSWORD=""
fi
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_roles WHERE rolname = '${PG_TENANT_RUNTIME_ROLE}'" | grep -q 1; then
PG_TENANT_RUNTIME_PASSWORD="$(openssl rand -hex 24)"
# OPS-12/F-15: separate Rolle OHNE CREATEDB fuer die laufende
# Tenant-DB-Verbindung aller Dienste -- die CREATEDB-faehige Rolle
# (PG_ROLE) wird nur noch intern beim Provisioning verwendet, nie mehr
# an konsumierende Dienste weitergegeben.
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "CREATE ROLE ${PG_TENANT_RUNTIME_ROLE} WITH LOGIN PASSWORD '${PG_TENANT_RUNTIME_PASSWORD}' NOCREATEDB;"
echo "Rolle '${PG_TENANT_RUNTIME_ROLE}' angelegt."
else
echo "Rolle '${PG_TENANT_RUNTIME_ROLE}' existiert bereits — Passwort wird NICHT neu gesetzt (siehe ${ENV_FILE} fuer das vorhandene)."
PG_TENANT_RUNTIME_PASSWORD=""
fi
if ! sudo -u postgres psql -tAc "SELECT 1 FROM pg_database WHERE datname = '${PG_DB}'" | grep -q 1; then
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "CREATE DATABASE ${PG_DB} OWNER ${PG_REGISTRY_ROLE};"
echo "Datenbank '${PG_DB}' angelegt (Eigentuemerin: ${PG_REGISTRY_ROLE})."
else
echo "Datenbank '${PG_DB}' existiert bereits."
fi
# Fuer Tenant-Provisioning (CREATE DATABASE pro Mandant, Modell C) braucht
# NUR die Admin-/Provisioning-Rolle CREATEDB-Recht — NICHT die Registry-Rolle.
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "ALTER ROLE ${PG_ROLE} WITH CREATEDB;" >/dev/null
# Verteidigungslinie 2: selbst falls die Registry-Rolle irgendwann doch
# CONNECT-Rechte auf einer Tenant-Datenbank erhalten sollte, bleibt sie ohne
# CREATEDB unfaehig, selbst Tenant-Datenbanken anzulegen/zu uebernehmen.
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "ALTER ROLE ${PG_REGISTRY_ROLE} WITH NOCREATEDB;" >/dev/null
sudo -u postgres psql -v ON_ERROR_STOP=1 -c "ALTER ROLE ${PG_TENANT_RUNTIME_ROLE} WITH NOCREATEDB;" >/dev/null
echo "== 3. Secrets/Konfiguration =="
mkdir -p /etc/nexarch
if [ ! -f "$ENV_FILE" ]; then
if [ -z "${PG_PASSWORD}" ] || [ -z "${PG_REGISTRY_PASSWORD}" ] || [ -z "${PG_TENANT_RUNTIME_PASSWORD}" ]; then
echo "FEHLER: mindestens eine Rolle existierte bereits, aber ${ENV_FILE} fehlt — Passwort kann nicht rekonstruiert werden." >&2
echo "Entweder ${ENV_FILE} von Hand mit den bekannten Passwoertern anlegen, oder die Rollen droppen und erneut installieren." >&2
exit 1
fi
AUDIT_ADMIN_TOKEN="$(openssl rand -hex 32)"
# QA-15 F-01..F-03: Dienst-Token fuer interne Admin-Endpunkte (ohne sie starten die Dienste nicht).
CORE_ADMIN_TOKEN="$(openssl rand -hex 32)"
LICADMIN_ADMIN_TOKEN="$(openssl rand -hex 32)"
TENANTADMIN_ADMIN_TOKEN="$(openssl rand -hex 32)"
# Ed25519-Schluesselpaar fuer die Lizenzsignatur (LIC-01/LIC-04):
# licadmin-devserver prueft Lizenzen nur mit dem OEFFENTLICHEN Schluessel,
# der private Schluessel wird NICHT deployt, sondern separat fuer das
# (noch zu bauende) offline Lizenz-Ausstellungswerkzeug aufbewahrt.
KEYPAIR_DIR=/etc/nexarch/license-signing
mkdir -p "$KEYPAIR_DIR"
chmod 700 "$KEYPAIR_DIR"
if [ ! -f "$KEYPAIR_DIR/private.b64" ]; then
go run "$SCRIPT_DIR/gen-ed25519-keypair.go" > "$KEYPAIR_DIR/keypair.txt"
grep '^PRIVATE=' "$KEYPAIR_DIR/keypair.txt" | cut -d= -f2 > "$KEYPAIR_DIR/private.b64"
grep '^PUBLIC=' "$KEYPAIR_DIR/keypair.txt" | cut -d= -f2 > "$KEYPAIR_DIR/public.b64"
chmod 600 "$KEYPAIR_DIR"/*.b64 "$KEYPAIR_DIR/keypair.txt"
rm -f "$KEYPAIR_DIR/keypair.txt"
fi
LICENSE_PUBLIC_KEY="$(cat "$KEYPAIR_DIR/public.b64")"
cat > "$ENV_FILE" <<EOF
# Automatisch generiert von deploy/install.sh am $(date -Iseconds).
# NICHT ins Repository committen — enthaelt Zugangsdaten/Secrets.
NEXARCH_REGISTRY_DSN=postgresql://${PG_REGISTRY_ROLE}:${PG_REGISTRY_PASSWORD}@localhost:5432/${PG_DB}?sslmode=disable
NEXARCH_ADMIN_DSN=postgresql://${PG_ROLE}:${PG_PASSWORD}@localhost:5432/postgres?sslmode=disable
NEXARCH_TENANT_DSN_TEMPLATE=postgresql://${PG_ROLE}:${PG_PASSWORD}@localhost:5432/%s?sslmode=disable
NEXARCH_TENANT_RUNTIME_DSN_TEMPLATE=postgresql://${PG_TENANT_RUNTIME_ROLE}:${PG_TENANT_RUNTIME_PASSWORD}@localhost:5432/%s?sslmode=disable
NEXARCH_TENANT_SCHEMA_PATH=${INSTALL_DIR}/migrations/tenant
NEXARCH_LISTEN_ADDR=127.0.0.1:8080
NEXARCH_CORE_ADMIN_TOKEN=${CORE_ADMIN_TOKEN}
NEXARCH_LICADMIN_ADMIN_TOKEN=${LICADMIN_ADMIN_TOKEN}
NEXARCH_TENANTADMIN_ADMIN_TOKEN=${TENANTADMIN_ADMIN_TOKEN}
# Kommagetrennte, explizite Ursprungslisten fuer die Verwaltungsoberflaeche (kein *):
# NEXARCH_LICADMIN_CORS_ORIGINS=https://admin.example.test
# NEXARCH_TENANTADMIN_CORS_ORIGINS=https://admin.example.test
NEXARCH_AUDIT_ADMIN_TOKEN=${AUDIT_ADMIN_TOKEN}
NEXARCH_LICENSE_PUBLIC_KEY=${LICENSE_PUBLIC_KEY}
EOF
chmod 600 "$ENV_FILE"
chown nexarch:nexarch "$ENV_FILE"
echo "${ENV_FILE} angelegt (Secrets generiert)."
else
echo "${ENV_FILE} existiert bereits — wird nicht ueberschrieben."
fi
echo "== 4. Build =="
mkdir -p "$INSTALL_DIR/bin"
cd "$REPO_ROOT"
for svc in core auditlog-devserver licadmin-devserver metrics-devserver secheaders-devserver statuspage-devserver tenantadmin-devserver; do
go build -o "$INSTALL_DIR/bin/${svc}" "./cmd/${svc}"
echo "gebaut: ${svc}"
done
cp -r migrations "$INSTALL_DIR/migrations"
chown -R nexarch:nexarch "$INSTALL_DIR"
echo "== 5. Registry-Migrationen =="
# shellcheck disable=SC1090
source "$ENV_FILE"
ALREADY_MIGRATED=$(sudo -u postgres psql -tAc "SELECT to_regclass('public.tenants') IS NOT NULL" -d "${PG_DB}")
if [ "$ALREADY_MIGRATED" = "t" ]; then
echo "Registry-Datenbank hat bereits Tabellen — Migrationen werden uebersprungen (nicht idempotent, siehe Skript-Kommentar)."
else
for f in $(find "$INSTALL_DIR/migrations" -maxdepth 1 -name '*.up.sql' | sort); do
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${PG_DB}" -f "$f" >/dev/null
echo "angewendet: $(basename "$f")"
done
fi
# Migrationen laufen als 'postgres' (fuer CREATE EXTENSION etc.) — die von
# 'postgres' angelegten Tabellen gehoeren deshalb NICHT automatisch der
# Registry-Rolle. Ohne diese expliziten GRANTs (inkl. ALTER DEFAULT
# PRIVILEGES fuer kuenftige Migrationen) koennte NEXARCH_REGISTRY_DSN gar
# nicht auf die Registry-Tabellen zugreifen.
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${PG_DB}" -c "GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO ${PG_REGISTRY_ROLE};" >/dev/null
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${PG_DB}" -c "GRANT ALL PRIVILEGES ON ALL SEQUENCES IN SCHEMA public TO ${PG_REGISTRY_ROLE};" >/dev/null
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${PG_DB}" -c "ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON TABLES TO ${PG_REGISTRY_ROLE};" >/dev/null
sudo -u postgres psql -v ON_ERROR_STOP=1 -d "${PG_DB}" -c "ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA public GRANT ALL ON SEQUENCES TO ${PG_REGISTRY_ROLE};" >/dev/null
echo "== 6. systemd-Units =="
for tmpl in "$SCRIPT_DIR"/systemd/*.service.tmpl; do
name="$(basename "$tmpl" .service.tmpl)"
sed "s#__INSTALL_DIR__#${INSTALL_DIR}#g" "$tmpl" > "/etc/systemd/system/${name}.service"
done
systemctl daemon-reload
for tmpl in "$SCRIPT_DIR"/systemd/*.service.tmpl; do
name="$(basename "$tmpl" .service.tmpl)"
systemctl enable --now "${name}.service"
done
echo "== Fertig =="
echo "Status pruefen mit: systemctl status 'nexarch-*'"
echo "Health-Checks:"
echo " curl -s http://localhost:8080/healthz"
echo " curl -s http://localhost:8081/healthz # licadmin"
echo " curl -s http://localhost:8082/healthz # tenantadmin"
echo " curl -s http://localhost:8083/healthz # auditlog"
echo " curl -s http://localhost:8084/healthz # statuspage"
echo " curl -s http://localhost:8085/healthz # metrics"
echo " curl -s http://localhost:8086/unprotected # secheaders (kein /healthz)"