Skip to content

release: remédiation de l'audit de sécurité et de qualité en production (govulncheck 42→0) - #22

Merged
AbrahamOP merged 9 commits into
mainfrom
dev
Aug 9, 2026
Merged

AbrahamOP merged 9 commits into
mainfrom
dev

Conversation

@AbrahamOP

Copy link
Copy Markdown
Collaborator

Passage en production de la remédiation de l'audit du 2026-08-04, déjà fusionnée dans dev
(PR #21) et vérifiée sur l'instance DEV déployée (/readyz → {"status":"ready","database":"ok"},
donc migrations appliquées sur une base existante).

Avant Après
govulncheck — vulnérabilités appelées 42 0
Couverture de tests 14,3 % 26,5 %

Les défauts bloquants corrigés

  • Aucun compte MFA ne pouvait se connecter — le template rendait un mot de passe vide au second
    POST, l'utilisateur récoltait « Identifiants invalides » et un compteur de rate-limit. État de
    pré-authentification côté serveur, plus aucun credential dans le HTML.
  • Le code d'une PR de fork s'exécutait sur le runner self-hosted (dépôt public + go test).
    Les jobs de PR basculent sur des runners GitHub jetables. Injection shell via le titre de PR corrigée.
  • Le verdict N3 mentait : sans healthcheck configuré — le cas de toutes les cibles
    auto-découvertes — le moteur rendait « l'application répond » alors que seul le boot était prouvé.
  • La commande d'installation du canal ne marchait pas chez un tiers : curl récupérait la page
    de login et sudo bash l'exécutait. Route publique + jeton à usage unique.
  • Aucune action privilégiée n'était tracée — shell root, déploiement de clé, playbook,
    désactivation MFA ne laissaient rien dans le journal d'audit.
  • XSS stockée dans la vue Wazuh et Ansible contournait l'épinglage TOFU du reste du produit.

Plus : rate-limiter non contournable par en-tête forgé, révocation de session, RBAC du catalogue
d'applications, SSRF du health-check fermée, migrations versionnées bloquantes, rétention réellement
appliquée derrière un interrupteur explicite, /healthz et /readyz, certificat TLS persisté,
secrets _FILE, durcissement des conteneurs, thème clair réparé, 31 alert() remplacés,
modales accessibles.

Réconciliation avec main

main avait divergé de 18 commits jamais redescendus dans dev, dont plusieurs faisaient la même
chose que la remédiation en plus récent. Fusionner sans arbitrer aurait fait régresser ces mises
à jour. Ce qui a été tranché, commit 350f4e7 :

Sujet Décision
alpine Version de main (3.24) retenue, mais épinglée par digest comme le reste du fichier
Dépendances Go Versions de main retenues (x/crypto v0.54.0, chi v5.3.1 — plus récentes que les miennes)
toolchain go1.25.12 Conservé : c'est ce qui rend cohérents le builder du Dockerfile et le toolchain local, et ce qui solde les vulnérabilités stdlib
Actions Versions de main (checkout v7, setup-go v7, qemu/buildx/login v4, metadata v6, build-push v7) ré-épinglées par SHA
notify-pr Les deux apports conservés : la garde de main sur un DISCORD_WEBHOOK vide (PR #18) et la désinfection anti-injection des valeurs d'auteur de PR

Ce qui se déclenche au merge

deploy-prod → release (bump sémantique + tag + release GitHub) → publish (image multi-arch).

Deux gestes manuels ensuite :

  1. Rendre public le paquet ghcr.io/goacloud/goacore. Un paquet créé par Actions naît privé, et
    le docker pull anonyme du README en dépend. C'est la première publication sous l'organisation :
    l'image change d'espace de nommage suite au transfert du dépôt.
  2. Supprimer l'ancien paquet ghcr.io/abrahamop/goacore, figé sur 0.7.7 et désormais rattaché à
    aucun dépôt.

Vérification

go build, go vet, gofmt, go test ./... -race et govulncheck (0 vulnérabilité) verts après
réconciliation. Contrairement à la PR vers dev, celle-ci déclenche la CI complète — build,
détecteur de course, migrations sur MySQL réel, validation du helper goabackup.

Réserve de relecture

15 000 lignes écrites en grande partie par des agents, sur un produit destiné à des clients, et le
prochain merge part en production. Le morceau à relire en priorité reste
internal/services/backup.go : la rotation des archives est la seule opération destructive
ajoutée. Elle est désarmée par défaut — un interrupteur retention_enabled que la migration laisse
à faux sur tout l'existant — et un test vérifie qu'une installation existante ne supprime rien.

Claude Code and others added 9 commits August 6, 2026 01:14
Corrige les défauts relevés par l'audit du 2026-08-04 (9 dimensions,
contre-expertise adversariale). 12 lots, périmètres de fichiers disjoints.

