Gå til hovedindhold

Integritet i revisionskæden

Hver administrativ handling, der ændrer tilstand i din fremforge-organisation, registreres i en revisionslog: ændringer af SSO-konfiguration, tilsidesættelser af push-beskyttelse, opdateringer af branch-beskyttelse, overgange i faktureringen og SCIM-provisionering. fremforge går et skridt videre end en almindelig log: hver post er kryptografisk kædet til den foregående, og kædens hoved WORM-forankres (Write-Once-Read-Many) i T Cloud Public OBS-lager med Object Lock hvert 2. minut.

Resultatet: du kan opdage enhver manipulation af revisionsloggen, og du kan selv verificere det uden at skulle tage fremverks ord for det.

Denne side beskriver, hvad kæden garanterer, hvad den ikke garanterer, og hvordan du gennemfører verifikationen fra ende til anden. Den tekniske forpligtelse står i DPA bilag A.7.

Garantien

For hver post i revisionsloggen, der registreres for din organisation, gemmer fremforge:

  • Selve hændelsen (aktør, handling, tidsstempel, felter).

  • En prev_hash, hashen af den foregående post for din tenant.

  • En hash, SHA-256 over postens kanoniske JSON.

    Kanoniseringen er eksakt, og rækkefølgen nedenfor er alfabetisk efter nøgle, ikke den rækkefølge, felterne nævnes i andre steder på denne side. En verifikator, der hasher dem i en anden rækkefølge, får en anden digest og konkluderer fejlagtigt, at kæden er brudt:

    sha256( {"action":…,"actor":…,"created_at_iso":…,"fields_json":…,"prev_hash":…,"tenant_id":…} )

    Intet whitespace. prev_hash er den bogstavelige null ved kædens epoke. created_at_iso er tidsstemplet som YYYY-MM-DDTHH:mm:ss.sssZ. fields_json er indholdet som en JSON-streng, indlejret som en streng og ikke som et indlejret objekt, så den bevares byte for byte i stedet for at blive serialiseret igen. Det er den sidste detalje, folk falder over: parser og serialiserer du indholdet igen, vil din digest ikke matche vores.

Fordi hver posts hash indeholder den foregående hash, bryder en ændring af en post kæden fremad. GET /orgs/{org}/audit/chain returnerer de rå rækker, så du selv kan opdage det; GET /orgs/{org}/audit/integrity rapporterer den gennemgang, vi foretog.

Derudover skriver et separat forankringsjob hvert 2. minut (tenant_id, latest_hash, count, timestamp) til T Cloud Public OBS med Object Lock-opbevaring på 1095 dage (3 år) i COMPLIANCE-tilstand. Forankringsobjektet kan fysisk ikke slettes inden for opbevaringsperioden: lagerlaget afviser sletninger fra alle, også fremverk. En angriber, der på en eller anden måde ændrede hele kæden i databasen, ville stadig stå over for forankringerne i OBS, som ikke stemmer overens.

Oplysning om huller. Kadencen på 2 minutter er målet; vi lover ikke kontraktligt en RPO på ≤2 min for forankring af kæden. Kan T Cloud Public OBS ikke nås (regional vedligeholdelse, en netværkshændelse), prøver forankringsjobbet igen ved næste kørsel og registrerer hullet i kædens metadata. Det værste observerede vindue, før alarmen for brudt kæde udløses, er 12 minutter (kadence + ANCHOR_STALE-tærsklen). Huller beskrives i den næste hændelsesrapport på statussiden og fremhæves i DPA §A.7-klausulen om revisionslogning.

Hvad den fanger

TrusselFanget?
Indholdet i én række i revisionsloggen ændres (aktør, handling, felter)✓, hash_mismatch på den række
Én række slettes midt i kæden✓, prev_hash_mismatch på rækken efter
Hele kæden omskrives af en angriber med fuld skriveadgang til databasen✓ hvis omskrivningen skete for mere end ~10 min siden (12 minutter i værste fald): WORM-forankringen stemmer ikke overens
Hele kæden omskrives, og der manipuleres også med WORM-bucket’enObject Lock i lagerlaget afviser fysisk skrivningen til OBS; det ville kræve et kompromis hos T Cloud Public
En administrativ handling, der slet ikke udsender en revisionshændelseDet er et hul i dækningen, ikke et brud på kæden, se “Hvad den ikke fanger”

