Solution SaaS : comment isoler les « Tenants » avec des IAM « policies » générées dynamiquement

solution saaS comment isole Tenants IAM policies

En tant qu’entreprise créatrice de solutions SaaS (« software as a service ») sur AWS, vous utilisez certainement AWS Identity and Access Management (IAM) comme colonne vertébrale de votre stratégie d’isolation de vos ressources et datas (« tenants ») pour vos clients.

IAM vous permet de définir une série de permissions en créant des « policies » et en les attachant à des identités IAM (utilisateurs, groupes d’utilisateurs ou rôles) ou des ressources AWS. Une « policie » est un objet dans AWS qui, lorsqu’il est associé à une identité ou à une ressource, définit les autorisations de ces dernières.

AWS évalue ces « policies » lorsqu’un principal IAM (utilisateur ou rôle) envoie une demande. Les permissions déterminent si la demande est autorisée ou refusée. AWS prend en charge six types de “policies” :

  • basées sur une identité,
  • basées sur les ressources,
  • les limites d’autorisations,
  • les politiques de contrôle de service Organizations,
  • les listes ACL
  • et les politiques de session.

En utilisant ce service, vous êtes nombreux à créer des « policies » distinctes pour chaque client. Mais très vite, cela peut engendrer une explosion voire un “plat de spaghettis” et rendre la situation impossible à maintenir.

Pour éviter cette situation, la solution est de mettre en place une génération dynamique. Cette méthode offre une expérience d’isolation plus évolutive et davantage administrable. Voyons comment la déployer.

RBAC, ABAC, et IAM « policies » générées dynamiquement : quelles différences entre ces méthodes d’isolation ?

Avant toute chose, voici un rappel des trois principales méthodes d’isolation possibles avec IAM.

1. RBAC – Authentification des utilisateurs avec le contrôle d’accès basé sur les rôles : vous attribuez à chaque client un rôle IAM dédié ou un groupe de rôles IAM statiques qu’il utilise pour accéder aux ressources. RBAC est efficace lorsque vous avez un petit nombre de Tenants et des « policies » relativement statiques.

2. ABAC – Contrôle d’accès basé sur les attributs : cette technique convient à un ensemble d’applications SaaS qui connaissent une croissance rapide, sauf si vous devez prendre en charge fréquemment des modifications ou ajouts de rôles. Dans ce cas, les IAM « policies » générées dynamiquement s’imposent.

3. IAM « policies » générées dynamiquement : cette technique crée dynamiquement une IAM « policie » pour un client conformément à l’identité de l’utilisateur. Choisissez cette technique dans des applications hautement dynamiques avec des définitions de rôles changeantes ou fréquemment ajoutées (par exemple, scénario de collaboration de Tenants).

Les fondamentaux de l’isolation avec IAM

Voici comment vous pouvez isoler des Tenants avec IAM à l’aide du Security Token Service (STS).

fondamentaux isolation IAM
  1. Les requêtes entrantes incluent une en-tête d’authentification avec un JSON Web Token (JWT) (1) qui comprend des données identifiant le Tenant actuel. Ce jeton est signé par un fournisseur d’identité, garantissant que le JWT ne peut pas être modifié et que l’identité du Tenant peut être approuvée.
  2. Nous récupérons (2) la « policie » spécifique au Tenant à partir d’IAM et demandons à STS de renvoyer un identifiant (3) défini dans notre gestion des permissions.
  3. Lorsque nous tentons d’accéder à (4) Amazon DynamoDB, notre autorisation (5) d’effectuer l’appel SDK getItem est vérifiée par IAM, autorisant ou refusant l’accès en fonction de la « policie » spécifique au Tenant que nous avons récupérée.

Ce modèle est simple et limite l’accès à des données Amazon DynamoDB spécifiques pour notre Tenant. Cependant, ce modèle nécessite de créer une permission personnalisée pour chaque Tenant du système :

