Hoe we complexe permissies opnieuw hebben opgebouwden zonder over te stappen naar Zanzibar

RBAC (Role-Based Access Control), permissies en autorisatie behoren tot dezelfde klasse engineeringprojecten als facturering, schema-migraties en auditlogs:

  • Ze worden beschouwd als noodzakelijke hulpmiddelen.
  • Gebruikers besteden er zo min mogelijk tijd aan.
  • Het zijn lastige engineeringprojecten, zelfs als ze eenvoudig lijken.

In het beste scenario zijn er geen klachten en is het systeem correct. Het kan nooit "uitblinken"; er is geen 10x RBAC-systeem waar mensen op Twitter enthousiast over zijn. Toegangscontroles zijn een vakje dat een product moet afvinken, maar geen 'killer feature'.

Dat maakt ze niet minder belangrijk. Enterprise-kopers zullen nooit een orderformulier tekenen zonder deze functies. En in secrets management (een van de categorieën waarin Infisical opereert), betekent een fout in de toegang dat iemand inloggegevens ziet die hij niet zou mogen zien, of dat de deployment van een klant om drie uur 's nachts faalt omdat een service de toegang tot een secret is verloren.

Tailscale en Oso hebben beiden geschreven over waarom autorisatie moeilijker is dan het lijkt. Het bouwen van toegang op basis van mappen heeft me hetzelfde geleerd, maar vanuit een andere hoek.

Waarom we toegang op basis van mappen hebben gebouwd

Conceptueel is RBAC eenvoudig. Elke identiteit, mens of machine, heeft een rol (bijv. admin, member of guest) die permissies bevat:

  • Acties: Wat je kunt doen.
  • Onderwerpen: Op wat je de acties kunt uitvoeren.

We maken gebruik van CASL, waarbij een basispermissie gekoppeld aan een rol er als volgt uit kan zien:

{
  "action": ["readValue", "create"],
  "subject": "secrets",
  "conditions": { "environment": "staging" }
}

Deze identiteit kan secrets lezen en aanmaken in staging-omgevingen. CASL weigert automatisch alles wat niet expliciet is toegestaan. Een rol met de bovenstaande permissie zou dus nooit secrets in productie-omgevingen kunnen aanpassen.

Role-based access is populair omdat mensen het intuïtief begrijpen. Rollen in RBAC komen overeen met rollen in teams. Het in bulk toekennen of weigeren van privileges bespaart tijd vergeleken met het handmatig instellen van permissies voor elk teamlid, wat anders een fulltime baan zou worden.

Maar niet alle wijzigingen in toegangscontrole passen perfect bij rollen. Er zijn twee specifieke scenario's waarom we toegang op basis van mappen hebben gebouwd:

  1. Extra toegang nodig: Neem een engineer die de enige is in zijn team met Netsuite-ervaring. Deze persoon heeft de credentials van de engineer-rol nodig, plus specifieke Netsuite-credentials.
  2. Toegang beperken: Een externe specialist die is ingehuurd om AWS Lambda-functies te refactoren heeft engineering-credentials nodig, maar er is geen reden om hem financiële gegevens te laten zien. Deze persoon heeft de credentials van de engineer-rol nodig, minus de billing-credentials.

In beide gevallen verandert de rol van de persoon niet, maar veranderen de toegangsvereisten wel. Dit oplossen met conventionele RBAC geeft ons twee onbevredigende opties:

  • Het bewerken van de engineer-rol verleent onnodige toegang aan anderen of ontneemt noodzakelijke toegang.
  • Het maken van een aangepaste rol op basis van 'engineer' creëert parallelle configuraties voor een eenmalig gebruiksscenario, wat kan leiden tot configuratiedrift.

Bij Infisical hadden we een derde optie genaamd Additional Privileges, die nu wordt vervangen door folder-based RBAC. Dit was een per persoon toegekend recht dat bovenop een rol zat. Het werkte, maar was te omslachtig (het toonde elke mogelijke permissie die je kon verlenen) en kon geen privileges intrekken.

Dit was krachtig, maar niet intuïtief. Het vereiste begrip van elk Infisical-concept, onze data-hiërarchie en de specifieke setup van die instantie. We kunnen dit niet van elke gebruiker verwachten, waardoor additional privileges twee risico's met zich meebrachten:

  • Gebruikers konden de privileges foutief configureren en te veel of te weinig toegang verlenen.
  • Gebruikers konden besluiten dat ze er geen tijd voor hadden en bredere toegang verlenen, wat de beveiligingspositie in gevaar bracht.

