Contactez-nous
-
sécurité

La sécurité dans le Cloud démystifiée avec Cloud Custodian

Nous le savons tous, mettre des centaines de développeurs, architectes, et intervenants du métier sur les belles plateformes du Cloud Public peut poser nombre de problèmes de sécurité, d’explosions de coûts et d’expositions de données si l’on ne met pas en place une forme de sécurité active et opérationnelle en amont.

Cloud Custodian

Sommaire

La plupart des fournisseurs de plateformes Cloud Public nous précisent une matrice de responsabilité partagée des risques (un overview des responsabilités dans un précédent article dans AWS). Dans les faits, l’utilisateur est responsable de la sécurité dans le Cloud tandis que le fournisseur est responsable de la sécurité de ses infrastructures. Partant de ce postulat, tout n’est pas à faire mais une bonne partie des risques de la Cloud Control Matrix sont à couvrir de la même manière que dans un Cloud Privé. L’objectif de ce billet est de vous présenter un outil open-source, Cloud Custodian, qui permet de répondre à tous les besoins de sécurité qui nous incombent en tant qu'utilisateurs d’un Cloud Public.

Introduction à Cloud Custodian

Certains fournisseurs de Cloud Public mettent à disposition des outils de compliance adaptés à leurs problématiques. Citons Security Hub et Config sur AWS, Chronicle et Cloud Asset Inventory sur GCP ou encore Security Center et Sentinel sur Azure. Néanmoins la question du contrôle des plateformes sur des critères liés au métier même des applications sur un moteur unifié entre les providers reste en suspens.

Développé par Capital One, Cloud Custodian est une librairie open-source qui permet de mettre en place des règles de conformité, des contrôles de sécurité sur la configuration des ressources grâce à un simple langage déclaratif en YAML, le tout compatible sur tous les gros fournisseurs de Cloud Public.

Quelques exemples pour commencer sur Azure et GCP :

Voici une policy permettant de trouver et supprimer tous les disques qui ne sont pas attachés à une machine dans une souscription Azure :

# myPolicy.yml
policies:
   - name: find-non-attached-disks
     resource: azure.disk
     filters:
      - type: value
        key: managedBy
        value: null
     actions:
      - type: delete

Ensuite, une policy notifiant la présence d’un snapshot Pub/Sub relatif à un topic déjà supprimé dans GCP (notez que pour ce cas, vous avez besoin d’un autre topic Pub/Sub qui portera l’envoi de notification) :

# myPolicy.yml
policies:
   - name: notify-on-delete-pub-sub-snapshot
     resource: gcp.pubsub-snapshot
     filters:
      - type: value
        key: topic
        value: _deleted-topic_
     actions:
      - type: notify
        to: 
          - notification@adress.com
        format: txt
        transport: 
          type: pubsub
          topic: _my-notification-pubsub-topic_

NB: Dans le cas de GCP, la librairie Cloud Custodian est encore en beta et continue d’évoluer pour couvrir plus de services et cas d’usage.

Pour simplifier le contenu de ce billet, nous allons essentiellement expliciter le fonctionnement de Cloud Custodian via des exemples sur AWS. Commençons avec l’exemple de cette policy :

# myPolicy.yml
policies:
   - name: set-default-ebs-encryption
     resource: aws.account
     filters:
      - type: default-ebs-encryption
        state: false
     actions:
      - type: set-ebs-encryption
        state: true
        key: alias/aws/ebs

Pour utiliser cette règle après avoir installé Cloud Custodian, rien de plus simple, il vous suffira de lancer custodian run myPolicy.yml, ainsi le moteur va hériter de vos variables d’environnement pour exécuter cette policy.

Exécuter cette policy forcera tous les nouveaux volumes EBS à être chiffrés par défaut utilisant la clé AWS KMS spécifiée (alias/aws/ebs) pour les ressources EBS.

D’un point de vue technique, le moteur de Cloud Custodian va exécuter un processus Python (3.x) pour ensuite appeler les API d’AWS conformément à ce qui est décrit dans la policy.

