# Sikkerhet i m51.ai: Slik beskytter vi kundedataene dine

> Row-Level Security, tofaktor, rate-limiting og kontinuerlig revisjon — de konkrete tiltakene som holder multi-tenant-dataene dine trygge.

- Publisert: April 2026 (2026-04-14)
- Forfatter: m51.ai Lab
- Lesetid: 9 min lesing
- Serie: m51 Plattform
- Kanonisk HTML-utgave: https://m51.ai/lab/sikkerhet
- English edition (Markdown): https://m51.ai/en/lab/sikkerhet.md
- Alle Lab-artikler (Markdown-indeks): https://m51.ai/lab/index.md

---

Markedsføringsdata er blant de mest sensitive dataene en bedrift har. Kampanjebudsjetter, kundelister, organisk ytelse, annonseresultater, og ikke minst OAuth-tilganger til Meta, LinkedIn, Google Ads og analyseverktøy — alt samlet på ett sted.

Når vi bygger en multi-tenant plattform som håndterer dette for hundrevis av kunder samtidig, kan ikke sikkerhet være et salgsargument som legges på i etterkant. Det må være en arkitekturell forutsetning fra første linje kode.

**Denne artikkelen beskriver de konkrete tekniske tiltakene som er i drift i m51.ai i dag. Ingen løfter, ingen hype — bare hva vi gjør, hvorfor, og hvordan du kan verifisere det.**

---

## Dataisolasjon mellom kunder

Alle kjernemodeller i plattformen har en customer_id som identifiserer hvilken kunde dataen tilhører. Denne kolonnen brukes både i applikasjonslaget, der hvert API-kall verifiserer at innlogget bruker tilhører riktig customer_id, og i databaselaget, der Row-Level Security (RLS) er aktivert på alle sensitive tabeller.

**RLS er Postgres sin innebygde mekanisme for radbasert tilgangskontroll. Vi bruker den bevisst som total sperre, ikke som per-kunde-filter: de beskyttede tabellene har kun policyer for service_role — nøkkelen som bare backend-tjenestene våre holder — og ingen policyer for anon- eller authenticated-rollene. Anon-nøkkelen, som er tilgjengelig for klienter, har dermed ikke tilgang til de beskyttede tabellene i det hele tatt.**

Én ting skal sies presist, for det er et spørsmål enhver teknisk innkjøper bør stille: backend kjører selv som service_role, som i Supabase-modellen er en privilegert rolle som passerer RLS. Isolasjonen mellom kunder håndheves derfor i applikasjonslaget — hvert kunde-scopet API-kall går gjennom en tilgangskontroll som verifiserer at innlogget bruker har tilgang til akkurat den kunden, og hver spørring filtreres på customer_id. RLS sin jobb i denne arkitekturen er å garantere at databasen ikke kan nås utenom backend.

At applikasjonslaget faktisk holder, er ikke en påstand — det er testet mekanisk. En vakt-test itererer samtlige API-ruter og feiler bygget hvis et endepunkt mangler tilgangskontroll (offentlige ruter må stå i en begrunnet allowlist). I tillegg kjører dedikerte cross-tenant-tester som forsøker å lese andre kunders ressurser og krever 403/404 tilbake, og en adversarial testsuite der enhver cross-tenant-lekkasje er definert som merge-blokkerende P0.

### Hvilke tabeller beskyttes av RLS

Alle tabeller som inneholder kundespesifikke data kjører under service_role-policyer. Et utdrag fra migrasjon 043 som aktiverte RLS på tidligere ubeskyttede tabeller:

| Tabell | Innhold | RLS-policy |
| --- | --- | --- |
| daily_kpi_history | Daglige KPI-snapshots per kunde | service_role only |
| pagespeed_snapshots | PageSpeed-målinger | service_role only |
| agent_activity_log | Logg over agent-kjøringer | service_role only |
| scroll_stopper_simulations | Kampanjesimuleringer | service_role only |
| budget_recommendations | Budsjettanbefalinger | service_role only |
| agreement_acceptances | Avtaleaksepter | service_role only |