Hvad den ikke fanger

  • Huller i dækningen. Ændrer en kodesti tilstand uden at udsende en revisionshændelse, er kæden tavs. Vi dækker de tilstandsændrende ruter i bilag A.7; vi gennemgår ruterne kvartalsvis for at bekræfte, at dækningen holder.
  • Historik fra før bilag A.7. Revisionshændelser, der blev udsendt, før forankring af kæden blev slået til for din tenant, har markøren pre-chain-epoch og er udtrykkeligt IKKE en del af den verificerbare kæde.
  • Læsehændelser. Kæden registrerer kun ændringer. Et læk af læseadgang (nogen ser data, de ikke burde) vises ikke som en post i kæden. Revisionsloggen registrerer læsningen, hvor ruten udsender en hændelse, men dækning af skrivebeskyttede ruter er en separat garanti, ikke en garanti fra kæden.

Verificér kæden selv

Det er en lokal genberegning, ikke en aflæsning af en status. fremforge audit-verify henter de rå rækker fra GET /orgs/{org}/audit/chain, hver med sin hash og prev_hash, og genberegner hver digest og hvert led på din maskine. Derefter henter den vores egen dom og sammenligner. Exit-koden er dit resultat; en uoverensstemmelse mellem de to rapporteres og får kommandoen til at fejle.

Forskellen betyder mere, end den måske ser ud til. Alt ovenfor dette afsnit beskriver en garanti mod en, der når databasen, men ikke API’et: WORM-forankringen er i Object Lock COMPLIANCE-tilstand, så en manipuleret database kan ikke bringes til at stemme overens med den. Intet af det begrænser selve API’et, og indtil august 2026 blev krydstjekket mellem gennemgangen af kæden og forankringen udført af den samme part, som rapporterede svaret. Lokal verifikation er det, der gør garantien til noget, du kan kontrollere, i stedet for noget, vi påstår.

Forankringen er signeret, og det er dig, der verificerer signaturen, ikke os. Hver forankring skrives med en DSSE-konvolut, der er signeret med den samme Ed25519-nøgle som vores SLSA-build-provenance, hvis offentlige halvdel er offentliggjort på www.frem.sh/.well-known/slsa-trust-root.json.

fremforge audit-verify henter den tillidsrod direkte fra www.frem.sh, ikke fra API’et, og der er ingen reserve med en nøgle, API’et har leveret: en signatur, der er tjekket mod en nøgle, som den reviderede part har givet dig, beviser intet. Derefter tjekker den, at konvoluttens subject-digest er lig med sha256(anchor_head), så en konvolut, der er kryptografisk gyldig for en anden forankring, afvises i stedet for at blive accepteret.

To ærlige forbehold:

  • Forankringer skrevet før 2026-08-04 er usignerede og rapporteres som NOT CHECKED. Der signeres ikke med tilbagevirkende kraft: en forankrings værdi er, at den blev skrevet på et bestemt tidspunkt under Object Lock, og at omskrive historikken for at tilføje en signatur ville ophæve netop det.
  • Signering er bevidst ikke fatal hos os. Fejler signeringen, skrives forankringen stadig: en usigneret forankring er langt bedre end ingen forankring, fordi forankringen er sidste værn til at afsløre manipulation. Vi alarmerer på det (AUDIT_ANCHOR_UNSIGNED, Major), så forringelsen er tydelig i stedet for tavs, og audit-verify rapporterer NOT CHECKED i stedet for at lade som om.

Brug fremforge CLI:

# Install (one-liner — see /cli/ for manual download and SHA-256 verification)
curl -sSfL https://cli.frem.sh/cli/install.sh | sh

# Configure auth — generate a PAT at https://frem.sh/user/settings/applications
# with the audit:read scope.
export FREMFORGE_TOKEN='<your PAT>'

# Verify the chain for your org
fremforge audit-verify acme --human

Outputtet ser sådan ud:

✓ local verification: ok
  (hashes recomputed on this machine — the server was not asked)

  walk_mode:              tail
  rows_walked:            1284
  verified_count:         1280
  pre_chain_epoch_count:  4
  chain_broken_count:     0
  tenant_erased_count:    0
  payload_redacted_count: 0
  last_verified_hash:     a2b3c4...