1   {
2      "Effect": "Allow",
3      "Action": [
4            "dynamodb:*"
5       ],
6       "Resource": [
7            "arn:aws:dynamodb:us-west-2:123456789012:table/Employee"
8       ],
9       "Condition": {
10          "ForAllValues:StringEquals": {
11              "dynamodb: LeadingKeys": [ "Tenant1" ]
12          }
13      }
14   }

En ligne 11, nous voyons que l’identifiant du Tenant est codé en dur dans notre permission. Cela présente plusieurs problèmes :

  • Si le nombre de Tenants utilisant notre système augmente très rapidement, le nombre de permissions va devenir ingérable.
  • Lorsque vous publierez de nouvelles fonctionnalités pour votre service, vous devrez modifier vos ressources IAM existantes et mettre à jour les processus d’intégration. Cela créera un couplage étroit entre vos services et votre infrastructure de sécurité, ce qui peut augmenter la complexité de votre processus de déploiement.
  • Cela va limiter également la capacité de votre équipe à se concentrer sur la création de nouvelles fonctionnalités. Au fur et à mesure que votre l’isolation et la sécurité des Tenants va devenir de + en + difficile à maintenir et à tester, vous augmenterez également la possibilité d’introduire une erreur qui pourrait exposer les données des Tenants.
IsolatingSaaSTenants Fig IAMRolesMultiply

Comment mettre en place des IAM « policies » générées dynamiquement ?

Reprenons l’exemple précédent (tentative de restriction d’accès d’une ressource Amazon DynamoDB à un utilisateur). Cette fois, nous ne stockons pas notre « policie » dans IAM. Nous décidons de transformer notre « policie » en un modèle où les références de Tenants statiques sont remplacées par des espaces réservés de modèle.

Les espaces réservés de table et de Tenant dans le modèle suivant peuvent désormais être hydratés avec les valeurs appropriées lors de l’exécution.

1  {
2    "Effect": "Allow",
3    "Action": [
4         "dynamodb:*"
5    ],
6    "Resource": [
7         "arn:aws:dynamodb:*:*:table/{{table}}"
8    ],
9    "Condition": {
10        "ForAllValues:StringEquals": {
11            "dynamodb: LeadingKeys": [ "{{tenant}}" ]
12        }
13    }
14 }
  • En ligne 4, vous remarquerez que l’action de ce modèle a une portée large. Cela nous donne la plus grande flexibilité pour appliquer cette autorisation à une variété de cas d’utilisation.
  • Dans cette stratégie, les ressources, en ligne 7, ne sont pas spécifiques au Tenant, mais notez que certaines « policies » imposent l’isolation des Tenants au niveau des ressources.
  • L’opérateur de condition (en ligne 9) limite notre Tenant à ne voir que les lignes avec une clé qui commence par une valeur d’identifiant de Tenant spécifique (ligne 11).

Assumer un rôle

Dans cet exemple, le rôle doit contenir une permission autorisant l’accès à une ressource Amazon DynamoDB. Elle permet à quiconque d’accéder aux ressources DynamoDB sans aucune limitation spécifique au Tenant.

1 {
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Action": [
6        "dynamodb:GetItem",
7        "dynamodb:BatchGetItem",
8        "dynamodb:Query",
9        "dynamodb:DescribeTable"
10      ],
11      "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/Employee",
12      "Effect": "Allow"
13    }
14  ]
15 }

Maintenant, examinez la permission générée dynamiquement à l’aide du Security Token Service (STS).

1 {
2  "Version": "2012-10-17",
3  "Statement": [
4    {
5      "Action": [
6        "dynamodb:GetItem",
7        "dynamodb:BatchGetItem",
8        "dynamodb:Query",
9        "dynamodb:DescribeTable"
10      ],
11      "Resource": "arn:aws:dynamodb:us-west-2:123456789012:table/Employee",
12      "Effect": "Allow",
13      "Condition": {
14        "ForAllValues:StringEquals": {
15          "dynamodb: LeadingKeys": [ "tenant1" ]
16        }
17      }
18    }
19  ]
20 }

