Dans un précédent billet vous avez découvert ou redécouvert Cloud Custodian, un outil de conformité qui permet d’écrire des policies en yaml pour identifier des ressources ou des évènements sur votre environnement. Dans cet article nous allons voir qu’en nous appuyant sur le RBAC (Role-Based Access Control) des cloud providers et sur Cloud Custodian, nous pouvons facilement mettre en place un framework de sécurité adaptable à vos problématiques SSI.

Le modèle Zero-Trust

Mettant en place des solutions de sécurité dans le cloud, nous avons tous vu les principes de Zero-Trust.

  • La segmentation : d’un point de vue réseau, il est indispensable de mettre en place des verrous de communication inter-applications n’autorisant que le trafic strictement nécessaire.
  • Le contrôle d’accès : du côté des droits (utilisateurs et applications confondus) le least-privilege est roi, avec des accès temporaires et authentification sur requête d’accès par multi-facteurs. L'identité est l’unité.
  • La détection des menaces : le tout enrobé d’une solution d’exploration des logs (nécessairement inaltérables) d’accès et d’actions pour détecter d’éventuels comportements suspects pour effectuer une réponse adaptée.

Ces concepts ont des implémentations bien connues et éprouvées, beaucoup d’entreprises optent pour des solutions clés en main comme PrismaCloud de PaloAlto ou Dome9 de CheckPoint tandis que d’autres préfèrent l’implémentation d’une solution “maison” plutôt que de déléguer ces problématiques. Chaque vision se vaut, il faut néanmoins être conscient des coûts importants de chaque solution, que ce soit en termes de licence pour les solutions SaaS ou d’opérabilité et de complexité de développement pour les solutions maison.

Notez que bien que la sécurité dans le cloud doive faire partie de vos priorités, il est néanmoins nécessaire de l’adapter pour pouvoir adopter une approche cloud solide utilisable et pérenne.

L’analyse à conduire, quand on souhaite adopter ce modèle de sécurité dans le cloud, n’est pas la même en fonction de l’étape d’adoption et toutes les parties prenantes de l’IT doivent obligatoirement y participer pour établir une gouvernance claire.

Le trust oui ! Mais le contrôle d’abord

La matrice de responsabilité d’AWS nous apprend deux choses : la première est qu’une partie des risques liés à la sécurité des infrastructures n’est plus vraiment un problème (en tous cas, plus le problème du consommateur de services cloud), la seconde est que nous nous devons d’être d’autant plus vigilants quant à la sécurité de ce que nous consommons dans le cloud. S’il est simple de provisionner des VMs ou d’utiliser des services managés, les configurer pour qu’ils soient sécurisés est autrement plus complexe tant les possibilités de configuration sont larges.

La première dimension que l’on retient est celle du RBAC (Role-Based Access Control) : un utilisateur physique, un service managé ou un service applicatif n’utilisent que des rôles qui sont un ensemble de politiques d’accès. L’union logique de ces politiques permet de définir des droits fins sur l’utilisation de services cloud autorisés pour une identité. Ce paradigme est intéressant pour plusieurs raisons que nous avons déjà exposées ici.

Dans AWS, il nous est permis d’écrire des politiques d’accès en allowed-list (Allow-statement) ou en block-list (Deny-statement).

Toute policy dans AWS se présente de la façon suivante :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "StatementAllow",
            "Effect": "Allow", #Effect block (can be Allow or Deny)
            "Action": "sts:AssumeRole", #Action block (can also be a list of string)
            "Resource": "arn:aws:iam::*:role/userrole" #Resource block (can also be a list of string)
        },
        {
            "Sid": "StatementDeny",
            "Effect": "Deny",
            "Action": "sts:AssumeRole",
            "NotResource": "arn:aws:iam::*:role/userrole"
        }
   ]
}

Ici, les deux statements semblent avoir le même effet mais ce n’est pas le cas : le premier donne une autorisation, le second empêche d’y déroger, peu importe les statement présents dans une autre policy, l’action sts:AssumeRole ne sera possible que sur la ressource userrole car un statement Deny prendra toujours le pas sur un statement Allow. L’idée est aussi de montrer la possibilité de faire de la double négation (Deny-NotResource ou Deny-NotAction), cela fait gagner en flexibilité dans la déclaration des droits en sacrifiant un peu de la facilité de compréhension.

En complément, il est possible d’appliquer des conditions supplémentaires à l’obtention d’un droit :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Action": "lambda:*",
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "aws:RequestedRegion": [
                        "eu-west-1"
                    ]
                }
            }
        }
    ]
}

Dans cet exemple, on autorise toute action sur l’API AWS Lambda à la condition qu’elle soit effectuée dans la région ‘eu-west-1’. Il existe plusieurs conditions globales qui sont disponibles pour chaque service et des conditions par service permettant un filtrage encore plus fin des droits.

Partant de cet écosystème, nous avons déjà de quoi répondre à une bonne partie des exigences du contrôle d’accès.

