Met de komst van de public cloud is het IT-landschap van veel organisaties sterk veranderd. Tegenwoordig hebben organisaties vaak meerdere landing zones in bijvoorbeeld de public cloud aangevuld met on-premises landing zones of infrastructuur die op de edge draait. Deze diversificatie van het IT-landschap die nog eens versterkt werd door het aantal agents die deelnemen aan het netwerk van een organisatie, denk daarbij aan mobiele telefoons, IoT devices en dergelijke, heeft ervoor gezorgd dat organisaties anders naar het security vraagstuk van hun IT-landschap zijn gaan kijken.
Zero trust wordt vaak ook wel perimeterless security genoemd, wat eigenlijk de lading best goed dekt. In het pre cloud tijdperk werd er veel in lagen van security gedacht, eigenlijk net zoals vroeger bij een kasteel. De slotgracht was de eerste perimeter (laag) van beveiliging, de kasteelmuur was de volgende perimeter en tot slot was er de burcht van het kasteel waar de meest waardevolle spullen werden opgeborgen. Dit werkte eigenlijk best goed totdat de diversificatie optrad. Het IT-landschap werd eigenlijk meer en meer een mesh van landing zones, applicaties, devices en users die in verschillende configuraties met elkaar moeten communiceren. En daarmee voldoet het traditionele lagen model niet meer aan de behoefte van deze tijd en moeten we andere concepten introduceren om het IT Landschap veilig te houden.
Zero trust architectuur
Er valt veel te zeggen over zero trust architectuur, het domein is vrij complex en daarom is het voor developers soms moeilijk om het goed toe te passen. In dit artikel gebruik ik een aantal principes die onder andere ook terug te vinden zijn in adviezen van de originele paper van het NIST (National Institute of Standards and Technology) en adviezen van onder andere het NCSC (National Cyber Security Centre).
In het “Zero Trust Architecture” paper wordt gesproken over 7 principes van zero trust, waarvan ik er in dit artikel een aantal wil bespreken namelijk:
“All data sources and computing services are considered resources.”
“Access to resources is determined by dynamic policy—including the observable state of client identity, application/service, and the requesting asset—and may include other behavioral and environmental attributes”
“All resource authentication and authorization are dynamic and strictly enforced before access is allowed”
Deze principes zijn interessant voor veel developers omdat het iets zegt over hoe je toegang tussen applicaties, services en gebruikers moet authentiseren en autoriseren. Met het implementeren van deze 3 zero trust principles ben je er nog niet, maar het gaat voor dit artikel te ver om ook de andere principles toe te lichten.
RBAC-Model
Role based access control is in veel gevallen een goed model om een zero trust architectuur mee te ontwikkelen. Het voorziet in elementen om zonder harde perimeters een zero trust architectuur te ontwerpen en te implementeren die toch veilig is.
Een RBAC-model kent doorgaans een subject (vaak ook wel identity) genoemd dat staat voor een gebruiker of andere agent in de architectuur. Dat kan dus bijvoorbeeld een ingelogde gebruiker, service of device zijn. Een subject is aan één of meerdere roles toegewezen welke weer één of meerdere permissies bevat. Een permissie geeft uiteindelijk toegang tot een bepaalde operatie of resource.
De API’s, applicaties of services in de architectuur moeten valideren of een subject zich geauthentiseerd heeft bij de gedeelde identityserver aansluitend dient het subject de juiste permissies te bevatten om de operatie uit te kunnen voeren.
Identity Management
Het fundament van een goede RBAC-implementatie ligt in een single source van (subjects) identities. Steeds meer organisaties zien de interne gebruiker als een van de belangrijkste risico factoren. Dit komt omdat zij vaak fouten maken door bijvoorbeeld in phishing mails te trappen of doordat ze wachtwoorden hergebruiken tussen privé en werk accounts. Vanuit een security perspectief is het belangrijk dat een user identity altijd te herleiden is naar een daadwerkelijke gebruiker en idealiter dat hetzelfde account zoveel mogelijk gebruikt wordt in het IT-landschap (SSO). In de praktijk kan dit bijvoorbeeld het corporate account van een gebruiker zijn waarmee de gebruiker ook toegang krijgt tot de cloud omgevingen.
Naast user accounts hebben we in een zero trust architectuur ook te maken met machine of service identities die een rol spelen in het landschap. We kunnen er namelijk niet meer vanuit gaan dat alles wat achter de een bepaalde perimeter leeft veilig en een good actor is. Het principe van zero trust houdt in dat elke identiteit zowel users, services als machines zich kunnen authentiseren en autoriseren.
Op OpenID of OAuth gebaseerde identity services gebruiken doorgaans 1 of meerdere identity providers om de gebruiker te authentiseren, dit kan bijvoorbeeld een Azure Active Directory, Facebook of Google account zijn of eventueel een eigen ontwikkelde identity provider. In OpenId Connect kunnen clients op een bepaalde manier een gebruiker authentiseren bij de OpenId Connect server, dit noemen we de grant type. Voor server to server communicatie wordt hier vaak de client_credentials flow voor gebruikt waarbij een client_id en client_secret worden uitgegeven waarmee de service, applicatie of device zich kan authentiseren.
Ik zou aan raden om een bestaande dienst af te nemen als identity en access management service. Dit kan een 3rd Party SaaS dienst zijn zoals Auth0, Okta of Ping maar het kan ook een service zijn die beschikbaar is in Azure zoals Azure B2C of Azure Active Directory. Veel organisaties hebben in het verleden ook gebruik gemaakt van IdentityServer als OpenId Connect Server die zelf te hosten en uit te breiden is.
Autorisatie policies
Het is inmiddels vrij standaard om voor publieke API’s een autorisatie model te implementeren. Dit doen we vaak door bepaalde operaties al dan niet beschikbaar te maken voor de ingelogde gebruiker op basis van de gegevens in bijvoorbeeld het JWT-token. Een van de zero trust principes schrijft echter voor dat het autorisatie model niet hard coded maar dynamisch moet zijn. Dynamisch is een vrij ambigu begrip maar wat het in ieder geval betekent is dat het autorisatie model niet in de code van individuele services/API’s moet liggen. De vuistregel die ik vaak hanteer is dat het toekennen van rollen aan gebruikers of services configurabel moet zijn en zonder redeploy van services geeffectueerd moet kunnen worden.
Veel identity en access managementdiensten zoals Auth0, hebben functionaliteit beschikbaar om rollen en permissies te definiëren. De makers van IdentityServer hebben hiervoor ook een product ontwikkelt genaamd PolicyServer. In alle gevallen is het mogelijk om een boom van policies inclusief rollen en permissies (autorisaties) vast te leggen. Om het iets concreter te maken heb ik 2 voorbeelden geselecteerd.
Voorbeeld Auth0
Rollen in Auth0 worden gekoppeld aan permissies die vanuit de API’s worden gedefinieerd. Een rol geeft dus effectief toegang tot een set van permissies welke in feite de rechten zijn voor toegang tot de specifieke API’s. Gebruikers kunnen bepaalde rollen toegewezen krijgen.
Voorbeeld PolicyServer
PolicyServer van IdentityServer kent het concept Policies welke Rollen bevatten. Rollen bevatten weer Permissies. Het model verschilt iets met Auth0 doordat het een Policies entiteit bevat die weer child Policies onderkent. Een policy is eigenlijk een verzameling van rollen die voor een bepaalde applicatie of service benodigd zijn.
User role constraint
Het complexe gedeelte van een goede RBAC-implementatie zit hem vaak in de user/subject role constraint, in andere woorden: hoe zorg je ervoor dat een gebruiker of service de juiste rollen en rechten toebedeeld krijgt? Auth0 en PolicyServer hebben hierin een verschillende implementatie die op subdelen wel overeenkomt.
Belangrijk is, is dat je de ingelogde identity kunt koppelen aan rollen. In Auth0 kan je dit doen door simpelweg de gebruiker te koppelen aan rollen of de applicatie te koppelen aan permissies. Je kunt dit ook meer dynamisch maken door “Auth Pipelines” te gebruiken en een rule te ontwikkelen in JavaScript die op basis van bijvoorbeeld Active Directory Groepen bepaalde rollen toekent aan een gebruiker. Het mooie aan Auth0 is dat het extensible is doormiddel van code snippets toe te voegen aan de authenticatie pipeline.
function addSalesEmployeeFromAzureActiveDirectory(user, context, callback) { var salesGroup = 'SalesEmployees'; if (user.groups) { if (typeof user.groups === 'string') { user.groups = [user.groups]; } var userIsSalesEmployee = user.groups.some(function (group) { return salesGroup === group; }); if (userIsSalesEmployee) { context.idToken['https://example.com/roles'] = ["SalesEmployee"]; } } callback(null, user, context); }
Bij PolicyServer ligt het iets anders, een identity maakt aanspraak op een rol als het voldoet aan 1 van de 3 opties, namelijk: User ID assignment, Identity Role assignment en Claims Evaluation assignment. Daarmee is het in PolicyServer mogelijk om individuele users rechten te geven op bepaalde rollen, maar je kunt ook een groep van users rechten geven door in het Authenticatie JWT token een IdentityRole mee te geven in de claim: Role. Je zult dan wel in de identity server oplossing ervoor moeten zorgen dat je gebruikers een role claim kunt geven op basis van bijvoorbeeld Azure Active Directory groepen of andere logica. De laatste optie is om een Claims Evaluation assignment toe te voegen waarin je simpelweg een C# expression schrijft waarmee je een custom validatie kunt uitvoeren. Zoals onderstaand voorbeeld beschrijft:
user => user.HasClaim(claim => claim.Type == "department" && claim.Value == "sales")
Evaluatie van Authenticatie en Autorisatie in API’s
De laatste stap is uiteraard de evaluatie van de identity en de autorisatie van de API of service die wordt aangeroepen. Het grote verschil tussen Auth0 en PolicyServer zit hem in het feit dat bij Auth0 het JWT-token verrijkt is met claims die in het autorisatie model zijn geconfigureerd. In het geval van PolicyServer wordt runtime geëvalueerd bij de PolicyServer. Beide oplossing hebben z’n eigen voor- en nadelen, zo is Auth0 waarschijnlijk iets performanter, maar duurt een aanpassing in het autorisatie model wel net zolang als de lifetime van het JWT token.
Voorbeeld implementatie van Auth0
Allereerst moet uiteraard de Auth0 authenticatie provider toegevoegd worden aan de ASP.net core middleware. Vervolgens wordt het ASP.net Autorisatie model van de API gelinkt aan het autorisatie model van Auth0 welke meekomt met specifieke claims in het JWT-token.
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddAuthentication(options => { options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme; }).AddJwtBearer(options => { options.Authority = $"https://{Configuration["Auth0:Domain"]}/"; options.Audience = Configuration["Auth0:Audience"]; }); services.AddAuthorization(options => { options.AddPolicy("WriteAccess", policy => policy.RequireClaim("permissions", "create:invoice", "update:invoice")); options.AddPolicy("DeleteAccess", policy => policy.RequireClaim("permissions", "delete:invoice")); }); } }
In de controller of action kan nu afgedwongen worden dat de geauthentiseerde identity de juiste permissies heeft om de API te benaderen.
[ApiController] [Route("api/[controller]")] public class InvoiceController : ControllerBase { [HttpPost] [Authorize(Policy = "WriteAccess")] public ActionResult PostInvoice(Invoice invoice) { } }
Voorbeeld implementatie van PolicyServer
Net zoals bij Auth0 moet uiteraard eerst authenticatie provider toegevoegd worden aan de ASP.net core middleware. Vervolgens wordt het autorisatie model van ASP.net middels de Nuget package: “PolicyServer.Runtime.Client.AspNetCore” gekoppeld aan de PolicyServer. Daarbij wordt een Base Policy verwacht waartegen de ingelogde identity geëvalueerd zal worden.
public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { // base-address of your identityserver options.Authority = "https://demo.identityserver.io"; // name of the API resource options.Audience = "api1"; }); services.AddPolicyServerRuntimeClient(Configuration.GetSection("PolicyServerRuntimeClient")) .AddAuthorizationPermissionPolicies(); } } De AddPolicyServerRuntimeClient verwacht een aantal appsettings, waaronder de base policy. { "PolicyServerRuntimeClient": { "PolicyServerUrl": "https://[policyserverdomain]", "BasePolicy": "BEHEER", "TokenClient": { "Authority": "https://[identityserverdomain]", "ClientId": "policy.runtimeclient" } } }
Net zoals bij Auth0 kan nu in de controller of action afgedwongen worden dat de geauthentiseerde identity de juiste permissies heeft om de API te benaderen.
[ApiController] [Route("api/[controller]")] public class InvoiceController : ControllerBase { [HttpPost] [Authorize("create_invoices")] public ActionResult PostInvoice(Invoice invoice) { } }
Samenvatting
In een zero trust architectuur authentiseren we elke subject of identity in het landschap. Daarbij hanteren we een autorisatie model die we doorgaans vastleggen bij onze centrale identityprovider of daar een product zoals PolicyServer voor aanschaffen of ontwikkelen. Op basis van de geauthentiseerde identiteit kan de API controleren of opvragen of de identiteit wel de juiste permissies heeft om de operatie uit te voeren.
Bio
Dibran Mulder is een Cloud Solution Architect werkzaam bij Cloud Republic. Dibran is co-host van devtalks.nl een podcast voor software developers in Nederland.