Contenu

Récupérer des objets S3 de manière sécurisée avec Curl et OpenSSL en utilisant SIGV4

Alors que je travaillais sur un projet pour un client, j’ai rencontré une situation complexe. J’avais besoin de récupérer des objets S3 depuis des instances EC2 qui avaient été construites à partir d’une AMI de l’AWS Marketplace et déployées dans des subnets privés sans accès à Internet. Le service S3 est accessible via des Gateway endpoints S3 déployés dans ces subnets privés. Cependant, l’instance n’avait pas AWS CLI installé, et la version de Curl qui était installée n’avait pas l’option pour générer automatiquement la Signature v4 pour s’authentifier avec l’API AWS (–aws-sigv4). Cela posait un problème pour moi, car je devais trouver un moyen de m’authentifier avec l’API AWS afin de récupérer des objets S3 avec seulement Curl et OpenSSL comme outils.

Processus d’authentification de l’API AWS en utilisant HTTP avec SIGV4

Pour s’authentifier avec l’API AWS en utilisant HTTP, nous pouvons utiliser le processus AWS Signature Version 4, également connu sous le nom de SIGV4. Ce processus est détaillé dans la documentation officielle AWS ici. Les principales étapes du processus sont les suivantes :

Premièrement, nous devons créer une Canonical Request qui consiste à créer une chaîne de caractères qui inclut la méthode HTTP, l’URI, les paramètres de la query string et les headers de la requête.

GET
/
Action=DescribeInstances&Version=2016-11-15
content-type:application/x-www-form-urlencoded; charset=utf-8
host:ec2.amazonaws.com
x-amz-date:20220830T123600Z

host;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

Exemple d’une Canonical Request pour exécuter l’Action DescribeInstances sur l’API AWS EC2 (Source : Documentation officielle AWS)

Ensuite, générer une String to Sign en créant une chaîne qui spécifie l’algorithme de hash utilisé, la région, la date et l’heure de la requête et le hash de la Canonical Request que nous avons créée à l’étape 1

AWS4-HMAC-SHA256
20220830T123600Z
20220830/us-east-1/ec2/aws4_request
f536975d06c0309214f805bb90ccff089219ecd68b2577efef23edd43b7e1a59

Exemple d’une String to Sign (Source : Documentation officielle AWS)

Calculer la Signature avec HMAC-SHA256 (une façon d’utiliser une fonction de hash cryptographique, SHA-256) et notre AWS Secret Access Key pour hasher la String to Sign que nous avons créée à l’étape 2, puis encoder le résultat en base64.

Et enfin, ajouter la Signature à la requête HTTP dans le header ‘Authorization’. Si la Signature a été correctement calculée, nous devrions être authentifiés avec succès auprès de l’API AWS.

Cependant, dans mon cas, je suivais ces étapes depuis une instance EC2 avec un Instance Profile associé. Et comme nous le savons, un IAM Instance Profile ne fournit pas des credentials à long terme mais des credentials temporaires. Et dans ce cas, le calcul de la signature est effectué en utilisant ces credentials temporaires.

J’ai constaté qu’il manquait des informations sur la façon de former une requête HTTP pour interagir avec l’API AWS S3 en utilisant une Signature Version 4 générée avec des credentials temporaires, et notamment pour télécharger un objet depuis S3. C’est pourquoi j’ai décidé d’écrire cet article pour partager comment résoudre ce problème avec un script bash.

Récupérer les credentials IAM pour un Instance Profile depuis les instance metadata

Premièrement, nous récupérons les credentials IAM depuis les instance metadata car ils sont nécessaires pour calculer la Signature. Notamment le Session Token car dans ce cas nous travaillons avec des credentials temporaires.

