CI: Loeschschutz messen statt annehmen, Datenbanktests wirklich fahren
Some checks failed
CI / Sicherheitsprüfungen (push) Waiting to run
CI / Backend (Go) (push) Failing after 1m47s
CI / Frontend (React/TypeScript) (push) Has been cancelled

Der Angriffstest auf ein gehaertetes Repository scheiterte in der CI: "rm -rf
auf ein geschuetztes Repository gelang vollstaendig". Der Befund ist richtig —
und der Fehler lag im Test, nicht im Produktivcode.

immutableFlagSupported() sagt nur, ob das Betriebssystem das
Unveraenderlich-Kennzeichen *kennt*; unter Linux gibt es immer "ja" zurueck. Ob
es auch *durchgesetzt* wird, haengt am Dateisystem und an CAP_LINUX_IMMUTABLE.
In einem Container auf overlayfs ist beides nicht gegeben: Das Setzen scheitert
still, und der Angriff gelingt.

Die Anlage selbst macht es richtig — sie ist beim Setzen nachsichtig (ein Backup
ohne technischen Loeschschutz ist besser als gar keines) und sagt die Wahrheit
ueber die gemessene Stufe. Der Test tut das jetzt auch: Er misst zuerst und
prueft nur dort, wo es etwas zu pruefen gibt. Nachgewiesen in beide Richtungen —
auf macOS laeuft der Angriff wirklich, im Container wird mit Begruendung
uebersprungen.

Zwei Luecken in der CI dabei gefunden:

- SYNCOVA_TEST_DATABASE_URL fehlte. Neun Testdateien uebersprangen ihre
  Datenbanktests still, darunter der Upgrade- und der Rollback-Test. Ein
  uebersprungener Test sieht in der Zusammenfassung aus wie ein bestandener.
- make cross-build lief nicht mit. Genau daran ist in Phase 5 monatelang
  unbemerkt geblieben, dass der Agent sich fuer Windows gar nicht uebersetzen
  liess.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Jerrit Fritzsche 2026-08-17 09:45:27 +02:00
parent 610719c316
commit 9c48cb1c1c
2 changed files with 51 additions and 8 deletions

View File

@ -84,6 +84,10 @@ jobs:
SYNCOVA_DB_SSLMODE: disable
# Nur für diesen CI-Lauf; schützt keine echten Daten.
SYNCOVA_ENCRYPTION_KEYS: 'v1:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='
# Ohne diese Variable überspringen neun Testdateien ihre Datenbanktests
# still — darunter der Upgrade- und der Rollback-Test. Ein übersprungener
# Test sieht in der Zusammenfassung aus wie ein bestandener.
SYNCOVA_TEST_DATABASE_URL: 'postgres://syncova_test:ci-only-ephemeral-password@127.0.0.1:5432/syncova_test?sslmode=disable'
run: go test -race -coverprofile=coverage.out $(go list ./... | grep -v '/node_modules/')
- name: Migrationen gegen echte Datenbank prüfen
@ -107,6 +111,12 @@ jobs:
- name: Binaries bauen
run: go build $(go list ./... | grep -v '/node_modules/')
- name: Übersetzbarkeit aller Zielplattformen
# Der Agent liess sich in Phase 5 monatelang gar nicht für Windows
# übersetzen, ohne dass es jemandem auffiel: unix.Statfs gibt es dort
# nicht. Der Bau auf der eigenen Plattform bemerkt das nie.
run: make cross-build
frontend:
name: Frontend (React/TypeScript)
runs-on: ubuntu-latest

View File

@ -352,14 +352,12 @@ func TestUnreadableHoldKeepsBackupProtected(testInstance *testing.T) {
// nicht. Die Daten waren da und weder als Repository erkennbar noch
// entschluesselbar.
func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) {
if !immutableFlagSupported() {
testInstance.Skip("dieses Betriebssystem kennt kein Unveraenderlich-Kennzeichen")
}
createdRepository := newHardenedTestRepository(testInstance, CreateOptions{
Name: "angriff", Immutable: true, Retention: 30 * 24 * time.Hour,
})
requireMeasuredFilesystemEnforcement(testInstance, createdRepository.RootPath())
backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "angriff-backup")
repositoryRoot := createdRepository.RootPath()
@ -420,14 +418,12 @@ func TestHardenedRepositorySurvivesRecursiveDelete(testInstance *testing.T) {
// Repository in einen Fehler — und ein Repository, das nie aufraeumen kann,
// laeuft irgendwann voll.
func TestPruneReleasesProtectedOrphanedChunks(testInstance *testing.T) {
if !immutableFlagSupported() {
testInstance.Skip("dieses Betriebssystem kennt kein Unveraenderlich-Kennzeichen")
}
createdRepository := newHardenedTestRepository(testInstance, CreateOptions{
Name: "bereinigung", Immutable: true, Retention: time.Minute,
})
requireMeasuredFilesystemEnforcement(testInstance, createdRepository.RootPath())
backupIdentifier := writeProtectedTestBackup(testInstance, createdRepository, "bereinigung-backup")
// Nach Fristablauf wird das Backup geloescht; seine Bloecke sind danach
@ -451,3 +447,40 @@ func TestPruneReleasesProtectedOrphanedChunks(testInstance *testing.T) {
testInstance.Error("die Bereinigung meldete keinen freigegebenen Speicher")
}
}
// requireMeasuredFilesystemEnforcement ueberspringt einen Test ohne echten Schutz.
//
// **Der Unterschied zu immutableFlagSupported() ist der Kern der Phase 11.**
// Jene Funktion sagt nur, ob das Betriebssystem das Kennzeichen *kennt* — unter
// Linux gibt sie immer "ja" zurueck. Ob es auch *durchgesetzt* wird, haengt am
// Dateisystem und an der Berechtigung CAP_LINUX_IMMUTABLE. In einem Container
// auf overlayfs ist beides typischerweise nicht gegeben: Das Setzen scheitert
// still, und ein rm -rf gelingt vollstaendig.
//
// Genau daran ist dieser Test in der CI gescheitert — zu Recht. Er behauptete
// Schutz in einer Umgebung, die keinen leisten kann. Die Anlage selbst macht es
// richtig: Sie ist beim Setzen nachsichtig (ein Backup ohne technischen
// Loeschschutz ist besser als gar keines) und sagt die Wahrheit ueber die
// **gemessene** Stufe.
//
// Der Test tut jetzt dasselbe: messen, und nur dort pruefen, wo es etwas zu
// pruefen gibt. Ein uebersprungener Test ist ehrlich; ein gruener, der nichts
// geprueft hat, waere es nicht.
func requireMeasuredFilesystemEnforcement(testInstance *testing.T, repositoryRoot string) {
testInstance.Helper()
enforcementReport, measureError := MeasureEnforcement(context.Background(), repositoryRoot, newTestLogger())
if measureError != nil {
testInstance.Fatalf("die Durchsetzungsstufe liess sich nicht messen: %v", measureError)
}
if enforcementReport.Level == EnforcementFilesystem {
return
}
testInstance.Skipf("dieses Dateisystem setzt den Loeschschutz nicht durch (gemessen: %s).\n"+
" Das ist keine Panne, sondern die Auskunft: In einem Container auf overlayfs "+
"oder ohne CAP_LINUX_IMMUTABLE gibt es keinen technischen Loeschschutz.\n"+
" Der Angriffsversuch braucht ein Dateisystem, das ihn abwehren kann.",
enforcementReport.Level)
}