Les IAM Permissions Boundaries AWS par la pratique
<span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span><span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span><span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span><span style="display: inline-block; width: 0px; overflow: hidden; line-height: 0;" data-mce-type="bookmark" class="mce_SELRES_start"></span><!-- pre { font-size: 80%; } pre.commandline, span.commandline { font-family: monospace; background-color: #020202; color: #b3c2d2; overflow: auto;width: 98%; padding: 2px 6px;} -->
Dans le premier article de cette série, nous avons expliqué les possibilités des Permissions Boundaries AWS, leur mécanisme et leur cas d'usage. Dans cet article, nous vous proposons de les mettre en pratique avec un exemple concret.
Tester n'est pas douter
Nous allons utiliser Terraform pour déployer un utilisateur "délégué" pouvant uniquement créer des rôles avec boundaries, et montrer que celles-ci l'empêchent bien de sortir des droits que nous lui avons fournis.
Notre utilisateur a pour tâche de déployer des applications (machines virtuelles EC2), utilisant chacune une table DynamoDB différente. Chaque VM ne doit pouvoir accéder qu'à sa table :
[caption id="attachment_77025" align="aligncenter" width="683"]
Figure 1 - scénario d'exemple
[/caption]
L'utilisateur devra donc créer un rôle par VM, pour la contraindre à n'accéder qu'à sa table.
L'exemple sera développé en deux topologies Terraform :
- Une première, exécutée par un utilisateur administrateur : elle va créer notre utilisateur et lui donner les droits nécessaires pour déployer VMs, rôles et tables DynamoDB.
- Une deuxième, à exécuter avec l'utilisateur créé précédemment : elle va créer une VM, une table DynamoDB et le rôle pour y accéder.
Pour suivre cet exemple, vous aurez besoin :
- D'un utilisateur administrateur sur un compte AWS.
- D'une version récente de Terraform et de son provider AWS.
Des connaissances de base sur Terraform vous seront utiles pour suivre l'exemple, tout comme une familiarité avec le modèle de droits d'AWS. Vous trouverez également le code complet de l'exemple sur Github, pour référence.Avertissements d'usage:
- Ceci n'est qu'un exemple simplifié. Ne le déployez pas en production tel quel sans une analyse de sécurité complète.
- L'exécution de cet exemple peut induire de la facturation sur votre compte AWS.
Création de la Permissions Boundary et de l'utilisateur
Nous allons commencer par créer la policy de Permissions Boundary pour notre utilisateur. Créez une première topologie Terraform et ajoutez le code suivant :
# 1_create_permission_bound_user/main.tf
provider "aws" { version = "~> 1.36.0" }
resource "aws_iam_policy" "permission_boundary_for_delegated_user" { name = "permission_boundary_for_demo_delegated_user"
policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": [ "dynamodb:*" ], "Effect": "Allow", "Resource": "*" } ] } EOF }
La Permissions Boundary est une simple Policy IAM, listant des droits AWS : ici elle autorise tous les droits DynamoDB.
Appliquez la topologie avec votre utilisateur administrateur :
$ cd 1_create_permission_bound_user $ terraform init $ terraform apply [...]
Plan: 1 to add, 0 to change, 0 to destroy.
Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve.
Enter a value: yes
[...]
Apply complete! Resources: 1 added, 0 changed, 0 destroyed.
Vous pouvez vérifier que cette Policy a bien été créée dans la GUI AWS.
Ajoutez maintenant à la topologie la déclaration de l'utilisateur délégué et de ses droits :
# 1_create_permission_bound_user/main.tf
resource "aws_iam_user" "delegated_user" { name = "demo_delegated_user" force_destroy = true }
data "aws_caller_identity" "current" {}
resource "aws_iam_user_policy" "permissions_for_delegated_user" { name = "permissions_for_delegated_user"
user = "${aws_iam_user.delegated_user.name}"
policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": [ "dynamodb:*", "ec2:*",
"iam:CreateInstanceProfile",
"iam:DeleteInstanceProfile",
"iam:GetInstanceProfile",
"iam:AddRoleToInstanceProfile",
"iam:RemoveRoleFromInstanceProfile",
"iam:ListInstanceProfilesForRole"
\],
"Effect": "Allow",
"Resource": "\*"
},
{
"Effect": "Allow",
"Action": \[
"iam:CreateRole",
"iam:PutRolePolicy",
"iam:DeleteRolePolicy"
\],
"Resource": "arn:aws:iam::${data.aws\_caller\_identity.current.account\_id}:**role/delegated-\***",
**"Condition": {
"StringEquals": {
"iam:PermissionsBoundary": "${aws\_iam\_policy.permission\_boundary\_for\_delegated\_user.arn}"
}
}** },
{
"Action": \[
"iam:GetRole",
"iam:DeleteRole",
"iam:GetRolePolicy",
"iam:PassRole"
\],
"Effect": "Allow",
"Resource": "arn:aws:iam::${data.aws\_caller\_identity.current.account\_id}:**role/delegated-\***"
}
\]
} EOF }
La première partie des droits est assez standard : l'utilisateur pourra manipuler les tables DynamoDB et les VMs EC2. Nous lui donnons également le droit de créer et manipuler les InstanceProfile faisant le lien entre rôle et VM. La deuxième partie est le coeur de notre sujet : l'utilisateur pourra créer des rôles (Create) et leurs droits (Put/DeleteRolePolicy), mais uniquement si le rôle porte la Permissions Boundary définie plus tôt dans la topologie.
Remarquez également que nous obligeons l'utilisateur à utiliser un préfixe sur les noms de ses rôles, en l'occurrence «delegated-». Cela sert en partie à les identifier facilement, mais est surtout nécessaire pour faire le lien avec le reste de l'IAM AWS. En effet, de nombreux droits ne gèrent pas la condition sur les Permissions Boundaries. C'est le cas des droits "GetRole", "PassRole"^<a id="ref1" href="#note1">1</a>^ et autres listés dans la troisième section, qui ne gèrent que les restrictions sur le nom, mais sont critiques pour manipuler les rôles. Nous limitons donc leur utilisation aux rôles préfixés par «delegated-».
Appliquez à nouveau la topologie pour créer l'utilisateur. Vous pouvez également vérifier qu'il a été créé avec cette policy dans la GUI AWS.
Nous avons fini avec la section "administrateur" de notre exemple. Passons maintenant côté utilisateur pour créer une application.
Création d'une VM et de sa table DynamoDB
Créez une deuxième topologie, et commencez par y insérer un bloc provider avec les access/secret keys de votre utilisateur fraîchement créé:
# 2_vm_with_permission_boundary/main.tf
provider "aws" { version = "~> 1.36.0" access_key = "put the access key of 'demo_delegated_user' here" secret_key = "put the secret key of 'demo_delegated_user' here" }
Pensez bien à générer une access/secret key pour demo_delegated_user plutôt que de reprendre ceux de votre utilisateur administrateur.
Ajoutez à la topologie la création d'une table DynamoDB d'exemple :
resource "aws_dynamodb_table" "app1_dynamodb_table" { name = "app1_table" read_capacity = 1 write_capacity = 1 hash_key = "Id"
attribute { name = "Id" type = "N" } }
resource "aws_dynamodb_table_item" "sample_data" { table_name = "${aws_dynamodb_table.app1_dynamodb_table.name}" hash_key = "${aws_dynamodb_table.app1_dynamodb_table.hash_key}"
item = <<EOF { "Id": { "N": "1" }, "Description": { "S": "A sample item" } } EOF }
Rien de bien novateur pour l'instant, cette table nous servira juste de cible pour tester les accès de la future VM. Appliquez déjà la topologie en l'état pour tester les access/secret keys de demo_delegated_user.
Revenons à plus intéressant : la création du rôle de la VM et de la policy décrivant ses droits. Ajoutez les objets correspondants à la topologie :
data "aws_caller_identity" "current" {}
resource "aws_iam_role" "delegated_vm_role" { name = "**delegated-**demo_vm_role" permissions_boundary = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:policy/permission_boundary_for_demo_delegated_user"
assume_role_policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": "sts:AssumeRole", "Principal": { "Service": "ec2.amazonaws.com" }, "Effect": "Allow" } ] } EOF }
resource "aws_iam_role_policy" "delegated_vm_role_policy" { name = "**delegated-**demo_vm_role_policy" role = "${aws_iam_role.delegated_vm_role.name}"
policy = <<EOF { "Version": "2012-10-17", "Statement": [ { "Action": [ "dynamodb:*" ],
"Resource": "${aws\_dynamodb\_table.app1\_dynamodb\_table.arn}",
"Effect": "Allow"
}
] } EOF }
C'est une création de rôle et policy standard en Terraform, si ce n'est que le rôle référence la Permissions Boundary créée plus tôt, et utilise le préfixe «delegated-». Vous pouvez essayer (et échouer) de créer le rôle sans ces éléments si vous voulez vérifier que les contraintes définies sur demo_delegated_user s'appliquent bien et qu'on ne vous ment pas :). Sinon, appliquez à nouveau la topologie.
Il ne vous reste plus qu'à créer la VM et ses ressources associées (instance profile, security group, AMI). Le code Terraform est un peu long, mais 100% standard:
resource "aws_iam_instance_profile" "delegated_vm_instance_profile" { name = "delegated-demo_vm_instance_profile" role = "${aws_iam_role.delegated_vm_role.name}" }
data "aws_ami" "ubuntu_16-04" { most_recent = true
filter { name = "owner-alias" values = ["amazon"] }
filter { name = "name" values = ["amzn2-ami-hvm-2.0.*-x86_64-gp2"] } }
resource "aws_security_group" "demo_vm_sg" { name = "demo_vm_sg" }
resource "aws_security_group_rule" "allow_outbound" { type = "egress"
from_port = 0 to_port = 65535 protocol = "all"
security_group_id = "${aws_security_group.demo_vm_sg.id}" cidr_blocks = ["0.0.0.0/0"] }
resource "aws_security_group_rule" "allow_ssh" { type = "ingress"
from_port = 22 to_port = 22 protocol = "tcp"
security_group_id = "${aws_security_group.demo_vm_sg.id}" cidr_blocks = ["0.0.0.0/0"] }
resource "aws_instance" "demo_vm" { instance_type = "t2.micro" ami = "${data.aws_ami.ubuntu_16-04.id}"
key_name = "PUT YOUR SSH KEYPAIR NAME HERE"
iam_instance_profile = "${aws_iam_instance_profile.delegated_vm_instance_profile.name}"
tags { Name = "permissions_boundary_demo_vm" }
vpc_security_group_ids = ["${aws_security_group.demo_vm_sg.id}"] }
output "vm_ip" { value = "${aws_instance.demo_vm.public_ip}" }
N'oubliez pas de renseigner une keypair AWS à laquelle vous avez accès dans le champ "key_name", puis appliquez la topologie.
$ terraform apply
[...]
Do you want to perform these actions? Terraform will perform the actions described above. Only 'yes' will be accepted to approve.
Enter a value: yes
[...]
Apply complete! Resources: 10 added, 0 changed, 0 destroyed.
Outputs:
vm_ip = IP publique de votre VM
Pour finir, il ne vous reste plus qu'à vous logger sur votre VM fraîchement créée et tester l'accès à DynamoDB:
$ ssh ec2-user@IP-de-votre-VM ssh $ aws dynamodb scan --region your-current-region --table-name app1_table { "Count": 1, "Items": [ { "Id": { "N": "1" }, "Description": { "S": "A sample item" } } ], "ScannedCount": 1, "ConsumedCapacity": null }
Vous pouvez jouer avec l'exemple pour tester les possibilités des Boundaries :
- Dupliquez la seconde topologie en changeant les noms de ressources pour créer une deuxième table & VM avec l'utilisateur délégué : l'utilisateur peut créer autant de rôles qu'il veut, tant qu'ils respectent la Boundary.
- Ajoutez des droits au rôle de la VM : par exemple des droits S3. L'ajout de droits va réussir, mais la Boundary ne contient pas ces droits, et ils ne seront pas utilisables dans la VM.
- Ajoutez ensuite les droits S3 à la Boundary, et voir que la VM y a désormais accès.
- Avec demo_delegated_user, essayez de créer un rôle sans la boundary, ou avec le mauvais préfixe…
La Permissions Boundary tient bien ses promesses : permettre à l'utilisateur de créer ses propres rôles, tout en l'empêchant de s'en servir pour obtenir des droits qui ne lui étaient pas accordés.
Conclusion
Nous espérons que cet exemple vous aura permis d'entrevoir les possibilités des Permissions Boundaries. Leur mise en place est somme toute assez simple ! Gardez cependant en tête que ceci était un exemple volontairement simplifié pour démonstration : la mise en place des Boundaries doit être adaptés au contexte de l'entreprise, et devra nécessairement s'accompagner de protections et bonnes pratiques adaptées vue la complexité engendrée.
Malgré cette complexité, cet ajout à l'IAM AWS répond à un besoin concret dans de nombreux cas de délégation, qu'on ne pouvait traiter que de manière bancale jusqu'à présent. Pour ces cas d'usage, les Permissions Boundaries offrent une solution propre et efficace, qui mérite amplement de subir la complexité engendrée.
Notes
^<a id="note1" href="#ref1">1</a>^: Qui permet d'associer un rôle à une VM, et est donc crucial pour limiter les actions de notre utilisateur délégué.