Daarnaast sloegen additional privileges paden op als strings, waardoor het hernoemen van een map de toegang verbrak. Mappen hebben een folderID (UUID), wat betekent dat het hernoemen van een map of het verplaatsen ervan van de ene omgeving naar de andere de toegang niet verbreekt.

We hadden een oplossing nodig die intuïtief was voor gebruikers. Mappen waren de juiste plek omdat we geen standaard mapstructuur leveren. Als er een map bestaat, weten gebruikers waarom en wat erin zit, omdat ze deze zelf hebben aangemaakt. Folder-based RBAC vereenvoudigt de vertaling van het toegangs문제 in het hoofd van de gebruiker naar een configuratie in Infisical.

Hoewel het resultaat voor de gebruiker eenvoudiger is, was het engineeringwerk complex omdat we ons bestaande model niet simpelweg konden uitbreiden.

Waarom ons RBAC-systeem te complex was om uit te breiden

We konden niet zomaar een extra manier toevoegen om toegang te verlenen die alleen voor mappen werkt, omdat er al veel manieren zijn waarop iemand toegang kan hebben tot een secret. Enkele voorbeelden van hoe toegang in Infisical wordt gedefinieerd:

  • Een gebruiker kan direct lid zijn van een project, net als een machine-identiteit.
  • Een groep kan het lidmaatschap beheren en zowel menselijke als machine-identiteiten bevatten (vaak via SCIM, SAML, OIDC of LDAP).
  • Rollen kunnen ingebouwd of aangepast zijn.
  • Verouderde service-tokens hebben een apart scope-model dat we nog steeds evalueren.

Dit lijkt misschien rommelig, maar de meeste RBAC-systemen hopen complexiteit op na vele "ja's" tegen klantverzoeken. Omdat Infisical open-source is, kunnen we deze functies niet zomaar verwijderen zonder potentieel problemen te veroorzaken voor self-hosted instanties. Secrets zijn bovendien bedrijfskritisch; fouten kunnen leiden tot outages of het volledig stilvallen van een engineering-organisatie.

Daarnaast zijn we niet overgestapt op een Zanzibar-stijl systeem zoals SpiceDB of OpenFGA. Dat zou betekenen dat elke permissiecontrole herschreven moet worden en dat elke self-hosted klant een extra stateful service moet draaien, wat zou leiden tot breaking changes.

Elk systeem dat we zouden bouwen, moest werken met alles wat al bestond. Dat betekende dat we dit project in twee fasen moesten aanpakken: eerst het standaardiseren van de permissies, en daarna de logica om de juiste permissie toe te passen. Dat laatste was het moeilijkste engineeringprobleem, maar het eerste was de fundering.

De vijf standaard permissieniveaus

We hebben eerst een lijst van vijf niveaus van permissies gebouwd. Deze zijn specifieker dan generieke lees- en schrijfrechten, maar blijven intuïtief.

NiveauWat het toevoegt
ListZien welke secrets er bestaan en hun namen, zonder de waarden te zien
ReadWaarden lezen, commit-geschiedenis bekijken, een dynamic secret lease nemen
EditSecrets en imports aanmaken, bewerken en verwijderen
ManageRotaties, dynamic secrets, syncs en honey tokens configureren
Full AccessMaptoegang verlenen aan andere mensen

Deze permissies zijn de bouwstenen voor onze implementatie van folder-based access. Technisch gezien wordt folder-based RBAC opgeslagen als een Additional Privilege. Ze bestaan als een rij in de additional_privileges tabel en hergebruiken kolommen zoals de identiteit van de gebruiker en of een toekenning tijdelijk is.

De functie introduceert twee nieuwe kolommen:

  • role is het permissieniveau uit de bovenstaande tabel.
  • folderID is de map waar de toekenning toegang tot geeft.

Oude Additional Privileges hebben een null waarde in een van deze nieuwe kolommen, wat ze onderscheidt van de nieuwere folder-based grants. Hierdoor blijven oude permissies werken, terwijl controles voorkomen dat een rij beide modellen tegelijk probeert te gebruiken.