Mais ce n’est pas fini ! AWS nous permet encore d’aller plus loin grâce aux Permissions Boundaries, il s’agit d’une policy utilisée comme limitation. Prenons l’exemple d’un rôle ayant des accès Administrateur (apposé par des professionnels, ne pas reproduire chez vous) et appliquons la policy définie précédemment. Le schéma de résolution des droits est le suivant :

Dans notre cas, le rôle Administrateur sera alors limité à l’utilisation du service AWS Lambda sur la région ‘eu-west-1’ malgré sa policy AdministratorAccess.

Revenons-en au principe de Control énoncé plus haut, ce mécanisme nous permet de répondre facilement à la problématique de l’élévation de privilège : comment empêcher un utilisateur de s’attribuer des droits supplémentaires, ou à un service donné par lequel il pourrait effectuer des actions illégitimes ? Le tout sans l'empêcher complètement d’utiliser les actions IAM bien sûr.

Prenons un rôle PowerUser ayant les droits iam:*. Ajoutons-lui la permission boundary suivante nommée PU-Boundary :

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Action": [
                "iam:CreateRole",
                "iam:AttachRolePolicy",
                "iam:DetachRolePolicy",
                "iam:UpdateAssumeRolePolicy"
            ],
            "Resource": "*",
            "Condition": {
                "StringEquals": {
                    "iam:PermissionBoundary": [
                        "arn:aws:iam::*:policy/PU-Boundary"
                    ]
                }
            }
        },
        {
            "Sid": "2",
            "Effect": "Allow",
            "Action": [
                "iam:CreatePolicy",
                "iam:DeletePolicy"
            ],
            "Resource": "*"
        }
    ]
}

Via la condition en première déclaration, l’utilisation de l’API IAM est restreinte aux rôles ayant cette même policy en Permission Boundary. L’utilisateur soumis à cette policy ne peut donc pas faire d’escalade de privilège, la boucle est bouclée.

Ceci étant dit, prenons un peu de hauteur : une policy peut aussi être appliquée Accounts-wide dans une optique d’Organisation. AWS Organization et Service Control Policies (SCP) permettent d’appliquer des policies (Deny-Only) à un compte ou un ensemble de comptes dans le cadre d’une Organization Unit (OU). Ces policies s’appliquent selon un arbre d’héritage, une SCP appliquée à une OU sera appliquée sur tous les comptes ainsi que toutes les OUs dépendantes de la première.

La résolution des SCP se fait en adéquation avec le RBAC et les Permission Boundaries selon le modèle suivant :




Tous ces mécanismes permettent de contrôler les accès et les utilisations du cloud AWS. En forçant des politiques strictes pour les utilisateurs et les services applicatifs, on balaye une partie des risques opérationnels induits par l’utilisation d’un cloud public.

Néanmoins, plusieurs problèmes se posent :

  1. le nombre de caractères dans une policy est restreint
  2. le nombre de polices applicables sur un rôle est restreint
  3. on ne peut appliquer qu’une seule permission boundary sur un rôle
  4. le nombre de SCP / OU / Account au niveau organisation est lui aussi, restreint
  5. les conditions sur les API IAM sont incomplètes vis-à-vis des options de configuration de chaque service

Quid des risques résiduels ? Nous voyons qu’avec ce modèle, nous pouvons faire le Trust ainsi qu’une partie du Control. Pour couvrir ces risques, allons chercher du côté d’une librairie open source que nous connaissons déjà bien.

Et Cloud Custodian dans tout ça ?

Avec ses différents modes de déclenchement, Cloud Custodian (C7N) est une excellente réponse à la couverture des risques résiduels engendrés par les manquements dans l’IAM AWS. Au travers d’une policy yaml, vous pouvez scanner toutes les ressources d’un type particulier ou réagir à un évènement dans cloudtrail via une fonction AWS Lambda déployée par C7N.

Mettons tout cela ensemble dans un mini wrapper pour l’exemple :

Vous trouverez toutes les sources de code et le projet complet ici.

Nous allons utiliser ce framework pour sécuriser une partie des services S3 et EC2 (plus spécifiquement l’activation forcée du chiffrement par défaut dans EBS).

Ce que nous cherchons à faire :

  • nous assurer que tous les buckets S3 présents sur le compte AWS soient chiffrés côté serveur et versionnés
  • sur toutes les régions utilisées, le paramètre “chiffrement par défaut des volumes EBS” doit être activé
  • recevoir une notification si une entité désactive ce dernier paramètre

Pour ce faire, nous nous reposerons sur un script Python embarquant d’une part le déploiement d’une landing zone Terraform et d'autre part le déploiement des policies Cloud Custodian.

La partie Terraform porte les éléments de rôle et la permission boundary nécessaires à la première partie du Trust and Control. Ce layer embarque aussi le rôle IAM que vont utiliser les Lambda Cloud Custodian pour s'exécuter.

Les policies C7N déployant les fonctions Lambda embarquant les contrôles seront, elles, dynamiquement déployées dans plusieurs régions avec ce wrapper pour donner l’architecture suivante :

L’idée est donc d’avoir la policy de normalisation des buckets S3 suivante :

