Pour qu'un workflow GitHub Actions crée une machine, la redémarre ou la détruise, il faut qu'il s'authentifie auprès de l'API. Jusqu'ici, cela voulait dire une clé d'API rangée dans les secrets du dépôt : une valeur qui vit des mois, que toute personne capable de modifier un workflow peut faire sortir, et qu'il faut penser à remplacer quand quelqu'un quitte l'équipe.
L'API FFxF accepte maintenant les jetons OIDC. GitHub Actions, GitLab CI et les autres émetteurs OIDC signent pour chaque tâche un jeton de quelques minutes qui dit d'où elle vient : quel dépôt, quelle branche, quel workflow. Vous dites à FFxF quels jetons accepter et avec quels droits ; le dépôt ne contient plus aucun secret. Cet article montre le montage complet, avec les sorties réelles d'une démonstration.
1. Ce qui change
- Avec une clé, l'API sait qui vous êtes, pas qui l'appelle. Une copie de la clé fonctionne aussi bien depuis l'ordinateur d'un inconnu.
- Avec l'OIDC, chaque appel porte une identité signée par
GitHub :
repo:acme/app:ref:refs/heads/main, tel fichier de workflow, tel déclencheur. FFxF vérifie la signature, puis compare ces informations à vos règles. - Le jeton expire au bout de cinq minutes. Retrouvé dans un journal le lendemain, il ne sert plus à rien.
2. Déclarer GitHub comme fournisseur
Dans la console, sous Compte → OIDC, ajoutez un fournisseur
nommé « GitHub Actions » avec l'émetteur
https://token.actions.githubusercontent.com. FFxF lui attribue une
audience propre à votre compte, de la forme
ffxf-oidc-… : c'est la valeur pour laquelle vos workflows
demanderont leurs jetons.
Cette audience est ce qui relie un jeton à votre compte. GitHub délivre des jetons à n'importe quel dépôt pour l'audience de son choix ; un jeton demandé pour une autre valeur que la vôtre est refusé avant même la vérification de sa signature.
3. Une liaison : quels jetons, quels droits
Une liaison réunit des conditions sur le contenu du jeton et les portées qu'elle
accorde, les mêmes que celles des clés d'API. Pour un déploiement depuis la
branche main, avec un seul fichier de workflow autorisé :
sub=repo:acme/app:ref:refs/heads/main: le dépôt et la branche.workflow_ref=acme/app/.github/workflows/deploy.yml@refs/heads/main: le fichier de workflow. Un autre workflow du même dépôt, même surmain, n'obtient rien.- Les portées : lire, créer, agir sur et détruire des machines, gérer les clés SSH. Rien sur la facturation.
Les jokers sont permis (* pour une suite de caractères,
? pour un seul), avec un garde-fou : dans sub, le
propriétaire du dépôt doit être nommé avant tout joker. repo:acme/*
accepte tous les dépôts d'acme ; repo:* accepterait
tous les dépôts de GitHub, et la console le refuse.
Le refus vise aussi repo:acme*, sans barre oblique : ce motif
laisserait passer le compte acme-evil. Une seconde liaison, en
lecture seule et avec une date d'expiration, sert aux pull requests :
4. Le workflow
La tâche a besoin de la permission id-token: write. Elle demande un
jeton à GitHub pour votre audience, le masque dans le journal, puis s'en sert
comme d'une clé :
permissions:
contents: read
id-token: write
jobs:
deploy:
runs-on: ubuntu-latest
env:
FFXF_API: https://api.ffxf.net/v1
FFXF_OIDC_AUDIENCE: ffxf-oidc-… # copiée depuis la console
steps:
- name: Jeton FFxF
run: |
token=$(curl -sS -H "Authorization: Bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL&audience=$FFXF_OIDC_AUDIENCE" | jq -r .value)
echo "::add-mask::$token"
echo "FFXF_TOKEN=$token" >> "$GITHUB_ENV"
- name: Lister les machines
run: curl -sS "$FFXF_API/vms" -H "Authorization: Bearer $FFXF_TOKEN"
Aucun secret dans les réglages du dépôt : l'audience n'en est pas un, puisqu'elle
ne suffit pas à s'authentifier. Sur GitLab CI, c'est encore plus court : un bloc
id_tokens avec aud: ffxf-oidc-… place le jeton dans une
variable.
5. La démonstration
Nous avons fait tourner un workflow de ce genre sur notre environnement de test, qui exécute le même code d'authentification que l'API publique. Les sorties ci-dessous sont réelles ; seuls le nom du dépôt et l'audience sont abrégés. D'abord, ce que contient le jeton délivré par GitHub :
{
"iss": "https://token.actions.githubusercontent.com",
"aud": "ffxf-oidc-z8ma…",
"sub": "repo:acme/app:ref:refs/heads/demo-oidc",
"ref": "refs/heads/demo-oidc",
"workflow_ref": "acme/app/.github/workflows/oidc-demo.yml@refs/heads/demo-oidc",
"event_name": "push",
"lifetime_s": 300
}
Cinq minutes de validité. La liaison de la démonstration accordait
billing.read et vms.read à cette branche et à ce
fichier. Les deux lectures passent :
GET /v1/account → HTTP 200 {"currency":"CAD","billing_mode":"prepaid"}
GET /v1/vms → HTTP 200
Fixer le budget mensuel demande billing.write, que la liaison n'a pas :
PUT /v1/account/budget → HTTP 403
{"code":"insufficient_scope",
"message":"The OIDC binding is missing the scope required for this call.",
"details":{"required":["billing.write"]}}
Le même workflow poussé sur une autre branche, demo-oidc-autre,
obtient un jeton tout aussi valide, mais aucune liaison ne l'accepte :
GET /v1/account → HTTP 403
{"code":"oidc_claim_mismatch","message":"The token does not match an enabled OIDC binding."}
Enfin, un jeton demandé à GitHub pour une autre audience que celle du compte est refusé d'emblée, sans qu'aucun appel ne parte vers GitHub pour vérifier sa signature :
GET /v1/account → HTTP 401
{"code":"invalid_oidc_token","message":"The provided OIDC token is invalid."}
Ce dernier code est le même pour un jeton mal signé ou expiré : la réponse ne dit jamais si une audience ou une liaison existe dans un autre compte.
6. Ce que FFxF vérifie, dans l'ordre
- L'émetteur et l'audience désignent un fournisseur actif de votre compte.
- La signature, avec les clés publiques que GitHub publie, gardées une heure en cache. Seuls les algorithmes asymétriques sont acceptés.
- L'émetteur exact, l'expiration, la date d'émission et le
sub. - Les liaisons actives du fournisseur : la première dont toutes les conditions correspondent, qui n'a pas expiré et qui porte la portée de l'appel, l'emporte.
Un jeton accepté agit ensuite comme une clé de votre compte : même quota, même crédit, mêmes plafonds de budget. Les seaux de débit et l'idempotence sont comptés par liaison. Pour couper un accès, désactivez la liaison : l'appel suivant est refusé.
7. Et pour un agent IA
Le serveur MCP FFxF relaie l'en-tête
Authorization tel quel. Un agent lancé dans une tâche GitHub Actions
peut donc piloter vos machines avec le jeton OIDC de la tâche, dans les limites de
la liaison, sans qu'aucune clé ne lui soit confiée. Les liaisons, elles, ne se
créent que dans la console : ni une clé ni un jeton ne peut en élargir une.
Checklist
- Un fournisseur par émetteur, et son audience copiée dans le workflow.
- Une liaison par usage, avec
subetworkflow_refépinglés. - Les portées strictement nécessaires : la destruction pour la seule tâche qui nettoie.
id-token: writedans les permissions du workflow, et le jeton masqué dans le journal.- Une date d'expiration sur les liaisons d'essai.
- La clé d'API que la CI utilisait jusqu'ici, révoquée.
Les conditions, les formats de sub chez GitHub et GitLab, et tous les
codes de refus sont dans la documentation OIDC.