Vi kjører også en automatisert sikkerhetsskanning av databaseskjemaet for hver migrasjons-PR. Hvis noen åpner en migrasjon som glemmer å sette RLS på en ny tabell, blokkeres PR-en automatisk før den når produksjon.

## Kryptering av sensitive data

RLS beskytter mot uautoriserte spørringer mot databasen. Men hva skjer hvis en angriper på en eller annen måte får tak i selve service_role-nøkkelen? Uten et lag til ville de fått klartekst-OAuth-tokens for alle kunder. Det var uakseptabelt, så vi krypterer alle sensitive felter individuelt før de skrives til databasen.

### Hva som krypteres

- OAuth-tokens for Google, Meta og LinkedIn — access_token, refresh_token og tilknyttet e-postadresse
- Integrations-secrets for Clickup, HubSpot, Tripletex og Slack — inkludert webhook-URL-er og API-nøkler

**Verdiene lagres som Fernet-ciphertext direkte i databasefeltene. Fernet er en autentisert krypteringsalgoritme (AES-128-CBC kombinert med HMAC-SHA256) som både hindrer lesing og oppdager manipulering.**

### Nøkkelen er separert fra databasetilgangen

Krypteringsnøkkelen avledes via HKDF-SHA256 fra en dedikert miljøvariabel, TOKEN_ENCRYPTION_KEY, som er atskilt fra både DB_SERVICE_ROLE_KEY og JWT_SECRET. Applikasjonen nekter å starte i produksjon hvis nøkkelen mangler eller er kortere enn 32 tegn — samme mønster som JWT_SECRET-validatoren.

**Det betyr at en angriper må kompromittere to uavhengige hemmeligheter for å lese tokens: både databasens service_role og applikasjonens krypteringsnøkkel. De har forskjellige rotasjonsrutiner og lagres ikke sammen.**

### Tamper-resistance via kontekstbinding

Hver ciphertext er bundet til en kombinasjon av kunde, provider og felt. Fernet-API-et i Python-biblioteket cryptography eksponerer ikke et eget AAD-parameter, så bindingen er bygget som et lag rundt Fernet: kontekststrengen legges inn i selve den autentiserte meldingen før kryptering, og ved dekryptering verifiseres det at lagret kontekst er identisk med forventet kontekst. Hvis noen forsøker å flytte en ciphertext fra én rad til en annen — for eksempel kopiere kunde A sitt Meta-token inn i kunde B sin rad for å tilegne seg tilgang — feiler dekrypteringen, og feltet behandles som utilgjengelig (fail-closed). HMAC-laget i Fernet oppdager i tillegg enhver endring i selve ciphertexten. Egenskapen er den samme som AAD gir i et AEAD-API — konteksten autentiseres sammen med innholdet — med den forskjellen at konteksten her i tillegg er kryptert.

### Versjonering og nøkkelrotasjon

Alle ciphertext har et versjonsprefiks (v1:...), og oauth_tokens- og integrations-tabellene har en encryption_version-kolonne. Det gjør det mulig å rotere nøkler uten nedetid: vi introduserer v2, re-krypterer gradvis i bakgrunnen, og fjerner v1 først når alle rader er oppdatert.

### KMS-klar arkitektur

Koden er bygget rundt et FieldEncryptor-grensesnitt. Dagens implementasjon bruker applikasjonslag-kryptering fordi produksjonsmiljøet vårt ikke har native KMS-integrasjon, men hele verktøykjeden — kryptering, dekryptering, rotasjon — kan byttes til AWS KMS, GCP KMS eller HashiCorp Vault uten å røre koden som skriver eller leser tokens. Vi bytter når første enterprise-kunde stiller eksplisitt krav.