policies:
  - name: S3-force-bucket-versioning
    description: This rule will force Bucket versioning
    resource: s3
    mode:
      type: periodic
      function-prefix: SEC-c7n-
      schedule: rate(2 minutes)
      role: arn:aws:iam::{account_id}:role/SECURITY/SEC-CustodianCore-global
    filters:
      - or:
        - type: value
          key: "Versioning.Status"
          value: absent
        - type: value
          key: "Versioning.Status"
          value: "Suspended"
    actions:
      - type: toggle-versioning
        enabled: true

  - name: S3-force-bucket-encryption
    description: This rule will force Bucket encryption
    resource: s3
    mode:
      type: periodic
      function-prefix: SEC-c7n-
      schedule: rate(2 minutes)
      role: arn:aws:iam::{account_id}:role/SECURITY/SEC-CustodianCore-global
    filters:
      - type: bucket-encryption
        state: False
    actions:
      - type: set-bucket-encryption
        crypto: AES256

S3 étant un service AWS Global, il suffit de déployer cette policy sur us-east-1 : nous pouvons, depuis cette région, scanner tous les buckets S3 présents sur le compte.

La régionalisation des règles est importante puisqu’une bonne partie des services AWS en sont dépendants, ce qui implique que les événements et les paramétrages des ressources se font région par région. C'est le cas du service EC2 duquel dépendent les volumes EBS que nous cherchons à chiffrer.

Pour répondre au second besoin d’avoir le paramètre de chiffrement par défaut activé, il suffit d'exécuter une policy C7N sans déployer de fonction Lambda :

policies:
 - name: EBS-set-encryption-by-default
   description: Will set Encryption by default
   resource: account
   filters:
     - type: default-ebs-encryption
       state: false
   actions:
     - type: set-ebs-encryption
       state: true
       key: alias/aws/ebs

La dernière demande est de pouvoir être notifié si un utilisateur désactive le paramètre précédemment forcé. Encore une fois, la magie de C7N opère avec la policy :

policies:
 - name: EBS-encryption-by-default-has-been-disabled
   description: Encryption by default of EBS has been disabled on a specific region
   resource: account
   mode:
     type: cloudtrail
     function-prefix: SEC-c7n-
     events:
       - source: ec2.amazonaws.com
         event: DisableEbsEncryptionByDefault
         ids: awsRegion
     role: arn:aws:iam::{account_id}:role/SECURITY/SEC-CustodianCore-global
   actions:
     - type: notify
       to: ["arn:aws:sns:us-east-1:{account_id}:SECURITY-alerts"]
       transport:
         type: slack
         url: "my_slack_hook_url"
         channel: "#cloud-security"

On notera ici l’utilisation d’une double notification, permettant (moyennant un hook) de notifier un channel Slack en plus des souscriptions à un topic SNS déployé au préalable.
Dans le repository mentionné ci-dessus, le déploiement de Terraform ainsi que de ces règles C7N est piloté par configuration et par un wrapper Python.

Disclaimer : Nous sommes ici à l’état de POC, il est nécessaire d’aller plus loin pour couvrir tous les risques liés à l’utilisation des services S3 et EC2. L’intérêt de ce petit outil est que vous pouvez y ajouter comme bon vous semble les règles dont vous avez besoin pour commencer à travailler avec Cloud Custodian et l’IAM AWS.

Bonus : Dans le cadre d’une organisation AWS, étant donné que les régions de travail des utilisateurs sont protégées grâce à votre implémentation du framework Trust and Control, vous pourrez simplement rendre indisponibles les régions AWS qui ne sont pas utilisées via SCP.

Tirer partie des side-projects de Cloud Custodian

Cloud Custodian est un éco système complet, permettant sous forme déclarative d’implémenter des contrôles de vos plateformes cloud (AWS, mais pas que ! Azure est aussi disponible, ainsi que GCP, en version Beta pour le moment).

Plusieurs initiatives notables sont dérivées de ce moteur de règles avec la notion d’organisation, c7n_org permet de faire hériter des règles au niveau organisation, sur plusieurs régions de plusieurs comptes AWS (mais aussi plusieurs souscriptions Azure et plusieurs projets GCP appartenant à une même organisation).

On peut aussi parler de c7n_mailer, qui embarque des connecteurs pour plusieurs plateformes de messaging ou de centralisation de logs (SMTP server, Datadog, Splunk, Slack).

Enfin, il existe aussi des initiatives provider-dependant comme c7n_guardian permettant d’activer GuardDuty sur une organisation et de lier tous les Detectors de toutes les régions au même compte master pour finalement réagir à un évènement généré par GuardDuty.

Cette implémentation du framework Trust and Control n'est bien sûr pas faite pour toutes les organisations, elle est coûteuse en temps de développement et nécessite des changements au fil des évolutions des cloud providers ainsi que des librairies open source. Elle permet néanmoins une gestion fine des droits, garantit la traçabilité et un fine tuning rarement égalé par les solutions PaaS des grands noms de la sécurité du cloud. Pour qui veut de la sécurité compréhensible et à la demande pour couvrir les risques opérationnels liés à l’adoption du cloud public, c’est la solution qu’il vous faut.