Du point de vue du code, assumer un rôle avec STS est simple. Voici une version abrégée du code qui assume un nouveau rôle.

1  AssumeRoleResponse response = sts.assumeRole (ar -> ar
2       .webIdentityToken(openIdToken)
3       .policy(scopedPolicy)
4       .roleArn(role));
5  Credentials tenantCredentials = response.credentials();
6  DynamoDbClient dynamoDbClient = DynamoDbClient.builder()
7       .credentialsProvider(tenantCredentials)
8       .build();

Lorsque nous appelons STS assumeRole (ligne 1), nous transmettons essentiellement notre « policie » générée dynamiquement (ligne 3) et notre rôle (ligne 4). Le résultat de cet appel à STS est une information d’identification, qui a été réduite aux seules autorisations.

Les clients du service SDK tels que DynamoDbClient (ligne 5) acceptent les informations d’identification (ligne 6). Tous les appels passés avec ce client bénéficient désormais de nos autorisations étendues.

Présentation d’un distributeur automatique de jetons (JSON Web Token)

Voyons maintenant comment simplifier la génération dynamique de policies en introduisant un distributeur automatique de jetons.

IsolatingSaaSTenants Fig TokenVendingMachine

Dans ce cas, le code d’application appelle le distributeur automatique de jetons et reçoit un jeton. Celui-ci contient les conditions de sécurité requises pour le Tenant.

  • Nos en-têtes HTTP entrants incluent une en-tête d’authentification avec un jeton porteur, qui dans cet exemple est un JSON Web Token (JWT) qui contient notre identité de Tenant (1).
  • Le gestionnaire JWT (2) vérifie le jeton et extrait le Tenant des revendications.
  • Ensuite, nous injectons le Tenant (3) et toutes autres variables nécessaires dans les modèles d’autorisation (4).
  • Après avoir chargé les modèles à partir du fichier, les rôles sont hydratés avec les variables que nous avons transmises. Le résultat est une « policie » entièrement formée et générée dynamiquement. Cette stratégie dynamique remplace les nombreuses permissions statiques spécifiques au Tenant dans notre exemple initial.
  • Ensuite, notre « policie » est utilisée pour assumer un rôle (6), qui remplace les rôles spécifiques au Tenant dont nous avions précédemment besoin.
  • Enfin, le distributeur automatique de jetons renvoie (7, 8) le jeton nouvellement créé, ou les informations d’identification, à notre développeur.

Voici le code qui pilote cet exemple :

1  TokenVendingMachine tokenVendorMachine = new TokenVendingMachine();
2  AwsCredentialsProvider tenantCredentials = tokenVendingMachine
    .vendToken(jwtToken);

Les informations d’identification envoyées par cet appel peuvent être utilisées pour accéder à d’autres ressources. Votre code, par exemple, peut utiliser ces informations d’identification pour accéder à Amazon Simple Storage Service (Amazon S3) :

1 S3Client s3Client = S3Client.builder()
2             .credentialsProvider(tenantCredentials)
3             .build();

Modèles d’autorisation

Le cœur du distributeur automatique de jetons est un ensemble de fichiers de modèles d’autorisation. Voyons maintenant comment gérer ces fichiers modèles, leur permettant d’évoluer indépendamment de notre code.

Exemple : comment créer une permission Amazon S3 pour restreindre l’accès au niveau d’un dossier ?

Voici comment accéder à la ListBuckets action (4) pour les Tenants dont le préfixe correspond à l’identifiant du Tenant. Cela limite la capacité d’un Tenant à interagir avec des objets dans des dossiers appartenant à d’autres Tenants.