### Tilstand i produksjon

Alle eksisterende rader er migrert fra klartekst til v1-kryptering via et idempotent backfill-script. Produksjonsdatabasen har null rader med klartekst-tokens. Nye tokens krypteres automatisk ved innskriving gjennom samme kodesti.

## Autentisering og tilgang

Pålogging går via et administrert Auth-lag med JWT som sesjonsformat. Passord lagres hashet med bcrypt. I produksjon kreves det at JWT_SECRET er minst 32 tegn langt — applikasjonen nekter å starte hvis denne ikke er riktig konfigurert.

Rollestruktur er bevisst enkel: en binær admin/bruker-rolle kombineres med et numerisk access_level som styrer hvilke sider og funksjoner brukeren ser. Tilgang verifiseres på hvert API-kall — ikke bare i UI-laget.

### Tofaktorautentisering (MFA)

MFA er innebygd, ikke en tilleggstjeneste fra tredjepart. Vi støtter tre metoder side om side:

- TOTP — standard autentiseringsapper (Google Authenticator, 1Password, Authy, osv.)
- SMS — engangskode til verifisert telefonnummer
- Gjenopprettingskoder — engangskoder du kan skrive ned hvis du mister tilgang til enheten din

Rate-limiting på MFA-endepunktene hindrer brute-force: send-OTP, verify-OTP og recovery-code-bruk har alle strammere grenser enn det generelle API-et.

## Beskyttelse av AI-laget

En AI-plattform har et angrepsflate som tradisjonelle webapper ikke har. Prompt-injeksjon, SSRF via AI-initierte HTTP-kall, og eksfiltrering gjennom hallusinerte lenker er reelle trusler. Vi behandler alle tre som førsteklasses sikkerhetsproblemer.

- Prompt-injeksjon: flere titalls kjente mønstre blokkeres før input når modellen — alt fra "ignore previous instructions" på flere språk til delimiter-injeksjon, rolleovertakelse, system-prompt-ekstraksjon og jailbreak-forsøk
- SSRF-vern: AI-verktøy som henter eksterne URL-er kjører gjennom en validator som avviser localhost, cloud-metadata-endepunkter (169.254.169.254), private IP-områder og DNS-rebinding
- HTML-sanitering: alle sanitiserte felter fjerner script-tagger, iframes, event-handlers (onclick og lignende) og javascript:-URI-er før de lagres eller rendres
- Unicode-normalisering: fullwidth-tegn og combining marks brukes aktivt for å omgå mønstermatching — input normaliseres før validering
- Tilgangskontroll på agent-nivå: verifiserer at innlogget bruker faktisk eier customer_id-en som forespørselen refererer til, ikke bare at brukeren er autentisert

**Alle fem punktene har dedikerte testklasser i test_security.py som kjøres på hver eneste PR.**

## Infrastruktur og motstandsdyktighet

Sikkerhet er ikke bare angrepsforsvar — det handler også om å holde plattformen tilgjengelig og forutsigbar under press, uten å blottlegge systeminformasjon når noe går galt.

### Rate-limiting

API-et har en generell grense, og strammere grenser på endepunkter som er sensitive for misbruk:

| Endepunkt | Grense |
| --- | --- |
| Generell API-trafikk | 60 forespørsler per minutt per IP |
| Innlogging | 5 forsøk per minutt |
| Registrering | 3 forsøk per minutt |
| Passordendring | 3 forsøk per minutt |
| MFA-verifisering | Strammere per-brukergrense |

Rate-limit-tellerne deles på tvers av alle backend-workere via en sentral datastore, slik at grensene holder selv under høy last med mange parallelle prosesser.

### Circuit breaker og graceful degradation