L’approche policy-based appliquée à la sécurité

Compte tenu des évolutions des standards de sécurité ainsi que de l’hétérogénéité des axes d’attaque, le plus souvent internes mais aussi externes, le modèle de centralisation et de gouvernance des règles de sécurité s’est adapté. En adoptant l’approche policy-based, à l’instar de nombre de domaines informatiques (Réseau, Automatisation, Orchestration, …), on enrichit le framework général de sécurité au sein des SI. Ce modèle s’articule autour de différents paradigmes pour fonctionner d’une manière optimale :

  • PMC (Policy Management Console) : un outil d’exploration/validation des policy ;
  • PR (Policy Repository) : référentiel qui centralise les policies existantes ;
  • PDP (Policy Decision Point) : moteur de traduction technique d’une policy en une exécution ;
  • PEP (Policy Enforcement Point) : support d’exécution d’une policy traduite en processus.

Dans le cas de Cloud Custodian, la librairie open-source couvre les parties PMC & PDP, il est d’usage d’y associer un VCS (Version Control System i.e. : Git) comme PR pour profiter des ses propriétés collaboratives et d'historisation. Le socle d’exécution d’une règle Cloud Custodian est dépendant du type de policy ainsi que du fournisseur Cloud Public que l’on cherche à lancer, il existe plusieurs modes de fonctionnement que nous expliciterons plus bas.

En tant que SecOps, nous cherchons un moyen simple et clair pour gérer la sécurité sur les différents environnements sous notre casquette. Se baser sur des politiques déclaratives apporte autant d’avantages que l’utilisation d’Infrastructure as Code pour centraliser d’une manière intelligible et pérenne dans le temps les caractéristiques techniques de nos plateformes. De plus, cela permet d’apporter tous les avantages liés au code en termes de versionnement, de normalisation mais aussi de profiter des pratiques DevOps déjà en place.

Une policy Cloud Custodian s’appréhende selon une structure bien précise, chaque fichier comprend une ou plusieurs policies n’ayant pas forcément de lien entre elles, néanmoins une bonne pratique est de les regrouper par couverture de risque. Par exemple, si l’on cherche à couvrir le risque de chiffrement at rest des volumes EBS dans AWS ou peut facilement définir la structure suivante :

# ebs-sse-enforcement.yml
policies:
  - name: set-default-ebs-encryption #Policy 1 will enforce the default setting
  ....
  - name: update-default-ebs-encryption #Policy 2 will catch a change in the default setting and set it back
  ....
  - name: notify-ebs-unencrypted-creation #Policy 3 will verify ebs encryption at each new resource creation
  ....
  - name: notify-ebs-unencrypted-attached #Policy 4 will verify ebs encryption at each volume attachment
  ....
  - name: report-unecnrypted-ebs #Policy 5 will scan all ebs volumes and report each unencrypted
  ....

Dans l’exemple ci-dessus nous adressons les problématiques de sécurité suivantes :

  • Conformité des ressources à un standard de chiffrement ;
  • Contrôle des nouvelles ressources ;
  • Contrôle du changement d’une ressource ;
  • Avertissement actif d’une perte de conformité d’une ressource ;
  • Scan de conformité de toutes les ressources suivantes.

Coupler cette approche à un fonctionnement GitOps et une CI/CD permet de rendre vos politiques de sécurités transparentes et source de vérité des règles de sécurité pour tous les utilisateurs des plateformes que vous cherchez à sécuriser.

Les différents types de policies

Nous allons voir dans cette section les différentes solutions qui sont mises à notre disposition avec cet outil pour couvrir les risques inhérents à l’utilisation des plateformes du Cloud Public, toujours en prenant l’exemple d’AWS.

Pour plus d’information concernant les possibilités qu’offre cette librairie open-source, une CLI est mise à disposition une fois le package installé.

Run : custodian schema [args] pour avoir une vue détaillée de ce qui est disponible avec la version courante de la librairie ainsi que le schéma de policy attendu pour chaque option.