INSTANCE_PROFILE="$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/)"
METADATA=$(curl -s http://169.254.169.254/latest/meta-data/iam/security-credentials/$INSTANCE_PROFILE)
ACCESS_KEY_ID=$(echo "$METADATA" | grep AccessKeyId | sed -e 's/  "AccessKeyId" : "//' -e 's/",$//')
SECRET_ACCESS_KEY=$(echo "$METADATA" | grep SecretAccessKey | sed -e 's/  "SecretAccessKey" : "//' -e 's/",$//')
SESSION_TOKEN=$(echo "$METADATA" | grep Token | sed -e 's/  "Token" : "//' -e 's/",$//')

Former la Canonical Request

Pour construire la Canonical Request, nous avons besoin de plusieurs informations comme la méthode HTTP, l’URI, les paramètres de la query string et les headers de la requête. Dans le cas de récupération d’un objet S3 :

  • La méthode HTTP sera GET
  • L’URI sera le chemin de l’objet sur le Bucket S3
  • Le payload de la requête sera vide dans ce cas donc nous laissons une ligne vide.
  • Le champ Host sera au format suivant : <bucket_name>.s3.amazonaws.com
  • Le header “x-amz-content-sha256” est requis pour l’API S3 et comme c’est le hash SHA256 du payload, ce sera le hash d’une chaîne vide.
  • Le header “x-amz-date” devrait être le timestamp de la requête au format ISO8601
  • Le header “x-amz-security-token” doit être défini dans ce cas car nous travaillons avec des credentials temporaires
  • Les signed headers qui est la liste des headers définis précédemment séparés par des points-virgules
  • Le hash du payload qui est dans ce cas le hash d’une chaîne vide

Enfin, nous stockons la Canonical Request complétée dans un fichier temporaire canonical_request.tmp

Récupérer le nom de la région AWS où l’instance est lancée

REGION=$(curl -s http://169.254.169.254/latest/meta-data/placement/availability-zone | sed s/.$//)
AWS_SERVICE="s3"

HTTP_METHOD="GET"
CANONICAL_URI="/$2"
#Récupérer la date actuelle au format ISO8601
DATE_AND_TIME=$(date -u +"%Y%m%dT%H%M%SZ")
DATE=$(date -u +"%Y%m%d")

#Calculer le hash SHA256 du payload, qui est une chaîne vide
EMPTY_STRING_HASH=$(echo -n | openssl dgst -sha256 |cut -d ' ' -f 2)

# Stocker la Canonical request
/bin/cat >./canonical_request.tmp <<EOF
$HTTP_METHOD
$CANONICAL_URI

host:$BUCKET_NAME.s3.amazonaws.com
x-amz-content-sha256:$EMPTY_STRING_HASH
x-amz-date:$DATE_AND_TIME
x-amz-security-token:$SESSION_TOKEN

host;x-amz-content-sha256;x-amz-date;x-amz-security-token
$EMPTY_STRING_HASH
EOF

#Supprimer la nouvelle ligne finale
printf %s "$(cat canonical_request.tmp)" > canonical_request.tmp

Calculer la signing key

Pour calculer la signing key qui sera utilisée pour générer la Signature, nous devons effectuer ces quatre étapes :

  • Concaténer la chaîne “AWS4” avec la secret access key et générer un hash SHA256 avec la Secret Access Key comme clé et la Date comme données. Nous stockons le résultat dans une variable appelée DATE_KEY. Pour simplifier le calcul, je définis une fonction qui génère un hash SHA256 avec Key et Data comme entrées

Fonction pour générer un hash sha256

function hmac_sha256 {
  KEY="$1"
  DATA="$2"
  echo -n "$DATA" | openssl dgst -sha256 -mac HMAC -macopt "$KEY" | sed 's/^.* //'
}

DATE_KEY=$(hmac_sha256 key:"AWS4$SECRET_ACCESS_KEY" $DATE)
  • Générer un hash SHA256 avec DATE_KEY comme clé et le nom de la Région comme Data. Le résultat est stocké dans une variable appelée DATE_REGION_KEY
DATE_REGION_KEY=$(hmac_sha256 hexkey:$DATE_KEY $REGION)
  • Générer un hash SHA256 avec DATE_REGION_KEY comme clé et le nom du Service comme Data (ici S3). Le résultat est stocké dans une variable appelée DATE_REGION_SERVICE_KEY
AWS_SERVICE="s3"
DATE_REGION_SERVICE_KEY=$(hmac_sha256 hexkey:$DATE_REGION_KEY $AWS_SERVICE)
  • Générer un hash SHA256 avec DATE_REGION_SERVICE_KEY comme clé et la chaîne “aws4_request” comme Data. Le résultat est stocké dans une variable appelée HEX_KEY qui est la Signing Key qui sera utilisée pour générer la Signature.
HEX_KEY=$(hmac_sha256 hexkey:$DATE_REGION_SERVICE_KEY "aws4_request")

Calculer la Signature

Afin de calculer la Signature, nous devons d’abord former la String to Sign qui spécifie l’algorithme de hash utilisé, la région, la date et l’heure de la requête et le hash de la Canonical Request que nous avons générée précédemment. La String to Sign est stockée dans un fichier temporaire appelé string_to_sign.tmp

# Générer le hash de la canonical request
CANONICAL_REQUEST_HASH=$(openssl dgst -sha256 ./canonical_request.tmp | awk -F ' ' '{print $2}')

# Stocker la String to Sign
/bin/cat >./string_to_sign.tmp <<EOF
AWS4-HMAC-SHA256
$DATE_AND_TIME
$DATE/$REGION/$AWS_SERVICE/aws4_request
$CANONICAL_REQUEST_HASH
EOF

printf %s "$(cat string_to_sign.tmp)" > string_to_sign.tmp

Nous avons maintenant la String to Sign et la Signing Key, nous pouvons calculer la Signature

Générer la signature

SIGNATURE=$(openssl dgst -sha256 -mac HMAC -macopt hexkey:$HEX_KEY string_to_sign.tmp | awk -F ' ' '{print $2}')

Effectuer la requête HTTP complète

Maintenant que nous avons la signature générée, nous pouvons effectuer la requête HTTP avec les headers appropriés pour télécharger un objet S3 de manière sécurisée sans utiliser l’outil AWS CLI !

curl -s https://$BUCKET_NAME.s3.amazonaws.com/$CANONICAL_URI \
  -X $HTTP_METHOD \
  -H "Authorization: AWS4-HMAC-SHA256 \ 
  Credential=$ACCESS_KEY_ID/$DATE/$REGION/$AWS_SERVICE/aws4_request, \ 
  SignedHeaders=host;x-amz-content-sha256;x-amz-date;x-amz-security-token, \
  Signature=$SIGNATURE" \
-H "x-amz-content-sha256: $EMPTY_STRING_HASH" \
-H "x-amz-date: $DATE_AND_TIME" \
-H "x-amz-security-token: $SESSION_TOKEN" \
-o "$OUTPUT"

Le script complet est disponible sur GitHub ici, les prérequis sont les suivants :

  • Un système d’exploitation avec un interpréteur Bash
  • Le programme curl pour envoyer des requêtes HTTP
  • Le programme openssl pour générer des hash et des signatures
  • Accès à l’interface instance metadata EC2 (http://169.254.169.254/latest/meta-data/)
  • Des credentials de compte IAM valides et un session token, accessibles via l’interface instance metadata EC2.
  • Les permissions nécessaires pour accéder à un objet dans un bucket S3, incluant le nom du bucket et le chemin vers l’objet.