✓ WORM anchor (OBS Object Lock):
    anchor_head:            f8e9d0...
    chain_tip_at_anchor:    a2b3c4...
    count_at_anchor:        1280
    timestamp:              2026-05-05T18:00:00.000Z
    age_seconds:            120
    agrees_with_local_walk: true
    signature:              VERIFIED (keyid fremforge-slsa-builder-v1)
    against the trust root fetched from www.frem.sh, not from this API —
    so the anchored tip is the tip that was anchored.

  server verdict:         ok
  server agrees with the local walk.

Uden --human udsender CLI’en rå JSON til scripts, inklusive begge domme og enhver uoverensstemmelse mellem dem:

# Exit 0 only if YOUR walk was clean and ours agreed
fremforge audit-verify acme || echo "verification failed: $?"

Exit-koder, der alle udledes af den lokale gennemgang:

  • 0: kæden blev verificeret, og vores dom var enig
  • 2: delvis: enten er der markørrækker af typen chain-broken (vi skrev revisionsrækken, men transaktionen for kæden fejlede, så rækken bevares for at være synlig, og kæden mangler et led der), eller loftet over antal rækker afkortede gennemgangen, så en del af kæden ikke blev tjekket. Bemærk, at vi rapporterer ok for en afkortet gennemgang, og det gør CLI’en ikke: at attestere rækker, ingen har gennemgået, er netop den slags overdrivelse, kommandoen er til for at undgå.
  • 3: brudt: en rækkes genberegnede hash matcher ikke dens gemte hash, ELLER en rækkes prev_hash matcher ikke den løbende kæde. Outputtet indeholder id’et på rækken first_break og årsagen.
  • 4: forankringen stemmer ikke: den WORM-forankrede spids findes ikke i din kæde, hvilket betyder, at den forankrede række blev slettet eller ændret. Bemærk, at det er et tjek af indeholdelse: en aktiv tenant, hvis nyeste rækker endnu ikke er forankret, er normal og består.
  • 5: vi var uenige: din gennemgang var ren, men vores dom var det ikke. Som regel er en række tilføjet mellem de to kald. Kør igen for at bekræfte; fortsætter det, ser vi noget, som du ikke ser.

Retningen af en uoverensstemmelse er ikke symmetrisk, og CLI’en siger, hvilken den fandt. At vores dom er dårligere end din, skyldes som regel timing. At vores dom er bedre end din, at vi siger ok, hvor din egen beregning siger broken, er netop det tilfælde, hele mekanismen findes for at afsløre, og det rapporteres som en hændelse og ikke som en advarsel.

Hvad vi gør internt

Kædens hoved forankres hvert 2. minut i WORM-lager, og kæden af forankringer verificeres uafhængigt igen efter en løbende tidsplan: hver tenants kæde af forankringer gennemgås fremad fra begyndelsen og tjekkes for kontinuitet. Enhver uoverensstemmelse, eller enhver tenant, hvis forankring bliver forældet (ældre end ~10 minutter; 12 minutter i værste fald), tilkalder vagten med det samme som en tidlig indikator for, at skrivningen af forankringer er stoppet. Påvirkningen for kunder er dækket af DPA §11A, og huller beskrives efter hændelsen på vores statusside.

Pseudonymisering under GDPR

Når du gør brug af retten til sletning, bliver revisionshændelser for din tenant pseudonymiseret, ikke slettet: feltet actor bliver til [erased], og fields_json nulstilles til {}. Kædeleddet (hashen) bevares, så kæden forbliver verificerbar; personoplysningerne fjernes.

Det betyder, at en kørsel af fremforge audit-verify mod en slettet tenant vil vise tenant_erased_count > 0, og de rækker rapporteres separat i stedet for som brud: en pseudonymiseret række matcher bevidst ikke længere sin egen hash, mens dens led til den følgende række stadig tjekkes. De fem tællere (verified_count, pre_chain_epoch_count, chain_broken_count, tenant_erased_count, payload_redacted_count) summerer ikke til rows_walked: én række kan både være slettet og have fået sit indhold redigeret og tælles under begge. Kæden er intakt; indholdet er renset. Selve rækkerne slettes ikke, når opbevaringsperioden på 3 år for WORM-forankringen udløber; i dag sletter intet job dem (rettet 2026-10-08: denne side sagde tidligere, at de blev slettet).

Uafhængig verifikation

Revisorer kan under NDA anmode om en gennemgang af kæden fra ende til anden: skriv til compliance@frem.sh for at få den tekniske vurderingspakke.