NB : Cloud Custodian utilisera par la suite votre environnement pour trouver les identifiants nécessaires à exécuter les actions que vous avez définies. La commande custodian run path/to/file.yml permet l’utilisation de credentials directs mais aussi, en fonction du provider défini dans la policy, divers mécanismes d’impersonation. Par exemple, pour une policy orientée pour AWS, nous pouvons spécifier les arguments --assume-role ou --profile.

Structure d’une policy

Avant de commencer à écrire des policies Cloud Custodian pour un cas d’usage bien précis, il faut comprendre comment les écrire et à quoi sert chaque champ. De manière générale, il faudra écrire des policies comme suit :

# myPolicy.yml
policies:
   - name: set-default-ebs-encryption #Identification unique de la policy
     resource: aws.account #Type de ressource concernée
     filters: #Liste de filtres qui seront appliqués lors de l'exécution
      - type: default-ebs-encryption 
        state: false
     actions: #Liste d’actions à effectuer sur les ressources filtrées
      - type: set-ebs-encryption
        state: true
        key: alias/aws/ebs
  • le champ filters est optionnel, par exemple, si l’on choisit comme ressource resource: aws.ebs et qu’on souhaite sélectionner pour action toutes les ressources sans distinction, il suffit de ne pas spécifier cette section.
  • actions comprend la liste d’actions à effectuer sur les ressources définies dans le champs filters

On peut donc traduire la séquence précédente de la manière qui suit :

« Nous cherchons dans la ressource account (qui contient les paramètres généraux d’une région ou d’un compte) le paramètre default-ebs-encryption comme étant désactivé, si un tel paramètre est trouvé dans cet état, l’action de la policy est de l’activer.»

Il existe plusieurs autres configurations de policies utilisables en fonction du risque que l’on cherche à couvrir, en voici quelques exemples.

Exécution locale (OneShot)

Reprenons ici l’exemple présenté ci-dessus ; cette policy est vouée à une exécution locale, c’est à dire qu’elle ne sera exécutée que sur commande (manuelle, d’une CI/CD ou d’un automate). Le but de cette policy est d’être exécutée de manière unitaire.

Exécution sur évènement

Tous les fournisseurs de plateforme Cloud Public offrent une solution d’analyse d’évènements (communément appelée auditlog) à laquelle nous pouvons associer une exécution de code pour couvrir un risque en temps réel.

Dans le cas d’AWS, CloudTrail et CloudWatch peuvent servir de déclencheurs à Cloud Custodian pour effectuer une action en fonction d’un évènement spécifique. Cette fonctionnalité s’implémente de la façon suivante :

# myPolicy.yml
policies:
   - name: notify-ebs-unencrypted-creation #Identification unique de la policy
     resource: aws.ebs #Type de ressource concernée
     filters: #Liste de filtres qui seront appliqués lors de l'exécution
      - type: value
        key: Encrypted
        value: False
    description: "This function will notify when an unencrypted EBS volume is created"
    mode:
      function-prefix: "c7n-"
      type: cloudtrail #Type d'exécution, ici en événementiel au travers d'un évènement lancé par AWS Cloudtrail
      events: #Définition des événements de déclenchement
        - source: ec2.amazonaws.com
          event: CreateVolume
          ids: responseElements.volumeId #Id de la ressource à identifier dans la requête entrante
     role: arn:aws:iam::{account_id}:role/Custodian-role-EBS # IAM rôle qui sera utilisé par la fonction AWS Lambda
     actions: #Actions à effectuer sur les ressources filtrées
      - type: notify
        transport:
          - properties:
            topic: arn:aws:sns:us-east-1:{account_id}:topics/my-notification-topic #Topic SNS de notification
            type: sns

Concrètement, l’exécution de cette policy va déployer plusieurs éléments sur le compte AWS cible.

  • Une fonction AWS Lambda nommée c7n-notify-ebs-unencrypted-creation
  • Un règle CloudWatch déclenché sur un évènement _CreateVolume_ lancé par CloudTrail