En circuit breaker sitter foran databaselaget. Etter fem påfølgende feil bytter den til OPEN-tilstand, returnerer HTTP 503 i 30 sekunder, og prøver deretter en forsiktig HALF_OPEN-probe før den eventuelt går tilbake til normal drift. Dette forhindrer at en midlertidig databaseforstyrrelse kaskaderer til full nedetid, og det gir databasen tid til å komme seg uten å drukne i retry-storm fra plattformen.

Databaseklienten over circuit breakeren har egen retry med eksponentiell backoff (0,5 → 1 → 2 sekunder) for forbigående feil som 408, 429, 500, 502, 503 og 504.

### Sikkerhetsheaders

Alle HTTP-svar fra backend setter:

- Strict-Transport-Security — tvinger HTTPS i nettlesere som har besøkt oss før
- Content-Security-Policy — begrenser hvor skript og ressurser kan lastes fra
- X-Frame-Options: SAMEORIGIN — forhindrer at plattformen embeddes i iframes på fremmede domener
- X-Content-Type-Options: nosniff — stopper nettlesere fra å gjette MIME-typer

CSP-policyen for dashboardet skiller seg fra CSP-en for markedsføringssidene: dashboardet har ikke connect-src til tredjeparts-analyseverktøy, mens de offentlige sidene gjør det for å kunne måle konvertering.

## Hemmeligheter og konfigurasjon

Null hemmeligheter er hardkodet i kildekoden. Alle API-nøkler, OAuth-credentials, database-URL-er og interne secrets lastes fra miljøvariabler i produksjonsmiljøet og er bare tilgjengelige for kjøretidsprosessene. Pydantic-basert validering sjekker at kritiske verdier er satt før applikasjonen starter — hvis noe mangler i produksjon, nekter prosessen å komme opp i det hele tatt.

Tre kritiske hemmeligheter er bevisst separerte med uavhengige rotasjonsrutiner:

- JWT_SECRET — signerer brukersesjoner; må være minst 32 tegn
- DB_SERVICE_ROLE_KEY — gir backend tilgang til databasen bak RLS
- TOKEN_ENCRYPTION_KEY — dekrypterer OAuth-tokens og integrations-secrets; må være minst 32 tegn

**En angriper må kompromittere alle tre for å eksfiltrere klartekst-tokens fra produksjonsdatabasen. Å lekke én nøkkel reduserer ikke sikkerheten til de to andre.**

Service-account-JSON for Google Workspace base64-kodes i en egen miljøvariabel og dekodes i minne ved oppstart. Selve JSON-filen ligger aldri på disk.

## Sikkerhet som kontinuerlig disiplin

Kontrollene over kjører ikke bare i produksjon. De valideres på hver eneste pull request av GitHub Actions før kode kan merges til main.

- ruff — Python lint-regler som fanger utrygge mønstre og dårlig stil
- ESLint + TypeScript-kompilering i strict-modus — frontend-typechecks hele kodebasen
- pytest med coverage — 787 backend-tester, inkludert hele sikkerhetssuiten i tests/test_security.py, tests/test_field_encryption.py og tests/test_oauth_token_encryption.py
- vitest — 162 frontend-tester som dekker tilgangskontroll, API-klient og sensitive hooks
- npm audit — stopper PR-er med høy- eller kritisk-alvorlige frontend-avhengigheter
- pip-audit — scanner alle Python-avhengigheter mot kjente sårbarheter i PyPA-databasen
- Database-sikkerhetsskanning — blokkerer migrasjons-PR-er med feil som manglende RLS eller usikre funksjoner

Med andre ord: en utvikler kan ikke ved et uhell slå av RLS på en tabell, legge inn et prompt-injeksjons-sårbart endepunkt, eller introdusere en avhengighet med kjent CVE. CI fanger det før koden når kunder.

## Hva artikkelen ikke dekker

Vi har med vilje holdt oss til tekniske kontroller som er verifiserbare i kildekoden. To ting ligger utenfor omfanget:

