diff --git a/docs/AI_CONTEXT.md b/docs/AI_CONTEXT.md index 5e085ed..6e9ff8b 100644 --- a/docs/AI_CONTEXT.md +++ b/docs/AI_CONTEXT.md @@ -48,8 +48,8 @@ Authoritativ: `docs/AUDIT_2026-05-25_TODO.md`. Kurzfassung: - Alt-Volumes fruehestens ab 2026-06-02 freigeben -- Hetzner-Account-Hygiene und Borg `append-only` pruefen -- FRITZ!Box-Servicefenster fuer Update, Config-Backup und IPv6-Exposure planen +- Hetzner-Account-Hygiene und Borg `append-only` mit `docs/EXTERNAL_OPERATOR_RUNBOOK.md` im Account/Storage-Box-SSH-Setup bestaetigen +- FRITZ!Box-Konfig-Backup und UI-Gegencheck fuer IPv6-Portfreigaben/Selbstfreigaben erledigen - Auth-/OIDC-/CrowdSec-/Hermes-Themen bewusst geparkt Letzte Bestaetigung: @@ -58,3 +58,4 @@ Letzte Bestaetigung: - H:/ Nearline-Pull 2026-06-01 repariert: Borg-Dumps werden kuratiert kopiert, Gitea-Bundles aktuell. - Family-Status-Dashboard liegt als `monitoring/grafana/dashboards/family-status.json` im Repo. - Alt-Volume-Freigabe ist per `ops/maintenance/release-alt-volumes.sh` vorbereitet; `--execute` nicht vor 2026-06-02. +- Externer Betreibercheck: `ops/maintenance/check-external-operator.sh`; FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`, DNS fuer Public Apps hat keine AAAA-Records, Host hat keine globale Provider-IPv6. diff --git a/docs/AUDIT_2026-05-25_TODO.md b/docs/AUDIT_2026-05-25_TODO.md index f84bcfa..b3c3e0d 100644 --- a/docs/AUDIT_2026-05-25_TODO.md +++ b/docs/AUDIT_2026-05-25_TODO.md @@ -8,9 +8,9 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi | Prioritaet | Punkt | Naechster Schritt | |---|---|---| | P0 | Alt-Volumes nach Burn-in freigeben | Ab 2026-06-02 `ops/maintenance/release-alt-volumes.sh --dry-run` pruefen, danach nur bei sauberem Ergebnis mit `--execute` freigeben | -| P1 | Hetzner-Account-Hygiene | Starkes einzigartiges Passwort, Backup-Zahlungsweg und Login-Benachrichtigungen extern bestaetigen | -| P1 | Borg `--append-only` fuer Hetzner pruefen | Rollback-faehigen Test fuer `borg serve --append-only` in Hetzner `authorized_keys` planen | -| P1 | FRITZ!Box-Servicefenster | FRITZ!OS-Update, FRITZ!Box-Konfig-Backup und IPv6-Exposure-Pruefung gemeinsam erledigen | +| P1 | Hetzner-Account-Hygiene | Mit `docs/EXTERNAL_OPERATOR_RUNBOOK.md` im Hetzner-Konto bestaetigen; keine Secret-Werte ins Repo | +| P1 | Borg `--append-only` fuer Hetzner pruefen | Produktiven Borg-UI-SSH-Key in der Storage-Box-`authorized_keys` per forced command auf `borg serve --append-only` einschraenken; separaten Maintenance-Key offline halten | +| P1 | FRITZ!Box-Servicefenster | FRITZ!Box-Konfig-Backup und UI-Gegencheck fuer IPv6-Portfreigaben/Selbstfreigaben; FRITZ!OS `154.08.25` ist technisch bereits beobachtet | | P2 | Family-Onboarding praktisch starten | Familienkonten, Vaultwarden-Organisation und Immich-Mobile-Backup gemeinsam einrichten | ## Bewusst geparkt @@ -27,6 +27,7 @@ Audit-Snapshots wurden aus der Arbeitskopie entfernt; Detailhistorie liegt in Gi ## Zuletzt geschlossen +- Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; Live-Baseline am 2026-06-01: FRITZ!OS `154.08.25`, keine Public-AAAA-Records fuer `*.kaleschke.info`, Host ohne globale Provider-IPv6, WAN `443/tcp` offen und `80/tcp`/`222/tcp` geschlossen. - Family-View Dashboard ist repo-seitig gebaut: `monitoring/grafana/dashboards/family-status.json` zeigt Family-App-Uptime, Backup-Alter, TLS-Restlaufzeit, Critical-Container und Image-Drift. - Alt-Volume-Freigabe ist vorbereitet: `ops/maintenance/release-alt-volumes.sh --dry-run` validiert aktive Pfade, Container-Health, Restore-Freshness und gemountete Altpfade; Test am 2026-06-01 fand vier Kandidaten und keine Blocker, Ausfuehrung bleibt wegen Cutoff bis 2026-06-02 gesperrt. - Borg-Nachlauf nach dem 2026-05-31-Sprint ist belegt: Archiv `Taegliche-Sicherung-2026-06-01T04:30:26.913`, 101669 Dateien, `rc=0`; Freshness-Check am 2026-06-01: Critical 0, Warnings 0. diff --git a/docs/EXTERNAL_DEPENDENCIES.md b/docs/EXTERNAL_DEPENDENCIES.md index 671ff40..fc8981f 100644 --- a/docs/EXTERNAL_DEPENDENCIES.md +++ b/docs/EXTERNAL_DEPENDENCIES.md @@ -1,6 +1,6 @@ # External Dependencies - KalliLab CORE -Status: Initiale Betreiber-Baseline 2026-05-26; konkrete Account-Recovery-Codes und Besitznachweise muessen ausserhalb des Repos bestaetigt werden. +Status: Betreiber-Baseline 2026-06-01; konkrete Account-Recovery-Codes und Besitznachweise muessen ausserhalb des Repos bestaetigt werden. ## Zweck @@ -78,3 +78,4 @@ Authoritativ ist `docs/SECRETS_MAP.md`. Diese Liste markiert nur externe Abhaeng | 2026-05-26 | Bekannte externe Abhaengigkeiten aus Repo-/Betriebsdoku dokumentiert; keine Secret-Werte aufgenommen. Borg-Passphrase ist laut Operator offline gesichert. | Account-Besitz, 2FA-Recovery-Codes und Zahlungswege extern bestaetigen | | 2026-05-26 | Telekom-DSL und FRITZ!Box 7590 (FRITZ!OS 8.21) als WAN-/Router-Abhaengigkeit aufgenommen; Ausfallschutz nicht eingerichtet | FRITZ!OS-Update im Service-Fenster pruefen | | 2026-05-28 | FRITZ!Box-Portfreigaben bereinigt: aktiv bleibt nur `443/tcp`; `80/tcp` entfernt, `222/tcp` bewusst nicht angelegt; UPnP-Recht fuer VONETS-Bridge deaktiviert | IPv6-Exposure bei naechstem WAN-/Router-Review pruefen | +| 2026-06-01 | Externer Betreibercheck vorbereitet: `docs/EXTERNAL_OPERATOR_RUNBOOK.md` und `ops/maintenance/check-external-operator.sh`; FRITZ!Box meldet per TR-064 FRITZ!OS `154.08.25`, Public DNS hat keine AAAA-Records, Host hat keine globale Provider-IPv6 | Hetzner Account-Hygiene und Borg append-only im Konto/Storage-Box-SSH-Key-Setup bestaetigen | diff --git a/docs/EXTERNAL_OPERATOR_RUNBOOK.md b/docs/EXTERNAL_OPERATOR_RUNBOOK.md new file mode 100644 index 0000000..29fb85d --- /dev/null +++ b/docs/EXTERNAL_OPERATOR_RUNBOOK.md @@ -0,0 +1,125 @@ +# External Operator Runbook + +Stand: 2026-06-01 + +Dieses Runbook schliesst die Betreiber-Aufgaben, die nicht vollstaendig aus dem +Repo automatisierbar sind: Hetzner-Account-Hygiene, Borg-Append-Only und +FRITZ!Box-Servicefenster. Keine Secret-Werte ins Repo schreiben. + +## 1. Vorher pruefen + +Auf dem Unraid-Host: + +```bash +bash /mnt/user/services/homelab-infra/ops/maintenance/check-external-operator.sh +``` + +Erwarteter Stand vom 2026-06-01: + +- FRITZ!Box 7590 meldet FRITZ!OS `154.08.25`. +- Public DNS fuer `*.kaleschke.info` liefert A-Records auf `217.249.115.154`, keine AAAA-Records. +- Host hat keine globale Provider-IPv6-Adresse; sichtbar ist nur Tailscale-IPv6 `fd7a:115c:a1e0::2c01:62b2`. +- WAN-Smoke gegen die Public-IP: `443/tcp` offen, `80/tcp` und `222/tcp` geschlossen. +- Borg UI nutzt `borg 1.4.x`; Repository `appdata-critical` liegt auf Hetzner Storage Box `ssh://...your-storagebox.de:23/./hetzner_borg_appdata_critical`. +- Restore-Freshness: `Critical 0`, `Warnings 0`. + +## 2. Hetzner Account-Hygiene + +Im Hetzner-/Storage-Box-Konto pruefen und extern/off-system dokumentieren: + +| Punkt | Soll | +|---|---| +| Passwort | stark, eindeutig, im Passwortmanager | +| 2FA | aktiv, Recovery-Codes offline auffindbar | +| Kontakt-E-Mail | aktuell und ohne Homelab-Abhaengigkeit erreichbar | +| Zahlungsweg | gueltig, Fallback bekannt | +| Storage Box | Produkt, Benutzer und Rechnungsstatus sichtbar | +| SSH/SFTP/WebDAV/SMB | nur benoetigte Protokolle aktiv | +| Recovery | Kundennummer, Login-Pfad und Support-Pfad extern notiert | + +Im Repo nur das Datum der Bestaetigung dokumentieren, nie Zugangsdaten. + +## 3. Borg Append-Only + +Ziel: Der produktive Backup-Client darf neue Archive schreiben, aber nicht +normal prune/delete/compact als unbeschraenkter Client ausfuehren. + +Hetzner dokumentiert Borg-Zugriff auf Storage Boxen inklusive `--remote-path` +fuer Borg-Versionen; fuer Borg 1.4 wird `--remote-path=borg-1.4` empfohlen. +Hetzner bestaetigt auch, dass append-only moeglich ist. Borg selbst setzt +append-only pro SSH-Key typischerweise ueber einen forced command in +`authorized_keys` um. + +Empfohlenes Zielmodell: + +```text +command="borg serve --append-only --restrict-to-repository /home/hetzner_borg_appdata_critical",restrict ssh-ed25519 borg-ui-append-only +command="borg serve --restrict-to-repository /home/hetzner_borg_appdata_critical",restrict ssh-ed25519 borg-maintenance +``` + +Hinweise: + +- Pfad auf der Storage Box vor dem Eintragen pruefen. Bei Hetzner werden Pfade + im Borg-Repo haeufig relativ als `./repo-name` verwendet; in + `authorized_keys` muss der serverseitige Pfad zur Storage-Box-Home-Struktur + passen. +- Der produktive Borg-UI-Key sollte append-only sein. +- Ein separater Maintenance-Key bleibt fuer bewusste Retention/Prune/Compact + noetig und darf nicht auf dem produktiven Homelab-Host liegen. +- Append-only verhindert nicht, dass ein kompromittierter Client Archive als + geloescht markiert; es verhindert die unmittelbare physische Entfernung. + Nach einem Vorfall keine unbeschraenkte Schreiboperation ausfuehren, bevor + die Borg-Transaktionen bewertet wurden. + +Nach Aenderung: + +1. Einen regulaeren Borg-Lauf abwarten oder manuell starten. +2. `check-external-operator.sh` ausfuehren. +3. In `docs/AUDIT_2026-05-25_TODO.md` nur das Ergebnis dokumentieren. + +## 4. FRITZ!Box-Servicefenster + +Vor dem Fenster: + +1. Familie informieren: Internet/Telefonie koennen kurz weg sein. +2. Aktuellen Repo-Stand und Borg-Freshness pruefen. +3. FRITZ!Box-Konfig exportieren: `System -> Sicherung -> Sichern`. +4. Sicherungsdatei nicht ins Repo legen; im Passwortmanager/off-system ablegen. + +In der FRITZ!Box: + +| Bereich | Soll | +|---|---| +| `System -> Update` | FRITZ!OS aktuell; am 2026-06-01 per TR-064 `154.08.25` beobachtet | +| `Internet -> Freigaben -> Portfreigaben` | nur `443/tcp -> 192.168.178.58:443` | +| IPv6-Portfreigaben | keine unerwarteten Freigaben; insbesondere kein `222/tcp`, kein Admin-Port | +| Selbststaendige Portfreigaben/UPnP | fuer `Kallilabcore` aus; neue Geraete nur bewusst erlauben | +| Gastnetz | bleibt aus, solange keine Gastnetz-Policy gepflegt wird | +| Ausfallschutz | bewusst aus; nur neu bewerten, wenn ein Mobilfunk-Fallback gewuenscht ist | + +Nach dem Fenster: + +```bash +bash /mnt/user/services/homelab-infra/ops/maintenance/check-external-operator.sh +``` + +Dann in `docs/NETWORK_INVENTORY.md` aktualisieren: + +- FRITZ!OS-Version +- IPv6-Status +- aktive Portfreigaben +- Datum der Konfig-Sicherung + +## Quellen + +- Hetzner Docs: Storage Box Zugriff mit SSH/rsync/BorgBackup, inklusive + Borg-Versionen, `--remote-path` und Append-Only-Hinweis: + +- BorgBackup Docs: `borg serve --append-only` und forced commands in + `authorized_keys`: + +- AVM FRITZ!Box Hilfe: IPv6-Portfreigaben werden separat verwaltet; eingehende + Zugriffe sind standardmaessig nicht offen: + +- AVM FRITZ!Box Hilfe: Sicherung der FRITZ!Box-Einstellungen: + diff --git a/docs/NETWORK_INVENTORY.md b/docs/NETWORK_INVENTORY.md index 5f2006e..a26c94b 100644 --- a/docs/NETWORK_INVENTORY.md +++ b/docs/NETWORK_INVENTORY.md @@ -1,7 +1,7 @@ # Network Inventory - KalliLab CORE -Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI bereinigt; VLAN/IPv6-Details offen. -Letzte Pruefung: 2026-05-28 +Status: Host-Audit erfasst; Router-Baseline und Portfreigaben-UI bereinigt; IPv6-Exposure technisch entschaerft. +Letzte Pruefung: 2026-06-01 ## Zweck @@ -14,11 +14,11 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun | Anschluss / Provider | DSL, Telekom | | Bandbreite (FRITZ!Box-UI) | ca. 87,3 Mbit/s Download, ca. 36 Mbit/s Upload | | Router-Modell | FRITZ!Box 7590 | -| Firmware | FRITZ!OS 8.21 (Update gemeldet, nicht eingespielt) | +| Firmware | FRITZ!OS 8.25 (`154.08.25` per TR-064 am 2026-06-01) | | Router-IP | 192.168.178.1 | | DHCP-Server | FRITZ!Box (Standardannahme, Override durch Operator nicht dokumentiert) | | Lokales Subnetz | 192.168.178.0/24 | -| IPv6 aktiv | TBD (FRITZ!Box-UI separat pruefen) | +| IPv6 aktiv | Windows-Client hat Provider-IPv6; Host hat keine globale Provider-IPv6, nur Tailscale-ULA | | DynDNS / DDNS | Cloudflare via `ddns-updater` (kein FRITZ!Box-DynDNS in Nutzung) | | Heimnetz-Geraete (FRITZ!Box-UI) | 36 aktive Geraete | | LAN-Ports belegt | LAN 1-4 verbunden | @@ -30,7 +30,7 @@ Dieses Dokument beschreibt Router, DNS, Tailscale, Portfreigaben und Netztrennun - Telekom-DSL ist Single-WAN; ohne Ausfallschutz ist Internet-Ausfall = kein DDNS-Update, keine ACME-Erneuerung, keine externen Push-Quellen. - Upload 36 Mbit/s ist die effektive Obergrenze fuer Off-site-Backup-Geschwindigkeit nach Hetzner und fuer Plex-Remote-Streaming. -- FRITZ!OS 8.21 hat ein angezeigtes Update; Einspielung ist Betreiber-Aufgabe und nicht Teil des Repos. +- FRITZ!OS ist am 2026-06-01 per TR-064 auf `154.08.25` beobachtet; FRITZ!Box-Konfig-Backup bleibt Betreiber-Aufgabe. ## DNS @@ -151,8 +151,8 @@ docker network inspect backend_net | jq '.[0].Internal' |---|---|---| | AdGuard Admin nur via Tailscale | live validiert 2026-05-26 | Compose bindet Admin-Port auf `100.80.98.33:8082`; DNS auf Port 53 funktioniert, LAN-Zugriff auf `192.168.178.58:8082` schlaegt fehl | | FRITZ!Box-Portfreigaben mit Repo-Soll abgleichen | **erledigt 2026-05-28** | Bereinigt: `80/tcp` entfernt (Cloudflare-DNS-Challenge ersetzt HTTP-01; Mobilfunk-Test bestaetigt Timeout auf `http://`, `https://` weiter ok). `222/tcp` bleibt bewusst nicht eingerichtet (Tailscale-only-Linie). UPnP-Selbstfreigabe-Recht fuer VONETS-Bridge (SolarEdge) deaktiviert. Aktiver Soll-Stand: ausschliesslich `443/tcp -> 192.168.178.58`. | -| FRITZ!OS 8.21 Update | gemeldet | Operator-Aufgabe; vor Update kurzes Service-Fenster planen, weil Reboot WAN/Tailscale-Aufbau unterbricht | +| FRITZ!OS Update | **erledigt / beobachtet 2026-06-01** | TR-064 meldet `154.08.25`; nach Betreiber-Login nur noch FRITZ!Box-Konfig-Backup ablegen | | Gast-/IoT-Zugriff auf Admin-Ports | aktuell entschaerft | Gast-WLAN ist inaktiv; bei Aktivierung muessen `192.168.178.58:8082`, `192.168.178.58:8181` und ggf. weitere LAN-Ports per FRITZ!Box-Kindersicherung/Netzwerk-Filter abgesichert werden | -| IPv6 Exposure | offen | Router und Traefik/Cloudflare pruefen; Telekom-DSL liefert in der Regel IPv6, FRITZ!Box-Standard-Verhalten klaeren | +| IPv6 Exposure | technisch entschaerft | Public DNS liefert keine AAAA-Records fuer `*.kaleschke.info`; Host hat keine globale Provider-IPv6. In der FRITZ!Box trotzdem IPv6-Portfreigaben gegenpruefen: keine Admin-/SSH-Freigaben | | WAN-Ausfallschutz | bewusst nicht eingerichtet | Mobilfunk-Stick-Failover an FRITZ!Box ist nicht aktiv; Internet-Ausfall = ACME/DDNS pausieren, lokale Apps laufen weiter | | Home Assistant InfluxDB Bind | validiert 2026-05-31 | `docker-proxy` bindet `127.0.0.1:8181`; keine LAN-Exposure. Wenn Home Assistant nicht lokal auf dem Host schreibt, braucht das eine bewusste Bind-Aenderung. | diff --git a/docs/README.md b/docs/README.md index 7521098..fe3729b 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ # Documentation Index -Stand: 2026-05-31 +Stand: 2026-06-01 Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Neue operative Dokumente duerfen nur in `docs/` liegen, wenn sie heute als Einstieg, Runbook, Inventar oder offene Arbeitsliste gebraucht werden. Erledigte Audits, Chat-Handoffs, Prompt-Dateien und abgeschlossene Plaene bleiben in der Git-Historie, aber nicht als dauerhafte Arbeitskopie. @@ -34,6 +34,7 @@ Diese Datei trennt aktive Betriebsdokumentation von historischer Arbeitsdoku. Ne | `HARDWARE_INVENTORY.md` | Host-, Disk-, SMART-, USV- und Power-Baseline | | `NETWORK_INVENTORY.md` | Router, DNS, Tailscale, Portfreigaben und Netzthemen | | `EXTERNAL_DEPENDENCIES.md` | Provider, Konten und externe Abhaengigkeiten | +| `EXTERNAL_OPERATOR_RUNBOOK.md` | Hetzner-/Borg-/FRITZ!Box-Betreibercheck | | `CAPACITY_AND_LIFECYCLE.md` | Kapazitaet, Wachstum und Upgrade-Trigger | ## Monitoring und Automatisierung diff --git a/ops/maintenance/check-external-operator.sh b/ops/maintenance/check-external-operator.sh new file mode 100755 index 0000000..895edba --- /dev/null +++ b/ops/maintenance/check-external-operator.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +set -euo pipefail + +HOST_IP="${HOST_IP:-192.168.178.58}" +FRITZBOX_URL="${FRITZBOX_URL:-http://192.168.178.1:49000/tr64desc.xml}" +BORG_DB="${BORG_DB:-/mnt/user/appdata/borg-ui/data/borg.db}" +REPO_ROOT="${REPO_ROOT:-/mnt/user/services/homelab-infra}" + +section() { + printf '\n## %s\n\n' "$1" +} + +section "FRITZBox" +if fritz_xml="$(curl -fsS --max-time 5 "$FRITZBOX_URL")"; then + printf '%s\n' "$fritz_xml" | grep -E '||' | sed -E 's/^[[:space:]]+//' +else + echo "FRITZBox TR-064 descriptor not reachable at $FRITZBOX_URL" +fi + +section "Host IPv6" +global_ipv6="$( + ip -6 addr show scope global \ + | awk '/inet6 / {print $2}' \ + | grep -Ev '^(fd|fe80:)' || true +)" +if [[ -n "$global_ipv6" ]]; then + echo "Provider/global IPv6 addresses present:" + printf '%s\n' "$global_ipv6" +else + echo "No provider/global IPv6 address on host; only ULA/link-local/Tailscale may be present." +fi +tailscale ip -4 2>/dev/null | sed 's/^/Tailscale IPv4: /' || true +tailscale ip -6 2>/dev/null | sed 's/^/Tailscale IPv6: /' || true + +section "DNS" +for name in \ + kaleschke.info \ + vault.kaleschke.info \ + git.kaleschke.info \ + cloud.kaleschke.info \ + traefik.kaleschke.info; do + echo "$name" + dig +short @1.1.1.1 "$name" A | sed 's/^/ public A /' || true + dig +short @1.1.1.1 "$name" AAAA | sed 's/^/ public AAAA /' || true +done + +section "Host Listeners" +ss -ltnp | awk ' + /:(80|443|222|53|8082|8181)[[:space:]]/ || + /100\.80\.98\.33:8082/ || + /127\.0\.0\.1:8181/ {print} +' | sort -k4 + +section "External Port Smoke" +public_ipv4="$(curl -4 -fsS --max-time 5 https://ifconfig.co 2>/dev/null || true)" +if [[ -z "$public_ipv4" ]]; then + public_ipv4="$(dig +short @1.1.1.1 kaleschke.info A | tail -n 1)" +fi + +for check in \ + "$public_ipv4 443 expected-open" \ + "$public_ipv4 80 expected-closed" \ + "$public_ipv4 222 expected-closed"; do + set -- $check + target="$1" + port="$2" + expected="$3" + if timeout 5 bash -c "cat < /dev/null > /dev/tcp/$target/$port" 2>/dev/null; then + result="open" + else + result="closed" + fi + printf '%s:%s %s (%s)\n' "$target" "$port" "$result" "$expected" +done + +section "Borg UI Repository" +if [[ -f "$BORG_DB" ]]; then + sqlite3 -header -csv "$BORG_DB" \ + "select name,repository_type,path,remote_path,last_backup,last_check,borg_version,custom_flags from repositories order by id;" + sqlite3 -header -csv "$BORG_DB" \ + "select id,repository,status,archive_name,started_at,completed_at,nfiles from backup_jobs order by id desc limit 3;" +else + echo "Borg UI database not found: $BORG_DB" +fi + +section "Restore Freshness" +if [[ -x "$REPO_ROOT/ops/restore-tests/run-restore-checks.sh" ]]; then + "$REPO_ROOT/ops/restore-tests/run-restore-checks.sh" freshness +else + echo "Restore freshness script not executable: $REPO_ROOT/ops/restore-tests/run-restore-checks.sh" +fi