Bloquants
- MFA: la connexion à deux étapes était cassée pour TOUS les comptes MFA.
  Le template rendait {{.Password}}, clé absente de la map de données, donc
  un mot de passe vide au second POST -> "Identifiants invalides" + compteur
  de rate-limit. Remplacé par un état de pré-authentification côté serveur
  (mfa_pending_user + expiration 5 min), sans credential dans le HTML.
- CI: les PR d'un dépôt public s'exécutaient sur le runner self-hosted
  (go test = exécution de code arbitraire). Bascule sur ubuntu-latest pour
  les PR. Injection shell via le titre de PR corrigée (passage par env).
- Publication GHCR enchaînée à la release (l'image latest avait 4 versions
  de retard sur le dépôt).
- Restauration N3: le verdict "application répond" était rendu sans exécuter
  de healthcheck. Le niveau prouvé est désormais calculé avant restauration.
- Canal GoaBackup: la commande d'installation téléchargeait la page de login
  chez un tiers. Route publique + jeton à usage unique.
- Audit: aucune action privilégiée n'était tracée (shell root, clés SSH,
  playbooks, MFA). Middleware AuditTrail + appels explicites.
- XSS stockée dans la vue Wazuh (nom d'agent, IP, OS non échappés).
- Ansible contournait l'épinglage TOFU appliqué par le reste du produit.

Sécurité
- Rate-limiter: X-Forwarded-For n'est plus cru sans TRUSTED_PROXIES; purge
  des entrées.
- Révocation de session par session_epoch; secours MFA par reset admin.
- RBAC: les mutations du catalogue d'applications passent en AdminOnly.
- SSRF du worker de health-check fermée au niveau du dialer.
- /setup: limitation de débit et création de compte atomique.

Données
- Migrations versionnées, erreurs réelles distinguées par code MySQL,
  Migrate() retourne une erreur et peut faire échouer le démarrage.
- retention_count effectivement appliqué; rpo_hours configurable;
  l'archive restaurée est enregistrée (preuve auditable).

Frontend
- 497 classes de palette brute -> tokens sémantiques (thème clair illisible).
- Config Tailwind <alpha-value>: 61 utilitaires d'opacité étaient omis du
  build. CSS régénéré et compilé dans l'image.
- 31 alert() -> composant toast partagé; modales accessibles; mobile.

Exploitabilité
- /healthz et /readyz; WriteTimeout neutralisé sur les endpoints streamés;
  certificat TLS persisté avec SAN configurables; support des secrets _FILE;
  durcissement des conteneurs.

Dépendances
- x/crypto v0.31.0 -> v0.52.0 (6 vulns, dont GO-2026-5020 atteignable depuis
  la console SSH), chi v5.2.1 -> v5.2.2.
- govulncheck: 42 -> 26 vulnérabilités appelées, plus aucune hors stdlib.

Couverture de tests: 14,3% -> 24,1%. go build, go vet, gofmt et
go test -race passent sur l'intégralité du dépôt.
Le premier commit a été relu par 5 relecteurs adversariaux (5 axes, 18
contre-expertises). 47 défauts retenus, dont 2 critiques : ils venaient
tous du même angle mort — du code correct JAMAIS CÂBLÉ, parce que
cmd/server/main.go et le routeur n'appartenaient à aucun périmètre.

Critiques
- Le canal Proxmox était mort : SetDefaultChannelHostKeys n'était appelé
  nulle part, donc la vérification TOFU fail-closed refusait toute
  connexion — tests N1/N2/N3, push off-site, healthcheck et sonde disque
  compris. Idem SetDefaultHostKeyStore pour l'exécution Ansible manuelle.
- L'image ne se construisait plus : le builder était épinglé sur Go 1.23.12
  (GOTOOLCHAIN=local) face à un go.mod monté à 1.25.0 par un autre lot.
  Builder repris sur golang:1.25-alpine (1.25.12) et directive `toolchain`
  ajoutée pour que le local, la CI et l'image parlent de la même version.

Câblage manquant
- L'erreur de Migrate() était jetée : le refus de démarrer sur schéma
  cassé était du code mort.
- BackupService.Wait() n'était appelé nulle part : la fuite de sandbox
  qu'il corrige subsistait. Drainé à l'arrêt, budget dédié.
- {{.InstallCommand}}/{{.InstallerSHA}} n'étaient utilisés par aucun
  template : la commande affichée restait un curl -k sans vérification
  d'empreinte, toute la machinerie sha256 était morte à l'écran.
- Le worker SOAR ignorait GetRecentAlertsWindow : le curseur dépassait
  encore la troncature. Clé de dédup passée à l'ID d'alerte (l'ancienne
  collisionnait pendant un pic).
- retention_enabled n'était exposé ni par le handler ni par la vue : le
  champ « conserver N archives » était devenu inerte, l'UI mentait.
- Les briques d'épinglage TOFU n'avaient aucun point d'entrée : trois
  routes Admin (scan/pin/delete) + parcours dans l'écran Clés SSH.
- TRUSTED_PROXIES n'était dans aucun compose ni .env.example.

Régressions introduites par la remédiation
- /api/mfa/verify contournait le durcissement de /api/mfa/disable : une
  session volée pouvait remplacer le second facteur au lieu de l'enlever.
- Le bouton « Désactiver 2FA » envoyait une requête sans corps -> 400.
- session_epoch n'était pas incrémenté au changement de mot de passe.
- La rotation de rétention s'armait rétroactivement sur les installations
  existantes (défaut de schéma à 3) et purgeait des archives que GoaCore
  n'a pas produites. Neutralisée derrière un interrupteur explicite.
- Le TOFU Ansible n'avait aucun chemin d'amorçage : toutes les
  planifications existantes cessaient de fonctionner.
- Le certificat auto-signé était émis en CA, avec sa clé privée dans un
  volume, et la doc invitait à l'importer dans le magasin de confiance.

Aussi : healthcheck MySQL compatible _FILE, HEALTHCHECK conteneur sur
/readyz, install/backup.sh (intégrité avant restauration, plancher de
rétention, dump en 0600), Échap ne jette plus une saisie non enregistrée,
palette Ctrl+K dans la pile de modales partagée, dernières couleurs brutes
migrées, job CI qui exerce enfin les migrations sur un vrai MySQL.

govulncheck : 42 -> 0 vulnérabilité appelée. L'étape CI devient bloquante.
Couverture : 14,3% -> 26,5%. build, vet, gofmt et go test -race verts.
…blic

Le nettoyage du Jalon 0 avait purgé l'infrastructure personnelle de l'auteur
du dépôt public ; les agents de remédiation l'avaient réintroduite dans
10 fichiers (29 occurrences), sans que le build ni les tests ne s'en
plaignent — un dépôt public ne fait pas d'erreur de compilation sur une
fuite de cartographie réseau.

- Exemples de documentation (.env.example, config.go, ratelimit.go, tls.go,
  ssh_keys.html) : plages RFC5737 192.0.2.0/24, comme le reste du dépôt.
- Tests : bascule sur 172.16.0.0/12, qui reste du RFC1918 — la sémantique
  « plage privée » des tests SSRF et TRUSTED_PROXIES en dépend — mais hors
  du plan d'adressage réel.
- docker-compose-dev.yml : TLS_HOSTS sans valeur par défaut, en-tête sans
  nom de domaine.
- Fixture de test : grafana.example.com.

Restent les 4 occurrences de ci-deploy.yml, déjà présentes sur origin/dev :
ce sont les cibles de déploiement de l'auteur, pas une régression de ce
diff. Elles sont désormais surchargeables par les variables de dépôt
DEV_PROBE_URL / PROD_PROBE_URL.
Le transfert AbrahamOP/GoaCore -> GoaCloud/GoaCore a détaché le paquet GHCR :
`goacore` reste la propriété du compte personnel et son champ `repository`
est désormais null (vérifié via l'API). Or les deux workflows codaient en dur
`ghcr.io/abrahamop/goacore` : le GITHUB_TOKEN d'un dépôt de l'organisation
n'a pas le droit d'écrire dans un paquet d'un compte personnel, donc la
prochaine release aurait échoué à publier.

- Image renommée en `ghcr.io/goacloud/goacore` (workflows, install/docker-compose.yml,
  README, ROADMAP) : l'espace de nommage suit le propriétaire du dépôt, et
  GITHUB_TOKEN retrouve ses droits sans jeton personnel à long terme.
- URL du dépôt mises à jour vers GoaCloud/GoaCore dans la documentation.

L'ancien paquet `ghcr.io/abrahamop/goacore` reste en place, figé sur 0.7.7 —
il n'a jamais été publié au-delà (cf. audit). À supprimer une fois la première
image publiée sous l'organisation.
Le transfert AbrahamOP/GoaCore -> GoaCloud/GoaCore a détaché le paquet GHCR :
`goacore` reste la propriété du compte personnel et son champ `repository` est
désormais null. Or publish-image.yml codait en dur `ghcr.io/abrahamop/goacore`,
et le GITHUB_TOKEN d'un dépôt d'organisation n'a pas le droit d'écrire dans le
paquet d'un compte personnel : la prochaine release aurait échoué à publier.

- Image renommée en `ghcr.io/goacloud/goacore` : l'espace de nommage suit le
  propriétaire du dépôt, GITHUB_TOKEN retrouve ses droits sans jeton personnel.
- URL du dépôt mises à jour (README, ROADMAP, install/docker-compose.yml,
  package.json). Les liens raw.githubusercontent.com ne suivent pas la
  redirection de transfert, donc ils devaient être corrigés.

Correctif isolé, extrait de la branche fix/audit-2026-08 pour ne pas laisser la
publication cassée en attendant la revue du reste.
# Conflicts:
#	.github/workflows/publish-image.yml
#	install/docker-compose.yml
fix: remédiation de l'audit de sécurité et de qualité (govulncheck 42→0, couverture 14→26 %)
`go test -race` exige cgo, donc un compilateur C. Le runner self-hosted n'en a
pas (ni gcc ni cc sur le CT), CGO_ENABLED retombe donc à 0 et l'étape échoue
avant même de lancer un test : « -race requires cgo ». C'est ce qui a fait
rougir la CI au merge de la remédiation, sans qu'aucune data race soit en cause.

Plutôt que d'installer une chaîne C sur le CT du runner pour un job qui n'a
besoin d'aucun accès au LAN, l'étape devient un job `race` sur ubuntu-latest,
où gcc est fourni — même raisonnement que pour le job de migrations MySQL.

Les déploiements et la release en dépendent (`needs: [build, race, helper,
db-tests]`) : une data race doit bloquer la mise en production, pas seulement
teinter un log.
`main` avait divergé de 18 commits (Dependabot + correctifs CI) jamais
redescendus dans `dev`. Plusieurs faisaient la même chose que la remédiation,
en plus récent : fusionner sans arbitrer aurait fait REGRESSER leurs mises à
jour. Arbitrages :

- Dockerfile : alpine 3.21 (moi) -> 3.24 (main), mais épinglé par DIGEST comme
  le reste du fichier. On prend la version de main sans perdre la
  reproductibilité du build.
- go.mod : les dépendances de main sont plus récentes (x/crypto v0.54.0 contre
  v0.52.0, chi v5.3.1 contre v5.2.2) et sont retenues telles quelles. Seule la
  directive `toolchain go1.25.12` est conservée de mon côté : elle est ce qui
  rend cohérents le builder du Dockerfile et le toolchain local, et ce qui
  solde les vulnérabilités de la bibliothèque standard.
- Workflows : versions d'actions de main (checkout v7, setup-go v7, qemu/buildx/
  login v4, metadata v6, build-push v7) RÉ-ÉPINGLÉES par SHA. Un tag d'action
  reste mouvant, et ces actions voient le GITHUB_TOKEN.
- notify-pr : les deux côtés corrigeaient le même bloc pour des raisons
  différentes. Garde de main sur un DISCORD_WEBHOOK vide (les PR Dependabot
  utilisent un autre magasin de secrets, `curl -sf` sortait en 3 et faisait
  échouer PR Check) ET désinfection des valeurs contrôlées par l'auteur de la
  PR (anti-injection). La garde passe en premier : inutile d'assainir pour ne
  rien envoyer.

Vérifié après réconciliation : go build, go vet, gofmt, go test -race et
govulncheck (0 vulnérabilité) tous verts.
@AbrahamOP
AbrahamOP merged commit ccb0cf8 into main Aug 9, 2026
19 checks passed
@AbrahamOP
AbrahamOP deleted the dev branch August 9, 2026 13:41
AbrahamOP pushed a commit that referenced this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant