A propos Compétences Expérience Services Blog Contact

Sécuriser la supply chain d'un projet Go : le pipeline qui tient en 2026 Securing a Go Supply Chain: The Pipeline That Holds in 2026

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.

OutilCe qu'il attrapeGo natifPour
gosecmotifs Go dangereux : creds en dur, crypto faible, exec shell, SQL concaténéouiun SAST Go rapide en CI
semgreprègles multi-langages et politiques maison, appliquées partoutnonune règle d'équipe sur plusieurs repos
staticcheckbugs et code mort, la correction avant la sécuritéouila 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.

  • govulncheck tourne 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: read par 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.

TL;DR: a Go project's security does not live in a code review. It lives in the pipeline. Your supply chain is all the code and tools between your keyboard and production. In 2026, it is the front door for attacks. Here is the pipeline step by step, with the tools, and where each one breaks.

This article is for Go teams that ship to production, and for people who buy DevSecOps. Not for a demo.

Your code may be clean. Your supply chain is not.

Your supply chain is your software supply chain. It holds your dependencies, your base images and your build tools. The big recent attacks did not come through a bug in your code. They came through that chain.

Two examples marked 2025. In March, the GitHub Action tj-actions/changed-files was compromised (CVE-2025-30066). The attackers moved version tags to a poisoned commit. Every repo pinned to that tag ran the malicious code. The result: secrets dumped into public CI logs, across about 23,000 repositories.

In September, it was Shai-Hulud. The first self-propagating worm in the npm ecosystem. It runs when a package is installed. It scans the environment for secrets, steals tokens, then republishes poisoned versions on its own.

Neither attack was a bug at the victim's site. Both entered through a dependency or a build tool. Go is not npm. But Go pulls code and runs tools too. The pipeline is where you defend.

The setup: a real Go pipeline, not a diagram

I am talking about two real things. My own Go project, and the pipelines I have built for clients.

On my own Go project, here is what already runs. gitleaks scans every commit before it leaves. golangci-lint, with staticcheck, blocks any pull request that fails. Dependencies are pinned by hash, and a bot updates them. The final image is distroless and non-root. The CI token is read-only.

On client pipelines, I added three layers. SAST, which reads code without running it (SonarQube). DAST, which tests the running app (OWASP ZAP). And image scanning (Grype, Trivy). With a measured drop in findings.

This article walks the whole chain, stage by stage. Some stages run on my side today. Others are the ones I recommend to close the chain. I tell you which is which each time.

Step 1: block known vulnerabilities with govulncheck

govulncheck is the official tool from the Go team. It compares your dependencies and the standard library against the Go vulnerability database.

Its strength is one idea. It reads your call graph. It only reports a vulnerability if your code actually reaches the vulnerable function. So you get far fewer false alarms than with a generic scanner. One line is enough in CI:

govulncheck ./...

Where it breaks. govulncheck only knows about published flaws. A zero-day slips through. A zero-day is a flaw still unknown to defenders. And malicious code with no CVE slips through too. A CVE is the public ID of a known flaw. Step 3 closes that gap.

Step 2: pin your dependencies, and let a bot raise them

Go already pins your dependencies by hash, in go.sum. If a published version changes under you, the build fails. Keep -mod=readonly in CI, so nothing edits your deps silently.

But pinned does not mean safe. You can happily pin malware. So you also need managed updates. A bot opens dependency PRs every day.

On my side, minor updates merge on their own once CI is green. Major versions get labeled and wait for a human. A major bump changes too much to be blind.

Where it breaks. A single patch can be poisoned, as in the tj-actions case. Auto-merging a malicious patch is a real risk. So: keep few dependencies, and never let a major through without review.

Step 3: go beyond the CVE, hunt behavior

Shai-Hulud had no CVE at install time. It was just code that runs and steals secrets. A CVE scanner sees nothing. So you add two things.

First, SAST on your own code. SAST reads your code without running it and flags risky patterns. Three tools come up often. Here is how I place them.

ToolWhat it catchesGo-nativeUse it for
gosecrisky Go patterns: hardcoded creds, weak crypto, shell exec, concatenated SQLyesa fast Go SAST in CI
semgrepmulti-language rules and custom policies, applied everywherenoone team rule across many repos
staticcheckbugs and dead code, correctness before securityyescode quality, already in golangci-lint

govulncheck is not in this table. It looks at known flaws in your deps, not at patterns in your code. The two are complementary.

Second, treat third-party code as suspect. Fewer dependencies. Read the diff of a new dependency before you add it. Prefer the standard library. Go helps here: a Go module does not run an install script, unlike npm. But go generate, cgo and build tags can run code at build time. So the build machine is still a target.

Where it breaks. SAST has false positives, and it cannot see runtime logic. No tool reads a dependency's mind. The real defense stays: fewer deps, and human review.

Step 4: generate an SBOM, because the CRA is coming

An SBOM is the inventory of every component in your build, with versions. Think of it as the ingredient label of your software.