Nadat de folder-based privileges waren geconfigureerd, was de logica nodig om deze permissies daadwerkelijk toe te passen.

Zorgen dat toegang op basis van mappen "wint"

Het bewust instellen van toegang op een map overrulet alles wat is overgeërfd van rollen, groepen en andere privileges. Dit was niet eenvoudig, omdat de reden dat we dit bouwen juist een conflict is tussen de toegang die een rol verleent en de toegang die iemand daadwerkelijk zou moeten hebben. Een identiteit kan toegang tot een object op meerdere plekken geconfigureerd hebben, dus we moesten garanderen dat de folder-based access grant altijd prioriteit krijgt.

In CASL-controles wint de laatste passende regel, en een deny (weigeren) gaat boven een allow (toestaan). Om alle bestaande systemen functioneel te houden, hebben we de maptoegang in twee lagen verdeeld:

  1. Laag één behoudt de bestaande regels.
  2. Laag twee wordt hieraan toegevoegd: dit is een blok deny-regels die elke actie op die map weigeren, gevolgd door de allow-regels voor het niveau dat aan de grant is gekoppeld.
[
  // Van de "engineer" rol van de contractant, ongewijzigd
  {
    "action": ["readValue", "create", "delete"],
    "subject": "secrets",
    "conditions": { "environment": "prod" }
  },
  // Laag twee: alles weigeren binnen /payments
  {
    "action": ["readValue", "create", "delete"],
    "subject": "secrets",
    "inverted": true,
    "conditions": { "environment": "prod", "secretPath": { "$eq": "/payments" } }
  },
  // Daarna alleen toestaan wat het 'Read' niveau toestaat
  {
    "action": ["readValue"],
    "subject": "secrets",
    "conditions": { "environment": "prod", "secretPath": { "$eq": "/payments" } }
  }
]

Het deny-blok is altijd identiek en alleen het niveau bepaalt wat er weer wordt toegestaan. Geen enkel niveau is verantwoordelijk voor het weigeren. Deze deny-lijst wordt geverifieerd door een test die faalt als iemand een nieuwe path-scoped permissie toevoegt zonder deze uit te breiden.

We ondersteunen nog steeds aangepaste rollen en permissies, en oude Additional Privileges bestaan nog, maar deze logica zorgt ervoor dat de intuïtieve folder-based RBAC prioriteit krijgt.

Ten slotte moesten we ervoor zorgen dat onze cache de juiste permissies opsloeg.

Hoe we cache-invalidering hebben opgelost

Het cachen van permissies die onderhevig zijn aan wijzigingen is lastig, omdat we moesten garanderen dat updates onmiddellijk effect hebben zonder de responstijd te vertragen.

In Infisical vinden permissiecontroles plaats bij vrijwel elk verzoek. Daarom cachen we resultaten en valideren we deze met een fingerprint. Folder grants zorgden hier voor problemen, en het oplossen hiervan vereiste meerdere pogingen:

  • Eerste poging: De grant-rijen fingerprinten zoals alles wat we doen. Een grant slaat de folder ID op, maar de regels hebben een pad nodig, dus lossen we de ID op tijdens het compileren. Dit betekende dat het hernoemen van een map ervoor zorgde dat elke grant op die map anders compileerde.
  • Tweede poging: De mappen waarnaar de grants verwijzen fingerprinten. Dit mislukte omdat een pad wordt gebouwd door de ancestor-keten omhoog te lopen. Het hernoemen van /a verandert wat een grant op /a/b oplevert, terwijl de rij van /a/b zelf ongewijzigd blijft. Je zou bijna de hele boom moeten fingerprinten.

Uiteindelijk hebben we een versie-teller per project geïmplementeerd. Deze teller wordt verhoogd bij elke schrijfactie aan de betreffende rij, inclusief elke map-hernoeming, verplaatsing en verwijdering. Deze teller wordt in de fingerprint gelezen in plaats van in de cache-key.

Conclusie

Het bouwen van RBAC is niet glamoureus, maar het is essentieel. Hoewel mensen zelden opmerken hoe geweldig het RBAC- of facturatiesysteem van een product is, geldt het omgekeerde wel: een fout hier leidt onvermijdelijk tot klachten.

Dat is waarom het belangrijk is om ervoor te zorgen dat dingen simpelweg "werken".