NB : Avant d’appliquer cette policy, il faut s’assurer que le compte AWS inclut un Trail CloudTrail multi région, un rôle IAM nommé Custodian-role-EBS utilisable par le service lambda.amazonaws.com ainsi qu’un topic SNS nommé my-notification-topic sur la région us-east-1.

On note donc que l'ajout d’un bloc mode permet de déporter le socle d’exécution de la policy d’une exécution locale à une exécution se basant sur le service FaaS du fournisseur Cloud Public. Ici dans AWS mais on retrouve le même fonctionnement avec les Cloud Functions de GCP ou Azure Functions dans le Cloud de Microsoft pour ne citer qu’eux.

Exécution périodique

Au même titre que dans l’exécution sur évènements, on peut moduler le bloc mode pour avoir une exécution périodique selon une expression cron() ou une expression rate().

# myPolicy.yml
policies:
   - name: notify-ebs-unencrypted-creation #Identification unique de la policy
     resource: aws.ebs #Type de ressource concernée
     filters: #Liste de filtres qui seront appliqués lors de l'exécution
      - type: value
        key: Encrypted
        value: False
    description: "This function will notify when an unencrypted EBS volume is created"
    mode:
      function-prefix: "c7n-"
      type: periodic #Type d'exécution, ici en événementiel au travers d'un évènement lancé par AWS CloudTrail
      schedule: rate(1 day) #Déclenchement tous les jours à l'heure de l'exécution de la policy, peut être remplacé par une expression cron(0 1 * * *) pour être déclenché tous les jours à 1h du matin
    role: arn:aws:iam::{account_id}:role/Custodian-role-EBS # IAM rôle qui sera utilisé par la fonction AWS Lambda
     actions: #Actions à effectuer sur les ressources filtrées
      - type: notify
        transport:
          - properties:
            topic: arn:aws:sns:us-east-1:{account_id}:topics/my-notification-topic #Topic SNS de notification
            type: sns

Si l’on considère qu’un scan est nécessaire sur une base journalière pour avoir à l’instant T l’état de conformité de votre plateforme en fonction des critères de sécurité, vous pouvez rechercher par exemple toutes instances-profiles pour EC2 qui ont des droits administrateurs et les tagger pour effectuer une vérification manuelle ultérieure.

Spécificités et cas particuliers

Nous savons à présent comment se définit une policy ainsi que les différentes applications possibles et modes de fonctionnement. Il existe néanmoins quelques utilisations spécifiques à certains services dues à leur temps de création par exemple, qui ne permettent pas d’effectuer directement les actions de remise en conformité.

Action Mark-for-op

Comme vous pouvez le deviner de par son nom, cette action permet de marquer une ressource pour y effectuer une action ou une vérification ultérieure. Dans le cas d’une table AWS DynamoDB par exemple, si l’on souhaite forcer une certaine configuration il faut tout d’abord appliquer un tag sur la ressource puis attendre que la table soit finalisée avant de faire les modifications de mise en conformité.

Pour mettre en place ce type d’action, nous pouvons dans le même fichier YAML déclarer plusieurs policies pour effectuer cette action en deux temps :

  • première étape : à la création d’une ressource, vérifier sa configuration et apposer un tag si la compliance n’est pas vérifiée.
