#!/bin/bash # ============================================================================ # rq-historical — eng begrenzter root-Wrapper fuer Red Queen (rq-host) # RED-QUEEN-VPS-HOST-ACCESS + PHASE-13.5-READONLY | Rain Ocampo (Hermes) | 2026-08-29 # # EINZIGER ZWECK: kontrollierter READ-ONLY-Zugriff + freigegebener Backfill- # Start ausschliesslich auf den Historical-Service. Least-Privilege. # # SICHERHEIT: # - Container-/Skript-Namen sind FEST im Script (kein Benutzer-Argument # fuer Container/Skript/Pfad). # - Alle Subkommandos akzeptieren NUR strikt validierte, vollstaendig # konsumierte Argumente; unbekannte/ueberzaehlige Argumente -> ERR. # - code/hashes/code-list nutzen eine FESTE Allowlist: User uebergibt nur # einen Key, der intern auf einen festen Containerpfad gemappt wird. # KEIN frei durchgereichter Pfad, KEIN Shell-String aus User-Input. # - Argumente werden als argv-Array an `docker exec` uebergeben # (KEINE Shell-Interpolation von Benutzereingaben). # - KEIN sudo ALL / NOPASSWD ALL / keine Root-Shell / kein docker run. # # AUFRUF (ueber /etc/sudoers.d/rq-historical, NUR als root): # sudo rq-historical ps # sudo rq-historical inspect # sudo rq-historical logs [--tail N] # sudo rq-historical run [--start YYYY-MM-DD] [--end YYYY-MM-DD] [--limit-days N] # sudo rq-historical code # sudo rq-historical hashes # sudo rq-historical code-list # ============================================================================ set -euo pipefail CONTAINER=historical-service # ------------------------------------------------------------------ Allowlist # FESTE Mapping-Tabelle (Key -> Containerpfad). Read-only, hart im Script. # Keys NICHT benutzerdefiniert erweiterbar. # NOTE: Bei assign-assoc-array die Assignments OHNE Leerzeichen um '=' schreiben, # sonst Syntaxfehler. (Korrektur unten: einzeilig mit declare -A init.) declare -A CODE_FILES=( [runner]="/app/app/run_backfill_year.py" [backfill]="/app/app/backfill.py" [pipeline]="/app/app/pipeline_v2.py" [repository]="/app/app/persistence/repository.py" [provenance]="/app/app/trust/provenance.py" [trust]="/app/app/trust/trust.py" [sanity]="/app/app/trust/sanity.py" [trust_gate]="/app/app/trust/trust_gate.py" ) # Relevanter Pfad fuer den 'run'-Subbefehl (Backfill-Entrypoint) SCRIPT=/app/app/run_backfill_year.py # ---------------------------------------------------------------- Validierung die() { echo "ERR: $*" >&2; exit 2; } req_ok() { docker inspect -f '{{.State.Running}}' "$CONTAINER" >/dev/null 2>&1 \ || die "Container '$CONTAINER' nicht vorhanden oder nicht am Laufen" } # -------------------------------------------------------------------- dispatch SUBCMD="${1:-}" shift || true case "$SUBCMD" in ps) [ $# -eq 0 ] || die "unerlaubtes Argument 'ps $*'" exec docker ps ;; inspect) req_ok [ $# -eq 0 ] || die "unerlaubtes Argument 'inspect $*'" exec docker inspect "$CONTAINER" ;; logs) req_ok tail_n=200 while [ $# -gt 0 ]; do case "$1" in --tail) [ $# -ge 2 ] || die "--tail braucht Wert" [[ "$2" =~ ^[0-9]+$ ]] || die "ungueltiges --tail" tail_n="$2"; shift 2 ;; *) die "unerlaubtes Argument 'logs $1'" ;; esac done exec docker logs --tail "$tail_n" "$CONTAINER" ;; run) req_ok declare -a RUN=() while [ $# -gt 0 ]; do case "$1" in --start) [[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "ungueltiges --start"; RUN+=("--start" "$2"); shift 2 ;; --end) [[ "${2:-}" =~ ^[0-9]{4}-[0-9]{2}-[0-9]{2}$ ]] || die "ungueltiges --end"; RUN+=("--end" "$2"); shift 2 ;; --limit-days) [[ "${2:-}" =~ ^[0-9]+$ ]] || die "ungueltiges --limit-days"; RUN+=("--limit-days" "$2"); shift 2 ;; *) die "unerlaubtes Argument 'run $1'" ;; esac done echo "[rq-historical] Start: docker exec $CONTAINER python $SCRIPT ${RUN[*]}" >&2 exec docker exec "$CONTAINER" python "$SCRIPT" "${RUN[@]}" ;; code) # Fester Allowlist-Key -> fester Containerpfad. KEIN frei gereichter Pfad. req_ok [ $# -eq 1 ] || die "code braucht genau EINEN Key (siehe code-list)" key="$1" path="${CODE_FILES[$key]:-}" [ -n "$path" ] || die "unbekannter Key 'code $key' (siehe code-list)" exec docker exec "$CONTAINER" cat "$path" ;; hashes) # SHA256 der festen Allowlist-Dateien aus dem laufenden Container. req_ok [ $# -eq 0 ] || die "unerlaubtes Argument 'hashes $*'" # Feste, vollstaendig konsumierte Dateiliste -> argv-Array (keine Interpolation). declare -a PATHS=() for k in "${!CODE_FILES[@]}"; do PATHS+=("${CODE_FILES[$k]}"); done exec docker exec "$CONTAINER" sha256sum "${PATHS[@]}" ;; code-list) # Nur die verfuegbaren fixen Keys + Containerpfade anzeigen. Kein FS-Zugriff. req_ok [ $# -eq 0 ] || die "unerlaubtes Argument 'code-list $*'" echo "Verfuegbare READ-ONLY-Code-Keys (historical-service):" for k in "${!CODE_FILES[@]}"; do printf ' %-11s -> %s\n' "$k" "${CODE_FILES[$k]}" done ;; *) die "Aufruf: rq-historical {ps|inspect|logs [--tail N]|run [--start --end --limit-days]|code |hashes|code-list}" ;; esac