Vérifier la conformité de vos tâches ECS avec AWS Config et Cosign
Nous assistons depuis ces derniers mois à l’émergence d’attaques sur la “Supply Chain” logicielle. Le principe est de compromettre vos chaînes de production d’applicatifs afin de pouvoir exécuter du code malveillant au sein de votre infrastructure.
Un vecteur d’attaque de plus en plus fréquent consiste à introduire du code malveillant à différents moments du cycle de vie de vos conteneurs.
La signature d’images de conteneur permet de valider la provenance d’une image de conteneur, de vérifier que l’image n’a pas été falsifiée et de définir des politiques pour déterminer les images validées que vous autorisez à extraire sur vos systèmes.
Dans cet article, vous allez découvrir comment signer vos conteneurs avec Cosign et comment vérifier la conformité et l’intégrité de vos conteneurs avec AWS Config.
Architecture générale de la solution
L’illustration ci-dessous présente l’architecture de la solution mise en oeuvre. Chaque étape du processus est détaillée dans les sections suivantes.
Signer vos images Docker avec Cosign et AWS Key Management Service (KMS)
Pour signer numériquement une image de conteneur, nous allons utiliser Cosign, un projet de Sigstore.
L’objectif du projet Sigstore est de fournir un standard open-source de signature et de vérification d’artefacts logiciels tels que des images Docker, mais aussi, des binaires, archives, etc. Cosign s’intègre nativement avec AWS Key Management Service (AWS KMS).
Avant de pouvoir procéder à la création d’une signature pour une image de conteneur, nous avons besoin de créer une paire de clés asymétriques. Cette dernière pourra être utilisée pour signer et pour vérifier la signature de l’ensemble de nos images de conteneurs.
1. Prérequis
Pour implémenter ce qui est décrit dans cet article, vous aurez besoin des éléments suivants :
- aws-cli v2 configuré avec le bon profil
- docker disponible en ligne de commandes
- cosign v1 disponible en ligne de commandes
- Informations d’identification de sécurité AWS Identity and Access Management (IAM) avec les permissions de créer :
- Une clé AWS Key Management Service (KMS)
- Un dépôt AWS Elastic Container Registry (ECR)
- Un rôle AWS Identity and Access Management (IAM)
- Une fonction AWS Lambda
- Une règle AWS Config
- Des tâches AWS Elastic Container Service (ECS)
Il est aussi possible d’utiliser l’environnement de développement AWS : AWS CloudShell / AWS Cloud9.
2. Création de la clé AWS CMK
La première étape consiste à créer une clé KMS gérée par le client (CMK) au sein de votre compte AWS. Pour cela, nous pouvons utiliser la ligne de commande suivante :
$ aws kms create-key --customer-master-key-spec RSA_4096 \
--key-usage SIGN_VERIFY \
--description "Cosign Signature Key Pair" \
--query KeyMetadata.Arn --output text
En retour, vous obtiendrez l’ID et l’ARN de la clé KMS qu’il convient de conserver pour la suite de cette article.
Nous nous y référerons sous les variables $KMS_KEY_ID
et $KMS_KEY_ARN
.
3. Construction, Envoi & Signature de vos images Docker
Nous allons stocker nos images Docker au sein d’un dépôt AWS ECR. Pour cela, nous allons créer un dépôt à l’aide de la ligne de commande suivante :
$ aws ecr create-repository --repository-name docker-cosign
Ensuite, nous allons construire une image Docker basée sur Nginx puis l’envoyer sur le dépôt ECR:
$ export AWS_REGION=<votre-region>
$ cat > Dockerfile-signed <<EOF
FROM nginxinc/nginx-unprivileged:stable-alpine
USER root
RUN echo "Cette image est signée." > /usr/share/nginx/html/index.html
USER nginx
EOF
$ aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
$ docker build -f Dockerfile-signed -t $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:signed .
$ docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:signed
Ensuite, à l’aide de l’utilitaire Cosign, nous allons signer l’image à l’aide de la clé KMS créée précédemment:
$ export AWS_REGION=<votre-region>
$ cosign sign --key awskms:///$KMS_KEY_ARN $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:signed
$ cosign verify --key awskms:///$KMS_KEY_ARN $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:signed
Pour illustrer le fonctionnement de vérification de conformité de l’image, nous allons également créer une image Docker non-signée dans le dépôt ECR.
$ export AWS_REGION=<votre-region>
$ cat > Dockerfile-unsigned <<EOF
FROM nginxinc/nginx-unprivileged:stable-alpine
USER root
RUN echo "Cette image n'est pas signée." > /usr/share/nginx/html/index.html
USER nginx
EOF
$ aws ecr get-login-password --region $AWS_REGION | docker login --username AWS --password-stdin $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com
$ docker build -f Dockerfile-unsigned -t $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:unsigned .
$ docker push $ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:unsigned
Après ces étapes, vous devez avoir l’aperçu suivant dans votre dépôt ECR :
Note: La signature est stockée dans le fichier sha256-<digest>.sig
au sein même du dépôt ECR.
Vérifier la conformité des conteneurs ECS avec AWS Config
Pour notre cas d’usage, nous allons développer une règle personnalisée AWS Config dont la logique permettra d’évaluer la conformité des conteneurs.
1. Création d’une fonction Lambda
Nous allons donc créer une fonction Lambda qui sera invoquée à chaque modification d’une tâche ECS :
- Lister l’ensemble des images Docker utilisées dans la définition de la tâche ECS
- Récupérer la clé publique utilisée pour signer les images Docker dans AWS KMS
- Vérifier la signature de chacune des imagess Docker
- Publier le status de conformité de la tâche ECS dans AWS Config
Le code source de la Lambda est disponible ici. Le SDK de Cosign étant disponible en Go uniquement, la Lambda est donc développée en Go.
Pour créer cette Lambda au sein de votre compte, suivez les étapes suivantes :
# 1. Télécharger le package de la fonction Lambda.
$ wget -O aws-config-ecs-check-image-signature.zip \
https://gitlab.com/atyos/public/aws-config-ecs-check-image-signature/-/raw/main/dist/bin/linux/amd64/aws-config-ecs-check-image-signature.zip?inline=false
# 2. Créer le rôle IAM pour cette fonction Lambda et récupérer son ARN
$ cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "lambda.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
$ aws iam create-role --role-name VerifyECSImageSignatureLambdaRole \
--assume-role-policy-document file://trust-policy.json \
--query Role.Arn --output text
# Note: L'ARN du rôle sera affiché après exécution de la commande.
# 3. Attacher une politique en ligne (inline policy) au rôle
# - Remplacer $AWS_REGION par la région dans laquelle vos ressources sont déployées
# - Remplacer $ACCOUNT_ID par la région dans laquelle vos ressources sont déployées
# - Remplacer $KMS_KEY_ID par l'ID de la clé KMS
$ cat > inline-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CreateLogGroup",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:$AWS_REGION:$ACCOUNT_ID:*"
},
{
"Sid": "PutLogEvents",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:$AWS_REGION:$ACCOUNT_ID:log-group:/aws/lambda/VerifyECSImageSignature:*"
},
{
"Sid": "PutEvaluationsToConfig",
"Effect": "Allow",
"Action": "config:PutEvaluations",
"Resource": "*"
},
{
"Sid": "GetKMSPublicKey",
"Effect": "Allow",
"Action": [
"kms:GetPublicKey",
"kms:DescribeKey"
],
"Resource": "arn:aws:kms:$AWS_REGION:$ACCOUNT_ID:key/$KMS_KEY_ID"
},
{
"Sid": "GetTokenForECR",
"Effect": "Allow",
"Action": [
"ecr:GetAuthorizationToken"
],
"Resource": "*"
},
{
"Sid": "GetImageFromECR",
"Effect": "Allow",
"Action": [
"ecr:BatchGetImage",
"ecr:GetDownloadUrlForLayer"
],
"Resource": "arn:aws:ecr:$AWS_REGION:$ACCOUNT_ID:repository/docker-cosign"
}
]
}
EOF
$ aws iam put-role-policy --role-name VerifyECSImageSignatureLambdaRole \
--policy-name VerifyECSImageSignatureLambdaPolicy \
--policy-document file://inline-policy.json
# 4. Créer la fonction Lambda
# - Remplacer KMS_KEY_ARN par l'ARN de la clé KMS
# - Remplacer LAMBDA_ROLE_ARN par l'ARN du rôle créé dans l'étape 2.
$ aws lambda create-function --function-name VerifyECSImageSignature \
--zip-file fileb://aws-config-ecs-check-image-signature.zip \
--handler main \
--runtime go1.x \
--architectures x86_64 \
--environment Variables={AWS_CONFIG_TEST_MODE=true|false,COSIGN_AWS_KMS_KEY_ARN=$KMS_KEY_ARN} \
--role $LAMBDA_ROLE_ARN
2. Configuration de la règle personnalisée dans AWS Config
Une fois que la Lambda VerifyECSImageSignature
est déployée, la règle AWS Config peut être configurée comme suit :
Afin de vérifier si la règle fonctionne, nous allons créer deux tâches ECS :
- La première
ECSTaskSignedImage
utilisera l’image signée$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:signed
. - La seconde
ECSTaskUnsignedImage
utilisera l’image non signée$ACCOUNT_ID.dkr.ecr.$AWS_REGION.amazonaws.com/docker-cosign:unsigned
.
Après quelques secondes, le résultat de la vérification de conformité apparaît dans la console AWS Config :
Nous obtenons donc le résultat attendu :
- La tâche ECS
ECSTaskSignedImage
utilisant l’image Docker signée est considéré commeCOMPLIANT
, c’est à dire conforme aux règles de sécurité définies dans le compte. - La tâche ECS
ECSTaskUnsignedImage
utilisant l’image Docker non-signée est considérée commeNON_COMPLIANT
, c’est-à-dire non conforme aux règles de sécurité définies dans le compte.
Estimation des coûts
Pour mettre en place cette implémentation, vous serez facturé par AWS pour les services suivants (les tarifs indiqués sont pour la région eu-west-3) :
- AWS Key Management Service : 0.15$ pour 10.000 requêtes sur l’API KMS.
- AWS Elastic Container Registry : 0.10$ par GB et par mois pour le stockage, 0.00$ de frais de transfert de données en restant dans la même région AWS.
- AWS Lambda : 0.0000000021$ pour 1ms d’exécution/128MB de mémoire et 0.20$ pour 1 million de requêtes.
- AWS Config : 0.001$ par évaluation de la règle et par région
- AWS Elastic Container Service (via AWS Fargate): 0.01458$ par vCPU par heure et 0.00159$ par GB par heure.
Suppression de l’ensemble des ressources
Si vous souhaitez supprimer l’ensemble des ressources provisionnées dans le cadre de cet article, suivez les étapes suivantes :
- Désactiver les tâches AWS ECS
- Supprimer la règle AWS Config
- Supprimer la clé KMS
$ aws kms schedule-key-deletion --key-id $KMS_KEY_ID --pending-window-in-days 7
- Supprimer le dépôt ECR avec les images associées
$ aws ecr delete-repository --force --repository-name docker-cosign
- Supprimer la fonction Lambda avec le rôle associé
$ aws lambda delete-function --function-name VerifyECSImageSignature
$ aws iam delete-role --role VerifyECSImageSignatureLambdaRole
Conclusion
Dans cette article, nous avons présenté et détaillé la manière d’intégrer les processus de signature et de vérification d’images Docker à l’aide de Cosign et d’AWS Config. Cette solution permet de s’assurer que les tâches ECS s’exécutant dans votre compte AWS utilisent exclusivement des images signées avec une clé KMS de votre choix.
Si la signature de l’image utilisée dans la tâche ECS est incorrecte, une non-conformité sera remontée et tracée dans la console AWS Config. Vous pourrez alors bénéficier de l’intégration native d’AWS EventBridge avec AWS Config pour notifier votre équipe de sécurité via SNS ou appliquer vos propres processus de rémédiationn en terminant la tâche ECS avec une autre fonction Lambda, par exemple.