# myPolicy.yml
policies:
   - name: tag-tables-without-pitr #Identification unique de la policy
     resource: aws.dynamodb-table #Type de ressource concernée
     filters: #Liste de filtres qui seront appliqués lors de l'éxécution
      - type: continuous-backup
        key: ContinuousBakcupStatus
        value: DISABLED
    description: "This function will tag a DynamoDB Table for action at creation if continous backups are disabled"
    mode:
      function-prefix: "c7n-"
      type: cloudtrail #Type d'exécution, ici en événementiel au travers d'un évènement lancé par AWS CloudTrail
      events: #Définition des événements de déclenchement
        - source: dynamodb.amazonaws.com
          event: CreateTable
          ids: responseElements.TableName #Id de la ressource à identifier dans la requête entrante
    role: arn:aws:iam::{account_id}:role/Custodian-role-DYNAMODB # IAM rôle qui sera utilisé par la fonction AWS Lambda
     actions: #Actions à effectuer sur les ressources filtrées
      - type: mark-for-op
        tag: custodian-action
        op: set-continous-backup
  • seconde étape : mettre en place une règle périodique qui va chercher des ressources disponibles pour effectuer l’action définie en première étape et ayant le tag custodian-action.
 - name: enforce-tables-pitr #Identification unique de la policy
     resource: aws.dynamodb-table #Type de ressource concernée
     filters: #Liste de filtres qui seront appliqués lors de l'éxécution
      - type: continuous-backup
        key: ContinuousBakcupStatus
        value: DISABLED
      - type: marked-for-op
        tag: custodian-action
        op: set-continous-backup
    description: "This function will enforce continuous backups on a tagged DynamoDB Table"
    mode:
      function-prefix: "c7n-"
      type: periodic
    schedule: rate(2 minutes)
    role: arn:aws:iam::{account_id}:role/Custodian-role-DYNAMODB # IAM rôle qui sera utilisé par la fonction AWS Lambda
     actions: #Actions à effectuer sur les ressources filtrées
      - type: set-continous-backup
      - type: remove-tag # Pour finir enlevons le tag placé précédemment
        tags: ['custodian-action']

Avec cette action en deux temps, nous avons donc la possibilité de forcer des paramétrages de ressources avec un temps d’action réduit même si le paramètre n’est pas modifiable à la création de la ressource.

Quand Cloud Custodian ne suffit pas

Dans sa démarche Cloud Native, l’outil Cloud Custodian open-source est en perpétuelle évolution (environ une release tous les deux à trois mois). Mais toute évolution si grande soit-elle ne pourra que se rapprocher et non pas égaler les évolution des fournisseurs cloud. Dans une bonne partie des cas, déclencher un traitement spécifique est nécessaire pour couvrir vos besoins. Je parle ici de la possibilité d’exécuter d’autre fonctions serverless après filtrage par Cloud Custodian, cela vous permet d’effectuer des workflows de remédiation complexes et plus adaptés à votre contexte de sécurité par l’utilisation de l’action générique (disponible sur toutes les ressources) invoke-lambda dans AWS. Dans les faits, cette fonctionnalité va renvoyer à un autre processus serverless (AWS Lambda, Cloud Fonction, Azure Fonction), le résultat du filtrage demandé au travers de la policy pour vous permettre d’exécuter du code que vous avez produit sur les ressources identifiées.

Pour illustrer cette action, nous allons tirer profit de l’RBAC d’AWS. Admettons que dans notre environnement, tous les rôles IAM créés doivent êtres soumis à une Permission Boundary. Pour savoir quelle boundary doit s’appliquer sur un rôle fraîchement créé, nous nous basons sur un référentiel dans DynamoDB. La règle est la suivante : Selon le path du rôle, une permission boundary spéciale y est appliquée.

Admettons qu'une base DynamoDB est déjà disponible sur la plateforme avec les paramètres suivants :

Role-path (PK)

Boundary-identifier

/Administrators/

arn:aws:iam::XXXXXXXXX:policy/AdminBoundary/generic-policy

/PowerUsers/

arn:aws:iam::XXXXXXXXX:policy/PUBoundary/generic-policy

/Application/

arn:aws:iam::XXXXXXXXX:policy/AppBoundary/generic-policy

Ainsi qu’une fonction AWS Lambda nommée custom-boundary-attacher emportant le pseudo-code suivant dans le langage de votre choix :

Fonction exec(event) {
    path = event.requestParameters.get(‘path’) //Permet de récupérer le path du rôle
    boundary_arn = AWSDynamoDBTable.Role-path(path).get(’Boundary-identifier’)
    AWSIAMRole(‘role’).SetBoundary(boundary_arn)
}

Une fois ces composants déployés, il n’y a plus qu'à écrire la policy Cloud Custodian qui réagira à la création d’un rôle pour exécuter la fonction ci-dessus :