- Juridisk behandling av personopplysninger — rettslig grunnlag, databehandleravtaler, dine rettigheter under GDPR — dekkes separat i personvernerklæringen
- Formelle sertifiseringer som SOC 2 eller ISO 27001 — vi har ikke disse per april 2026; vi er åpne om hvor vi står og diskuterer gjerne konkrete krav i salgssamtaler

**Les personvernerklæringen for juridiske detaljer** [Personvern](https://m51.ai/personvern)

---

## Ansvarlig rapportering

Hvis du oppdager en sårbarhet i m51.ai, vil vi høre om det. Send e-post til personvern@m51.no med en beskrivelse av funnet, gjerne med stegene som reproduserer problemet. Vi svarer innen to virkedager, og vi forfølger ikke sikkerhetsforskere som rapporterer i god tro.

Ikke test sårbarheter mot produksjonsdata som ikke tilhører deg. Hvis du trenger et testmiljø for verifisering, ta kontakt først — vi hjelper deg med å sette opp en isolert konto.

## Om m51.ai

m51.ai er plattformen som gir markedsføringsteam i bedrifter tilgang til spesialiserte AI-agenter for innholdsproduksjon, SEO, annonsering og kampanjeoptimalisering. Sikkerhetstiltakene beskrevet her er på plass fordi kundene våre stoler oss med arbeid som er kjernen av forretningen deres.

**Vil du se plattformen i aksjon? Book en demo.** [Book demo](https://m51.ai/#kontakt)

## Ofte stilte spørsmål

### Hvordan er dataene mellom ulike kunder isolert?

Hver kunde har sin egen customer_id som ligger som en nøkkel på alle kjernemodeller, og hvert kunde-scopet API-kall verifiserer at innlogget bruker har tilgang til akkurat den kunden. Denne håndhevelsen er mekanisk testet: en vakt-test feiler bygget hvis et endepunkt mangler tilgangskontroll, og dedikerte cross-tenant-tester forsøker å lese andre kunders data og krever avvisning. I databasen er Row-Level Security (RLS) aktivert på alle sensitive tabeller med policyer kun for service_role — klienter kan ikke nå tabellene utenom backend i det hele tatt. Backend kjører selv som service_role og passerer RLS, så isolasjonen mellom kunder ligger i applikasjonslaget; databasesperren garanterer at ingen kan gå utenom det.

### Hvor lagres API-nøklene mine til Meta, LinkedIn og Google Ads?

OAuth-tokens lagres kryptert på feltnivå i produksjonsdatabasen. Access_token, refresh_token og tilknyttet e-postadresse krypteres med Fernet (AES-128-CBC pluss HMAC-SHA256-autentisering) før skriving, med en nøkkel avledet fra TOKEN_ENCRYPTION_KEY — en miljøvariabel som er fysisk separert fra både databasens service_role-nøkkel og JWT_SECRET. En angriper som får tilgang til databaseinnholdet ser bare ciphertext. Samme kryptering dekker integrations-secrets for Clickup, HubSpot, Tripletex og Slack.

### Støtter M51 tofaktorautentisering?

Ja. Vi støtter TOTP-autentisering (Google Authenticator, 1Password, Authy og lignende), SMS-basert engangskode, samt engangs-gjenopprettingskoder hvis du mister tilgang til hovedenheten din. TOTP kan aktiveres fra kontoinnstillinger.

### Har dere SOC 2 eller ISO 27001-sertifisering?

m51.ai har per i dag ikke formell SOC 2- eller ISO 27001-sertifisering. Artikkelen her beskriver de konkrete tekniske tiltakene som er på plass. Hvis organisasjonen din krever sertifisering som forutsetning for bruk, ta kontakt — vi svarer ærlig på hvor vi er i prosessen og kan dele dokumentasjon på de tekniske kontrollene.

---

*Dette er en maskinlesbar Markdown-utgave av HTML-artikkelen på https://m51.ai/lab/sikkerhet. Innholdet genereres fra samme kilde som nettsiden.*
