Aller au contenu

Configurer GitHub Actions OIDC vers AWS sans clé statique

Configurer GitHub Actions OIDC vers AWS sans clé statique

Section intitulée « Configurer GitHub Actions OIDC vers AWS sans clé statique »

Une chaîne d’intégration et de déploiement continus, ou CI/CD (Continuous Integration and Continuous Delivery), ne doit pas contenir une clé AWS (Amazon Web Services) permanente. La fédération OpenID Connect, ou OIDC (OpenID Connect), permet au job GitHub Actions d’obtenir des identifiants AWS temporaires, liés à son dépôt, sa branche et son exécution.

→ Lire l’explication complète de la marmotte

Le compromis est net : on élimine une clé durable, donc une fuite exploitable pendant des mois, mais on transforme la politique de confiance en code de sécurité critique. Une condition trop large sur le dépôt ou la branche rend le jeton temporaire aussi dangereux qu’une clé permanente, avec davantage de syntaxe et moins de sommeil.

La recommandation est de restreindre le rôle AWS par la revendication sub complète, au minimum repo:ORGANISATION/DEPOT:ref:refs/heads/BRANCHE. Autoriser repo:organisation/* parce que « tous les dépôts sont de confiance » est une erreur d’architecture : tout dépôt compromis, créé par erreur ou mal gouverné devient une porte d’entrée cloud. Les fondations CI/CD ne remplacent pas une frontière d’autorisation.

Le cas traité utilise GitHub Actions comme émetteur de jetons et AWS Identity and Access Management, ou IAM, comme moteur d’autorisation. Le compte AWS doit autoriser la création d’un fournisseur OIDC, d’un rôle IAM et de politiques IAM. L’AWS Command Line Interface, ou AWS CLI, version 2 doit être authentifiée localement avec une identité d’administration, uniquement pendant le paramétrage initial.

Préparer les constantes suivantes. Remplacer les valeurs d’exemple avant toute commande :

Fenêtre de terminal
# Identifiant numérique du compte AWS cible
export AWS_ACCOUNT_ID="123456789012"
# Dépôt GitHub autorisé, avec casse exacte
export GITHUB_REPOSITORY="tortuetech/infra-production"
# Branche seule autorisée à demander le rôle
export GITHUB_BRANCH="main"
# Nom du rôle assumé par le pipeline
export AWS_ROLE_NAME="github-actions-deploy-production"

GitHub émet ses jetons depuis https://token.actions.githubusercontent.com. AWS vérifie la signature du jeton contre les clés publiques publiées par cet émetteur, puis évalue les revendications. Une revendication est une donnée signée dans le jeton, par exemple son audience, son dépôt ou sa référence Git. Elle n’est pas une variable d’environnement déclarative que le job peut réécrire avec enthousiasme et trois lignes de YAML.

Le job GitHub doit déclarer id-token: write. Cette permission n’autorise pas une écriture dans AWS, malgré son nom trompeur. Elle autorise uniquement l’obtention d’un jeton OIDC auprès de GitHub. Les autorisations AWS restent définies par la politique IAM attachée au rôle.

Ne pas exécuter un déploiement ayant accès à ce rôle depuis pull_request_target. Cet événement peut associer des permissions élevées à du code issu d’une demande de fusion non approuvée. Un jeton signé ne rend pas un script malveillant sympathique, il certifie seulement son origine et son contexte.

Créer le fournisseur OIDC une seule fois par compte AWS. L’audience sts.amazonaws.com désigne AWS Security Token Service, ou STS, le service qui échange le jeton GitHub contre des identifiants temporaires AWS.

Fenêtre de terminal
# Crée la relation de confiance entre le compte AWS et GitHub Actions.
# La commande échoue si ce fournisseur existe déjà dans le compte.
aws iam create-open-id-connect-provider \
--url "https://token.actions.githubusercontent.com" \
--client-id-list "sts.amazonaws.com"
# Contrôle de présence et récupération de l'ARN du fournisseur créé.
aws iam list-open-id-connect-providers \
--query "OpenIDConnectProviderList[?contains(Arn, 'token.actions.githubusercontent.com')].Arn" \
--output text

Le fournisseur OIDC n’accorde aucun droit AWS à lui seul. Il enregistre un émetteur accepté et l’audience attendue. Le rôle IAM porte la vraie décision : qui peut l’assumer, dans quel contexte, et quelles actions deviennent possibles ensuite.

Créer d’abord la politique de permissions. Elle doit viser les ressources précises manipulées par le déploiement. Le rôle ci-dessous peut lire et écrire dans un préfixe précis d’un compartiment Amazon Simple Storage Service, ou S3. Accorder s3:* sur * à un pipeline est le raccourci préféré des démonstrations commerciales et des incidents réels.

Fenêtre de terminal
cat > permissions-deploy.json <<'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DeployArtifactsOnly",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::tortuetech-production-artifacts/releases/*"
},
{
"Sid": "ListDeploymentPrefix",
"Effect": "Allow",
"Action": "s3:ListBucket",
"Resource": "arn:aws:s3:::tortuetech-production-artifacts",
"Condition": {
"StringLike": {
"s3:prefix": "releases/*"
}
}
}
]
}
EOF
aws iam create-policy \
--policy-name "github-actions-deploy-artifacts" \
--policy-document file://permissions-deploy.json

Politique de confiance AWS limitée au dépôt et à la branche

Section intitulée « Politique de confiance AWS limitée au dépôt et à la branche »

La politique de confiance est le point de contrôle déterminant. aud impose l’audience STS. sub lie le rôle à un dépôt et à la branche main. Utiliser StringEquals, pas StringLike, tant qu’aucun motif n’est indispensable. Un joker dans une identité est rarement un détail de confort, plutôt une permission qui attend son incident.

Fenêtre de terminal
cat > trust-policy.json <<EOF
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowGitHubActionsFromMainOnly",
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::${AWS_ACCOUNT_ID}:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:${GITHUB_REPOSITORY}:ref:refs/heads/${GITHUB_BRANCH}"
}
}
}
]
}
EOF
aws iam create-role \
--role-name "$AWS_ROLE_NAME" \
--assume-role-policy-document file://trust-policy.json
aws iam attach-role-policy \
--role-name "$AWS_ROLE_NAME" \
--policy-arn "arn:aws:iam::${AWS_ACCOUNT_ID}:policy/github-actions-deploy-artifacts"

Une protection supplémentaire consiste à déclarer repository_id dans les conditions IAM, lorsque la gouvernance impose une défense contre le renommage ou le transfert du dépôt. Cette revendication numérique est plus stable que le nom textuel. Elle demande toutefois de connaître l’identifiant GitHub du dépôt et de maintenir explicitement la politique si le dépôt est recréé. Pour la plupart des dépôts stables, combiner sub exact et revue des changements IAM constitue le minimum non négociable.

La politique de confiance ne protège pas contre un mainteneur autorisé qui modifie le workflow sur main. Ce risque relève des protections de branche, des règles de revue et de la séparation des droits GitHub. La fédération atteste le contexte GitHub, elle ne transforme pas ce contexte en sanctuaire.

Job GitHub Actions et échange du jeton temporaire

Section intitulée « Job GitHub Actions et échange du jeton temporaire »

Créer .github/workflows/deploy.yml. L’exemple demande un jeton OIDC, appelle explicitement STS, exporte les identifiants temporaires et vérifie l’identité obtenue. Aucun secret AWS n’est stocké dans GitHub.

name: deploy-production
on:
push:
branches: [main]
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- name: Échanger le jeton GitHub contre des identifiants AWS
shell: bash
env:
ROLE_ARN: arn:aws:iam::123456789012:role/github-actions-deploy-production
run: |
set -euo pipefail
# GitHub fournit une URL et un jeton d'accès éphémère au job.
OIDC_TOKEN="$(curl --fail --silent --show-error \
-H "Authorization: bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \
"${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=sts.amazonaws.com" \
| jq -r '.value')"
test -n "${OIDC_TOKEN}"
test "${OIDC_TOKEN}" != "null"
# STS retourne des clés de session limitées dans le temps.
aws sts assume-role-with-web-identity \
--role-arn "${ROLE_ARN}" \
--role-session-name "github-${GITHUB_RUN_ID}" \
--web-identity-token "${OIDC_TOKEN}" \
--duration-seconds 900 \
--output json > credentials.json
export AWS_ACCESS_KEY_ID="$(jq -r '.Credentials.AccessKeyId' credentials.json)"
export AWS_SECRET_ACCESS_KEY="$(jq -r '.Credentials.SecretAccessKey' credentials.json)"
export AWS_SESSION_TOKEN="$(jq -r '.Credentials.SessionToken' credentials.json)"
aws sts get-caller-identity
rm -f credentials.json
- name: Déployer les artefacts
run: aws s3 sync ./dist s3://tortuetech-production-artifacts/releases/

Fixer la durée à 900 secondes réduit la fenêtre d’exploitation, mais ne révoque pas une session déjà délivrée. Désactiver le fournisseur OIDC ou modifier une politique IAM empêche les futurs échanges, pas les clés temporaires déjà émises. Les permissions sensibles doivent donc être minimales, et les sessions courtes.

Sur un exécuteur auto-hébergé, installer AWS CLI v2, curl et jq, puis empêcher les jobs non approuvés de partager cet exécuteur. Un exécuteur persistant peut conserver des fichiers, processus ou variables entre jobs si son nettoyage est défaillant. Une identité parfaite attachée à une machine poreuse reste une identité parfaite dans une machine poreuse.

Une politique non testée est une décoration JSON. Créer une branche oidc-deny-test, y ajouter le même workflow avec un déclenchement sur cette branche, puis lancer le job. L’étape assume-role-with-web-identity doit échouer avec AccessDenied. Le journal doit montrer que STS refuse l’hypothèse du rôle, avant toute commande S3.

Tester aussi depuis un autre dépôt de la même organisation. Copier le workflow, conserver le même ROLE_ARN et lancer une exécution manuelle ou un push sur main. L’échec attendu est identique : la revendication sub contient le nom du second dépôt, qui ne correspond pas à la valeur autorisée.

Ne pas se contenter d’un test depuis la branche autorisée. Celui-ci prouve que le chemin nominal fonctionne, pas que la clôture tient. Les tests négatifs doivent être rejoués après toute modification de la politique de confiance, du nom du dépôt, de la stratégie de branche ou du mécanisme de déploiement.

Activer AWS CloudTrail pour conserver les appels STS et S3. Surveiller au minimum les événements AssumeRoleWithWebIdentity, les échecs répétés d’échange et les sessions dont le nom ne suit pas le format attendu. CloudTrail ne bloque rien, il permet de reconstituer un échange. C’est moins glamour qu’un tableau de bord violet, mais nettement plus exploitable après un problème.

La sauvegarde concerne le code de politique IAM, les workflows GitHub et la configuration d’infrastructure, pas des identifiants AWS inexistants. Versionner ces fichiers, protéger leurs modifications et prévoir un rôle d’urgence distinct, à authentification forte, pour réparer un fournisseur OIDC supprimé ou une politique devenue trop restrictive. Le rôle d’urgence ne doit jamais être le même rôle que celui du pipeline, sinon l’urgence devient une habitude avec des permissions d’administration.

Pour prolonger la réflexion sur les frontières d’identité et les privilèges, consulter la page identités et les principes d’architecture cybersécurité.

Sans fédération OIDC, l’accès AWS repose sur une clé statique déposée dans les secrets du dépôt. Cette clé reste valable jusqu’à rotation manuelle : une copie prise dans un journal de build, un cache d’exécuteur ou un poste de développement continue de fonctionner des mois plus tard, y compris hors de toute exécution GitHub. Le contexte disparaît aussi : CloudTrail n’enregistre qu’un utilisateur IAM unique, sans dépôt, sans branche, sans identifiant d’exécution, ce qui empêche de rattacher un appel S3 à un commit précis ou de distinguer un déploiement légitime d’un usage détourné. La révocation devient une opération globale qui interrompt tous les pipelines partageant la clé, et le test négatif décrit plus haut n’a plus d’équivalent : aucune condition ne peut refuser une branche non autorisée, puisque la clé ignore d’où elle est utilisée.

Un programme de déploiement doit parfois modifier des ressources hébergées chez un fournisseur cloud. Une ancienne méthode consistait à enregistrer une clé secrète permanente dans la plateforme de code. Si cette clé fuit, elle peut être utilisée jusqu’à son remplacement, parfois très longtemps.

Ici, la plateforme de code émet à chaque exécution un document signé qui décrit le programme en cours. Ce document contient notamment le dépôt de code, la branche utilisée et le destinataire prévu. Le fournisseur cloud connaît à l’avance la signature de la plateforme et vérifie que le document est authentique.

Le fournisseur cloud possède aussi une règle. Cette règle autorise uniquement le dépôt nommé et la branche nommée. Si le document vient d’une autre branche ou d’un autre dépôt, les noms ne correspondent pas et le fournisseur refuse de donner des accès. Le programme ne peut alors pas déployer.

Si les conditions correspondent, le fournisseur cloud remet des identifiants temporaires. Ils permettent seulement les actions inscrites dans la règle, par exemple déposer des fichiers dans un emplacement précis. Ils expirent rapidement. Le programme les utilise pendant son exécution, puis ils deviennent inutilisables.

Cette protection ne garantit pas que le code autorisé est sans danger. Une personne pouvant modifier la branche autorisée peut aussi modifier le programme de déploiement. Il faut donc protéger cette branche, examiner les changements et vérifier régulièrement que les essais depuis une branche ou un dépôt non autorisé échouent bien.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.