```
- name: iam-attach-boundary-at-creation
  resource: iam-role
  filters:
    - type: event
      key: detail.requestParameters.permissionBoundaryArn
      value: false
  description: "This function will trigger a custom process to attach a permission boundary on a newly created IAM Role"
  mode:
    function-prefix: "c7n-"
    type: cloudtrail #Type d'exécution, ici en événementiel au travers d'un évènement lancé par AWS CloudTrail
    events: #Définition des événements de déclenchement
      - source: iam.amazonaws.com
        event: CreateRole
        ids: responseElements.RoleName #Id de la ressource à identifier dans la requête entrante
    role: arn:aws:iam::{account_id}:role/Custodian-role-IAM
  actions:
    - type: invoke-lambda
      function: custom-boundary-attacher

On note ici l’utilisation d’un filtre générique event, ce filtre très pratique vous permet d’effectuer le filtrage avec Cloud Custodian directement sur l’évènement déclenché via CloudTrail, dans notre cas on vient filtrer les rôles à la création qui n’ont pas de permission boundary.

On voit dans ce cas d’usage, qu'à partir de cette fonctionnalité, on peut couvrir quasiment tous les risques liés aux ressources des fournisseurs cloud, en profitant des différentes possibilités de filtrage et de déclenchement de Cloud Custodian.

Réagir à la suppression d’une ressource

Cloud Custodian permet aussi de réagir à la suppression d’une ressource pour par exemple notifier qu’une action potentiellement malveillante a eu lieu.

Quand l’on cherche à déterminer la suppression ou l’absence d’une ressource, le workflow est toujours le même, nous devons tout d’abord utiliser la resource: account puis dans les filtres décrire une nouvelle policy pour cibler l’élément qui doit être manquant.

Par exemple, voici comment réagir à la suppression d’un Rôle IAM en notifiant un topic SNS :

- name: iam-notify-on-role-deletion
  resource: account
  filters:
    - type: missing
      policy: 
        resource: iam-role
        filters:
          - type: value
            key: roleName
            op: eq
            value: detail.requestParameters.roleName
  description: "This function will notify an SNS topic when an IAM role is deleted"
  mode:
    function-prefix: "c7n-"
    type: cloudtrail #Type d'exécution, ici en événementiel au travers d'un évènement lancé par AWS CloudTrail
    events: #Définition des événements de déclenchement
      - source: iam.amazonaws.com
        event: DeleteRole
        ids: requestParameters.RoleName #Id de la ressource à identifier dans la requête entrante
    role: arn:aws:iam::{account_id}:role/Custodian-role-IAM
  actions:
    - type: notify
      transport:
        - properties:
          topic: arn:aws:sns:us-east-1:{account_id}:topics/my-notification-topic #Topic SNS de notification
          type: sns

On note ici l’utilisation d’un autre filtre générique value_qui se base sur le retour d’un appel API Describe${resource}. Dans notre cas, Cloud Custodian va vérifier qu'aucun rôle IAM, ayant le nom trouvé en paramètre de la requête de déclenchement DeleteRole (detail.requestParameters.roleName), n'est présent sur la plateforme avant d'effectuer l'action.

Pour aller plus loin avec cet outil open-source

Dans ce billet, vous avez acquis le vernis nécessaire pour utiliser cet outil et commencer à développer vos propres règles de sécurité. L’utilisation “standalone” de Cloud Custodian est déjà efficace en soi, on voit d’ailleurs la volonté des fournisseurs Cloud d’ouvrir la sécurisation de leurs plateformes à ce type d’outils avec l’intégration de Cloud Custodian dans la liste des produits disponibles au travers d’AWS Security Hub. Il existe plusieurs dérivations de projets intégrés à la logique de Cloud Custodian, comme c7n_org, c7n_mailer, c7n_guardian, c7n_policystream, etc… De plus, si l’on souhaite déployer une implémentation de Cloud Custodian à l’échelle, dans un environnement distribué, emportant plusieurs organisations, comptes, projets ou tenants, vous aurez besoin de pousser plus loin la logique de développement autour de Cloud Custodian.