1  {
2   "Effect": "Allow",
3   "Action": [
4     "s3:ListBucket"
5   ],
6   "Resource": [
7     "arn:aws:s3:::{{bucket}}"
8   ],
9   "Condition": {
10    "StringLike": {
11      "s3:prefix": [
12        "{{tenant}}",
13        "{{tenant}}/",
14        "{{tenant}}/*"
15      ]
16    }
17  }
18 },
19 {
20  "Effect": "Allow",
21  "Action": [
22    "s3:GetObject",
23    "s3:PutObject",
24    "s3:DeleteObject"
25   ],
26   "Resource": [
27        "arn:aws:s3:::{{bucket}}/{{tenant}}/*"
28   ]
29 }

Les modèles sont conservés séparément de votre code, et sont déployés et versionnés indépendamment.

Étant donné que les modèles ne sont que des fichiers JSON, il peut être préférable de les considérer comme faisant partie de votre infrastructure, comme un processus de déploiement de code. Leur déploiement sur un système de fichiers tel qu’Amazon Elastic File System (Amazon EFS) ou Amazon S3, tous deux accessibles sur l’ensemble de votre architecture, s’intégrerait bien dans une architecture de microservices.

Génération d’une permission à partir de modèles

Maintenant que nos permission sont définies en dehors d’IAM, nous pouvons introduire du code qui charge nos modèles d’autorisation dans des instructions et les ajoute à des « policies » à portée dynamique lors de l’exécution. Voyons comment nous hydratons les modèles d’autorisation pour créer des permission.

Voici une classe Java simple appelée PolicyGenerator, responsable de la création d’une permission :

1 String scopedPolicy = PolicyGenerator.generator()
2                .s3FolderPerTenant(bucket)
3                .dynamoLeadingKey(tableName)
4                .tenant(tenantIdentifier)
5                .generatePolicy();

L’objectif est de permettre au développeur d’ajouter aussi facilement que possible des autorisations de sécurité correctement formées et valides. Ce processus nécessite l’accès à l’identifiant du Tenant qui a été extrait du JWT.

Chaque méthode d’autorisation que nous ajoutons prend également comme paramètres les valeurs requises nécessaires pour hydrater ce modèle spécifique. Le distributeur automatique de jetons dans notre exemple est configuré pour ajouter les autorisations dont le microservice a besoin et localiser les valeurs dont il a besoin à partir de variables environnementales.

Conclusion

La génération de « policies » dynamiques peut aider les fournisseurs SaaS à mettre en œuvre l’isolation des Tenants. L’exemple d’implémentation d’un distributeur automatique de jetons fournit un mécanisme administrable pour implémenter cette génération dynamique.

Lorsque vous implémentez votre propre distributeur automatique de jetons, gardez à l’esprit quelques points :
Les modèles d’autorisation doivent fournir une séparation de vos permissions d’isolement, leur permettant d’évoluer indépendamment de vos applications.
Les « policies » doivent permettre le principe du moindre privilège, limitant l’accès de vos Tenants aux services et aux données aussi étroitement que possible.
L’identité du Tenant doit être résolue de manière cohérente et vérifiable dans votre solution.
Votre solution doit être encapsulée et réutilisable dans tous vos services SaaS, en renvoyant des informations d’identification à l’échelle du Tenant.

Pour plus d’informations sur les exemples de cet article, consultez le référentiel AWS SaaS Factory GitHub : https://github.com/aws-samples/aws-saas-factory-dynamic-policy-generation

Il contient un exemple d’application et des ressources AWS CloudFormation pour provisionner automatiquement l’infrastructure nécessaire dans votre compte AWS.

Sources utilisées pour la rédaction de cet article :

  • https://aws.amazon.com/fr/blogs/apn/isolating-saas-tenants-with-dynamically-generated-iam-policies/
  • https://github.com/aws-samples/aws-saas-factory-dynamic-policy-generation
  • https://www.infracom.com.sg/how-exactly-to-implement-saas-tenant-isolation-with-aws-and-abac-iam/
  • https://www.premaccess.com/quest-ce-que-le-multi-tenant-aws-cloud-application-architecture/
  • https://aws.amazon.com/fr/partners/programs/saas-factory/
  • https://docs.aws.amazon.com/fr_fr/IAM/latest/UserGuide/access_policies.html