Two tools do the job in Go: syft or cyclonedx-gomod. They produce a file in CycloneDX or SPDX format. Those are the two machine-readable formats. Generate it in CI, and attach it to every release.

Why now? Because of the CRA, the EU Cyber Resilience Act. From 11 September 2026, a manufacturer must report an actively exploited vulnerability within 24 hours. You cannot hold a 24-hour clock without knowing what is in your product. The SBOM is that map. The formal SBOM duty arrives with full application of the text in 2027. But you need it before the clock starts.

Where it breaks. A stale SBOM is theater. It only helps when correlated with a live vulnerability feed. Generate it on every build, not once a year.

Step 5: scan the image, not just the code

Your Go binary is not the whole artifact. The container ships an OS layer too. Scan the final image. Trivy or Grype read it and flag known flaws, in system packages and in the binary alike.

Shrink the target first. A multi-stage build compiles the binary, then copies it into a tiny base. I use a distroless base, non-root, with a static binary. No shell, no package manager, almost nothing to exploit.

FROM golang:1.25-bookworm AS builder
# ... go mod download, then:
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

Where it breaks. A scanner only knows its database. A minimal image cuts the surface, but the base image ages. Rebuild and rescan often.

Step 6: get secrets out of the repo and the build

Shai-Hulud harvested the secrets sitting in the environment. Hence the rule: a secret never sits in the repo, and never bakes into an image layer.

gitleaks scans every commit for keys and tokens. On my side, it runs as a pre-commit hook. The same hook blocks any .env, .pem or .key file from being added.

The real secrets live in a dedicated manager, injected at deploy time. I use Infisical. A managed vault works too.

In the Docker build, pass secrets with --mount=type=secret. The secret is available during the build step, but it never writes to a layer.

Where it breaks. gitleaks catches known patterns. A secret leaked before the hook, or a new format, slips through. So: rotate your secrets as soon as they are exposed.

Step 7: harden GitHub Actions, your most exposed link

Your CI holds your keys and write access. tj-actions proved it. The attackers moved a version tag to a poisoned commit. Every repo pinned to that tag ran the payload. Three moves limit the damage.

One. Least privilege on the CI token. Set contents: read by default, and add write only where a job needs it. On my side, the lint and test jobs are read-only.

permissions:
  contents: read      # read-only by default
  id-token: write     # only to request a short OIDC token

Two. Pin third-party actions by full commit hash, not by tag. A tag can move, a hash cannot. Many teams still pin by major tag, and that is a fair default. After tj-actions, pin third-party actions by SHA.

- uses: actions/checkout@a1b2c3d   # full hash (40 chars), not @v4

Three. Use OIDC to authenticate to the cloud. OIDC lets a job prove its identity and receive a short token, with no stored key. A stolen log or a leaked environment then holds a token that is already dead. That is the direct answer to tj-actions and to Shai-Hulud.

Where it breaks. SHA pinning protects against a moved tag, not against a compromised maintainer who publishes a release you then bump to. Reviewing version bumps is still needed.

Step 8: sign what you ship

The last lock. A signature proves an artifact comes from your pipeline, not from an attacker.

cosign, from the Sigstore project, signs an image or a file. With no key to manage, the signature ties to your CI identity. Your users verify it before they run the artifact. GitHub can also attach build provenance: a signed record of how and where it was built.

cosign sign   $IMAGE
cosign verify $IMAGE --certificate-identity ...

Where it breaks. A signature proves origin, not innocence. A signed malicious artifact is still malicious. But you gain traceability and revocation. You know what to recall, and fast.

The supply chain checklist before you ship

Before you ship a Go service, walk your supply chain end to end.

  • govulncheck runs in CI and reads your call graph
  • Dependencies pinned by hash, build in -mod=readonly
  • A bot raises deps: minor automatic, major reviewed by a human
  • A SAST on your code (gosec or semgrep), on top of staticcheck
  • Few dependencies, and a new dep's diff read before adding it
  • An SBOM generated on every build, in CycloneDX or SPDX format
  • The final image scanned (Trivy or Grype), on a distroless non-root base
  • Secrets out of the repo, scanned by gitleaks, injected at deploy
  • Build secrets passed via --mount=type=secret, never in a layer
  • CI token set to contents: read by default
  • Third-party actions pinned by SHA, not by tag
  • OIDC for the cloud, instead of long-lived keys
  • Artifacts signed with cosign, build provenance attached

What to remember

A chain is only as strong as its weakest link. And that link is rarely your code. It is a moved tag, an auto-merged patch, a secret in a log.

Go gives you a real edge. Static binaries, a tiny attack surface, govulncheck reading your call graph, no install scripts by default. Use it. Then close the chain: pin, scan, inventory, sign.

The CRA is not the reason to do this. It is the deadline that removes your excuse. Are you building a Go pipeline and want a second pair of eyes on its supply chain? That is exactly what I do. Get in touch. Ship your service. Not the breach with it.