TL;DR : la sécurité d'un projet Go ne vit pas dans une revue de code. Elle vit dans le pipeline. Ta supply chain, c'est tout le code et tous les outils entre ton clavier et la prod. En 2026, c'est la porte d'entrée des attaques. Voici le pipeline étape par étape, avec les outils, et l'endroit où chacun casse.
Cet article est pour les équipes Go qui livrent en prod, et pour ceux qui achètent du DevSecOps. Pas pour la démo.
Ton code est peut-être propre. Ta chaîne d'approvisionnement, non.
La supply chain, c'est ta chaîne d'approvisionnement logicielle. Elle contient tes dépendances, tes images de base et tes outils de build. Les grosses attaques récentes ne sont pas passées par un bug dans ton code. Elles sont passées par cette chaîne.
Deux exemples ont marqué 2025. En mars, l'action GitHub tj-actions/changed-files est compromise (CVE-2025-30066). Les attaquants déplacent des tags de version vers un commit piégé. Chaque dépôt épinglé sur ce tag exécute le code malveillant. Résultat : des secrets déversés dans les logs publics de CI, sur environ 23 000 dépôts.
En septembre, c'est Shai-Hulud. Le premier ver auto-propagé de l'écosystème npm. Il s'exécute à l'installation d'un paquet. Il scanne l'environnement pour trouver des secrets, vole les tokens, puis republie des versions piégées tout seul.
Aucune de ces attaques n'était un bug chez la victime. Les deux sont entrées par une dépendance ou un outil de build. Go n'est pas npm. Mais Go télécharge du code et lance des outils, lui aussi. Le pipeline, c'est là que tu défends.
Le contexte : un vrai pipeline Go, pas un schéma
Je parle de deux choses réelles. Mon propre projet Go, et les pipelines que j'ai montés pour des clients.
Sur mon projet Go, voici ce qui tourne déjà. gitleaks scanne chaque commit avant qu'il parte. golangci-lint, avec staticcheck, bloque chaque pull request qui échoue. Les dépendances sont épinglées par empreinte, et un robot les met à jour. L'image finale est distroless et non-root. Le token de CI est en lecture seule.
Sur des pipelines clients, j'ai ajouté trois couches. Du SAST, qui lit le code sans l'exécuter (SonarQube). Du DAST, qui teste l'app en marche (OWASP ZAP). Et du scan d'image (Grype, Trivy). Avec une baisse mesurée du nombre de failles.
Cet article parcourt toute la chaîne, étape par étape. Certaines étapes tournent chez moi aujourd'hui. D'autres sont celles que je recommande pour fermer la chaîne. Je te dis à chaque fois laquelle.
Étape 1 : bloque les vulnérabilités connues avec govulncheck
govulncheck est l'outil officiel de l'équipe Go. Il compare tes dépendances et la bibliothèque standard à la base de failles Go.
Sa force tient à une idée. Il lit ton graphe d'appels. Il ne signale une faille que si ton code atteint vraiment la fonction vulnérable. Tu as donc beaucoup moins de fausses alertes qu'avec un scanner générique. Une ligne suffit en CI :
govulncheck ./...
Où ça casse. govulncheck ne connaît que les failles déjà publiées. Un zero-day passe. Un zero-day, c'est une faille encore inconnue des défenseurs. Et du code malveillant sans CVE passe aussi. Une CVE, c'est l'identifiant public d'une faille connue. On comble ce trou à l'étape 3.
Étape 2 : épingle tes dépendances, et laisse un robot les monter
Go épingle déjà tes dépendances par empreinte, dans go.sum. Si une version publiée change sous tes pieds, le build échoue. Garde -mod=readonly en CI, pour que rien ne modifie tes deps en silence.
Mais épinglé ne veut pas dire sûr. Tu peux très bien épingler du code malveillant. Il te faut donc des mises à jour gérées. Un robot ouvre des PR de dépendances chaque jour.
Chez moi, les mises à jour mineures passent toutes seules une fois la CI verte. Les versions majeures sont étiquetées et attendent un humain. Une montée de version majeure change trop de choses pour être aveugle.
Où ça casse. Un simple patch peut être piégé, comme dans le cas tj-actions. Auto-merger un patch malveillant est un vrai risque. Donc : garde peu de dépendances, et ne laisse jamais un major passer sans revue.
Étape 3 : va au-delà de la CVE, traque le comportement
Shai-Hulud n'avait pas de CVE au moment de l'installation. C'était juste du code qui s'exécute et vole des secrets. Un scanner de CVE ne voit rien. Tu ajoutes donc deux choses.
D'abord, du SAST sur ton propre code. Le SAST lit ton code sans le lancer et repère les motifs à risque. Trois outils reviennent souvent. Voici comment je les situe.
| Outil | Ce qu'il attrape | Go natif | Pour |
|---|---|---|---|
| gosec | motifs Go dangereux : creds en dur, crypto faible, exec shell, SQL concaténé | oui | un SAST Go rapide en CI |
| semgrep | règles multi-langages et politiques maison, appliquées partout | non | une règle d'équipe sur plusieurs repos |
| staticcheck | bugs et code mort, la correction avant la sécurité | oui | la qualité du code, déjà dans golangci-lint |
govulncheck n'est pas dans ce tableau. Il regarde les failles connues de tes deps, pas les motifs de ton code. Les deux sont complémentaires.
Ensuite, traite le code tiers comme suspect. Moins de dépendances. Lis le diff d'une nouvelle dépendance avant de l'ajouter. Préfère la bibliothèque standard. Go aide ici : un module Go ne lance pas de script à l'installation, contrairement à npm. Mais go generate, cgo et les build tags peuvent exécuter du code au build. La machine de build reste donc une cible.
Où ça casse. Le SAST a des faux positifs, et il ne voit pas la logique à l'exécution. Aucun outil ne lit dans la tête d'une dépendance. La vraie défense reste : moins de deps, et une revue humaine.
Étape 4 : génère un SBOM, parce que le CRA arrive
Un SBOM, c'est l'inventaire de tous les composants de ton build, avec leurs versions. Vois ça comme l'étiquette des ingrédients de ton logiciel.
Deux outils font le travail en Go : syft ou cyclonedx-gomod. Ils produisent un fichier au format CycloneDX ou SPDX. Ce sont les deux formats lisibles par une machine. Génère-le en CI, et attache-le à chaque release.
Pourquoi maintenant ? À cause du CRA, le règlement européen Cyber Resilience Act. À partir du 11 septembre 2026, un éditeur doit signaler une faille activement exploitée sous 24 heures. Tu ne tiens pas un délai de 24 heures sans savoir ce qu'il y a dans ton produit. Le SBOM est cette carte. L'obligation formelle de SBOM arrive avec l'application complète du texte en 2027. Mais tu en as besoin avant que l'horloge démarre.
Où ça casse. Un SBOM périmé, c'est du théâtre. Il ne sert que corrélé à un flux de failles à jour. Génère-le à chaque build, pas une fois par an.
Étape 5 : scanne l'image, pas juste le code
Ton binaire Go n'est pas tout l'artefact. Le conteneur embarque aussi une couche système. Scanne l'image finale. Trivy ou Grype la lisent et signalent les failles connues, côté paquets système comme côté binaire.
Réduis la cible d'abord. Un build multi-étapes compile le binaire, puis le copie dans une base minuscule. J'utilise une base distroless, non-root, avec un binaire statique. Pas de shell, pas de gestionnaire de paquets, presque rien à exploiter.
FROM golang:1.25-bookworm AS builder
# ... go mod download, puis :
RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /app ./cmd/server
FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /app /app
USER 65534:65534
Où ça casse. Un scanner ne connaît que sa base de données. Une image minimale réduit la surface, mais l'image de base vieillit. Reconstruis et rescanne souvent.
Étape 6 : sors les secrets du dépôt et du build
Shai-Hulud récoltait les secrets présents dans l'environnement. D'où la règle : un secret ne traîne jamais dans le dépôt, et ne finit jamais gravé dans une couche d'image.
gitleaks scanne chaque commit à la recherche de clés et de tokens. Chez moi, il tourne en hook de pre-commit. Le même hook empêche d'ajouter un fichier .env, .pem ou .key.
Les vrais secrets vivent dans un gestionnaire dédié, injectés au moment du déploiement. J'utilise Infisical. Un coffre managé fait aussi l'affaire.
Dans le build Docker, passe les secrets avec --mount=type=secret. Le secret est disponible pendant l'étape de build, mais il ne s'écrit jamais dans une couche.
Où ça casse. gitleaks attrape les motifs connus. Un secret déjà fuité avant le hook, ou un format nouveau, passe. Donc : renouvelle tes secrets dès qu'ils sont exposés.
Étape 7 : durcis GitHub Actions, ta chaîne la plus exposée
Ta CI détient tes clés et un accès en écriture. tj-actions l'a prouvé. Les attaquants ont déplacé un tag de version vers un commit piégé. Chaque dépôt épinglé sur ce tag a lancé la charge. Trois gestes limitent les dégâts.
Un. Le moindre privilège sur le token de CI. Mets contents: read par défaut, et n'ajoute write que là où un job en a besoin. Chez moi, les jobs de lint et de test sont en lecture seule.
permissions:
contents: read # lecture seule par défaut
id-token: write # seulement pour demander un token OIDC court
Deux. Épingle les actions tierces par empreinte de commit complète, pas par tag. Un tag se déplace, une empreinte non. Beaucoup d'équipes épinglent encore par tag majeur, et c'est un choix acceptable. Après tj-actions, épingle les actions tierces par SHA.
- uses: actions/checkout@a1b2c3d # empreinte complète (40 car.), pas @v4
Trois. Utilise OIDC pour t'authentifier au cloud. OIDC laisse un job prouver son identité et recevoir un token court, sans clé stockée. Un log volé ou un environnement fuité ne contient alors qu'un token déjà mort. C'est la réponse directe à tj-actions comme à Shai-Hulud.
Où ça casse. Le SHA-pin protège d'un tag déplacé, pas d'un mainteneur compromis qui publie une release que tu montes ensuite. La revue des montées de version reste nécessaire.
Étape 8 : signe ce que tu publies
Le dernier verrou. Une signature prouve qu'un artefact vient de ton pipeline, pas d'un attaquant.
cosign, du projet Sigstore, signe une image ou un fichier. Sans clé à gérer, la signature est liée à l'identité de ta CI. Tes utilisateurs la vérifient avant de lancer l'artefact. GitHub peut aussi attacher une provenance de build : un enregistrement signé du comment et du où de la construction.
cosign sign $IMAGE
cosign verify $IMAGE --certificate-identity ...
Où ça casse. Une signature prouve l'origine, pas l'innocence. Un artefact malveillant signé reste malveillant. Mais tu gagnes la traçabilité et la révocation. Tu sais quoi rappeler, et vite.
La checklist supply chain avant de livrer
Avant de livrer un service Go, parcours ta chaîne d'approvisionnement de bout en bout.
govulnchecktourne en CI et lit ton graphe d'appels- Dépendances épinglées par empreinte, build en
-mod=readonly - Un robot monte les deps : mineures automatiques, majeures revues par un humain
- Un SAST sur ton code (gosec ou semgrep), en plus de
staticcheck - Peu de dépendances, et le diff d'une nouvelle dép lu avant l'ajout
- Un SBOM généré à chaque build, au format CycloneDX ou SPDX
- L'image finale scannée (Trivy ou Grype), sur une base distroless non-root
- Secrets hors du dépôt, scannés par
gitleaks, injectés au déploiement - Secrets de build passés en
--mount=type=secret, jamais dans une couche - Token de CI en
contents: readpar défaut - Actions tierces épinglées par SHA, pas par tag
- OIDC pour le cloud, à la place des clés longue durée
- Artefacts signés avec
cosign, provenance de build attachée
Ce qu'il faut retenir
Une chaîne vaut son maillon le plus faible. Et ce maillon, c'est rarement ton code. C'est un tag déplacé, un patch auto-mergé, un secret dans un log.
Go te donne un vrai avantage. Des binaires statiques, une surface d'attaque minuscule, govulncheck qui lit ton graphe d'appels, aucun script à l'installation par défaut. Sers-t'en. Puis ferme la chaîne : épingle, scanne, inventorie, signe.
Le CRA n'est pas la raison de faire tout ça. C'est la date limite qui t'enlève ton excuse. Tu montes un pipeline Go et tu veux un second regard sur sa chaîne d'approvisionnement ? C'est exactement ce que je fais. Écris-moi. Livre ton service. Pas la faille avec.