PI-GROUPE
BlockchainKubernetesSécuritéGitOpsArchitectureIaCCloud

Hyperledger Fabric sur AKS : tracer la chaîne d'approvisionnement aéronautique avec Bevel et GitOps

Un constructeur et son fournisseur échangent des certificats de conformité sur un registre immuable. Voici la modélisation complète, du MSP au chaincode, automatisée par Hyperledger Bevel 1.3.0 sur des clusters AKS managés.

La traçabilité aéronautique pose un problème que les bases de données classiques résolvent mal. Quand un constructeur reçoit une pièce critique d'un fournisseur, le certificat de conformité qui l'accompagne doit rester vérifiable pendant trente ans — et aucune des deux parties ne doit pouvoir le modifier après coup.

Un registre distribué répond à cette exigence. Reste à le déployer sans transformer le projet en cauchemar opérationnel, car Hyperledger Fabric cumule deux difficultés : une topologie réseau complexe et une gestion cryptographique intransigeante.

L'architecture qui suit repose sur deux impératifs. L'immuabilité de l'infrastructure, obtenue par GitOps : aucun composant ne se configure à la main. Et la rigueur du Membership Service Provider, car une clé privée mal gérée compromet l'ensemble du réseau.

Le cas d'usage et sa modélisation

Deux organisations partagent un canal.

Org1 désigne le constructeur. Il émet des demandes de conformité et valide les certificats reçus. Org2 désigne le fournisseur. Il produit les pièces et émet les certificats correspondants.

Chaque organisation exploite son propre cluster AKS, dans son propre abonnement Azure. Aucune ne délègue sa cryptographie à l'autre — condition non négociable pour que le registre conserve sa valeur probante.

ComposantOrg1 — ConstructeurOrg2 — Fournisseur
Autorité de certificationca.constructeur.aeroca.fournisseur.aero
Pairs2 (endorsement + requête)2
Base d'étatCouchDBCouchDB
Service d'ordonnancement3 nœuds Raft, partagés—
ClusterAKS aks-constructeur-weuAKS aks-fournisseur-neu

Le service d'ordonnancement en Raft mérite une précision. Trois nœuds constituent le minimum pour tolérer une panne. Les placer tous chez une seule organisation crée un déséquilibre de gouvernance : celle-ci pourrait censurer des transactions. Dans notre modèle, Org1 en héberge deux et Org2 un seul — compromis acceptable quand le constructeur porte la responsabilité réglementaire.

Le MSP, cœur du sujet

Un MSP définit qui appartient à quelle organisation et avec quel rôle. Concrètement, il s'agit d'une arborescence de certificats X.509 dont la structure ne tolère aucune approximation.

text
crypto-config/
└── peerOrganizations/
    └── constructeur.aero/
        ├── ca/                       # clé privée de l'AC — jamais dans Git
        ├── msp/
        │   ├── cacerts/              # certificat racine
        │   ├── tlscacerts/           # racine TLS, distincte de la racine d'identité
        │   ├── config.yaml           # définition des unités organisationnelles
        │   └── admincerts/           # obsolète depuis Fabric 2.x
        ├── peers/
        │   └── peer0.constructeur.aero/
        │       ├── msp/              # identité du pair
        │       └── tls/              # certificat serveur TLS
        └── users/
            └── Admin@constructeur.aero/

Le fichier config.yaml porte la logique de rôles. Depuis Fabric 2.x, l'appartenance se détermine par unité organisationnelle plutôt que par liste de certificats administrateurs.

yaml
NodeOUs:
  Enable: true
  ClientOUIdentifier:
    Certificate: cacerts/ca-constructeur-aero.pem
    OrganizationalUnitIdentifier: client
  PeerOUIdentifier:
    Certificate: cacerts/ca-constructeur-aero.pem
    OrganizationalUnitIdentifier: peer
  AdminOUIdentifier:
    Certificate: cacerts/ca-constructeur-aero.pem
    OrganizationalUnitIdentifier: admin
  OrdererOUIdentifier:
    Certificate: cacerts/ca-constructeur-aero.pem
    OrganizationalUnitIdentifier: orderer

Un certificat portant OU=admin confère les droits d'administration. Révoquer un administrateur revient donc à révoquer son certificat auprès de l'autorité, puis à publier la liste de révocation — opération que beaucoup d'équipes découvrent le jour où elles en ont besoin.

Ne versionnez jamais les clés privées. Bevel génère la cryptographie puis la dépose dans HashiCorp Vault. Le dépôt Git ne contient que des références à des chemins Vault. Une clé d'autorité de certification exposée dans un historique Git impose la reconstruction complète du réseau.

L'automatisation par Bevel

Hyperledger Bevel fournit des playbooks Ansible et des chartes Helm qui déploient un réseau complet. La version 1.3.0 s'appuie sur le modèle d'opérateur et sur ArgoCD pour la réconciliation.

Le fichier de configuration réseau décrit l'intégralité de la topologie.

yaml
# network-aero.yaml — configuration Bevel 1.3.0
network:
  type: fabric
  version: "2.5.9"
  frontend: disabled

  env:
    type: "production"
    proxy: ambassador
    ambassadorPorts: { portRange: { from: 15010, to: 15043 } }
    retry_count: 40
    external_dns: enabled

  docker:
    url: "registry.aero.io/bevel"
    username: "lecteur-bevel"
    password: "{{ vault_registry_password }}"

  # Configuration du canal partagé
  channels:
    - channel_name: canal-conformite
      consortium: ConsortiumAeronautique
      channel_status: new
      chaincode:
        name: "certificat-conformite"
        version: "1.0"
        lang: "golang"
        repository:
          username: "ci-bevel"
          password: "{{ vault_git_token }}"
          url: "gitlab.aero.io/chaincode/certificat-conformite.git"
          branch: main
          path: "chaincode/"
        arguments: '"init"'
        endorsements: "AND('ConstructeurMSP.peer','FournisseurMSP.peer')"
      participants:
        - organization: constructeur
          type: creator
          org_status: new
          peers:
            - peer: peer0
              gossipAddress: peer0.constructeur-net:7051
              peerAddress: peer0.constructeur.aero:8443
        - organization: fournisseur
          type: joiner
          org_status: new

  organizations:
    - organization:
        name: constructeur
        type: peer
        external_url_suffix: constructeur.aero
        cloud_provider: azure
        aws: { access_key: "", secret_key: "" }
        k8s:
          region: westeurope
          context: "aks-constructeur-weu"
          config_file: "/opt/bevel/kubeconfig/constructeur.yaml"
        vault:
          url: "https://vault.constructeur.aero"
          root_token: "{{ vault_root_constructeur }}"
          secret_path: "secretsv2"
        gitops:
          git_protocol: "https"
          git_url: "https://gitlab.aero.io/gitops/reseau-conformite.git"
          branch: main
          release_dir: "platforms/hyperledger-fabric/releases/prod"
          chart_source: "platforms/hyperledger-fabric/charts"
          git_repo: "gitlab.aero.io/gitops/reseau-conformite.git"
          username: "bot-gitops"
          password: "{{ vault_git_token }}"
          email: "gitops@constructeur.aero"
        services:
          ca:
            name: ca
            subject: "/C=FR/ST=Ile-de-France/L=Versailles/O=Constructeur/CN=ca.constructeur.aero"
            type: ca
            grpc: { port: 7054 }
          peers:
            - peer:
                name: peer0
                type: anchor
                gossippeeraddress: peer0.constructeur-net:7051
                peerAddress: peer0.constructeur.aero:8443
                certificate: "/opt/bevel/certs/peer0.crt"
                grpc: { port: 7051 }
                couchdb: { port: 5984 }

La clé d'endorsements — AND('ConstructeurMSP.peer','FournisseurMSP.peer') — impose que toute transaction soit approuvée par les deux organisations. C'est elle qui donne sa valeur au registre : aucune partie ne peut inscrire un certificat unilatéralement.

Le chaincode, écrit pour durer

Le contrat gère trois opérations : émettre un certificat, le consulter, et tracer son historique. Voici le cœur, en Go avec l'API de contrat.

go
package main

import (
    "encoding/json"
    "fmt"
    "time"

    "github.com/hyperledger/fabric-contract-api-go/contractapi"
)

// CertificatConformite représente l'attestation liée à une pièce livrée.
type CertificatConformite struct {
    Reference     string    `json:"reference"`
    NumeroSerie   string    `json:"numeroSerie"`
    Fournisseur   string    `json:"fournisseur"`
    NormeAppliquee string   `json:"normeAppliquee"`   // EN 9100, NADCAP…
    DateEmission  time.Time `json:"dateEmission"`
    EmpreinteDoc  string    `json:"empreinteDoc"`      // SHA-256 du PDF signé
    Statut        string    `json:"statut"`            // emis | valide | conteste
}

type Contrat struct {
    contractapi.Contract
}

// Emettre inscrit un certificat. Seul le fournisseur y est habilité.
func (c *Contrat) Emettre(ctx contractapi.TransactionContextInterface,
    reference, numeroSerie, norme, empreinte string) error {

    // Contrôle du MSP appelant : la logique métier ne se délègue pas au réseau.
    mspID, err := ctx.GetClientIdentity().GetMSPID()
    if err != nil {
        return fmt.Errorf("identité illisible : %w", err)
    }
    if mspID != "FournisseurMSP" {
        return fmt.Errorf("seul le fournisseur émet un certificat, MSP reçu : %s", mspID)
    }

    existant, err := ctx.GetStub().GetState(reference)
    if err != nil {
        return fmt.Errorf("lecture du registre impossible : %w", err)
    }
    if existant != nil {
        return fmt.Errorf("le certificat %s existe déjà", reference)
    }

    // L'horodatage provient de la transaction, jamais de l'horloge du pair :
    // deux pairs doivent produire un résultat identique.
    horodatage, err := ctx.GetStub().GetTxTimestamp()
    if err != nil {
        return fmt.Errorf("horodatage indisponible : %w", err)
    }

    cert := CertificatConformite{
        Reference:      reference,
        NumeroSerie:    numeroSerie,
        Fournisseur:    mspID,
        NormeAppliquee: norme,
        DateEmission:   time.Unix(horodatage.Seconds, 0).UTC(),
        EmpreinteDoc:   empreinte,
        Statut:         "emis",
    }

    donnees, err := json.Marshal(cert)
    if err != nil {
        return fmt.Errorf("sérialisation impossible : %w", err)
    }

    // Événement consommable par les systèmes amont du constructeur.
    if err := ctx.GetStub().SetEvent("CertificatEmis", donnees); err != nil {
        return fmt.Errorf("émission de l'événement impossible : %w", err)
    }

    return ctx.GetStub().PutState(reference, donnees)
}

// Valider bascule le statut. Réservé au constructeur.
func (c *Contrat) Valider(ctx contractapi.TransactionContextInterface, reference string) error {
    mspID, _ := ctx.GetClientIdentity().GetMSPID()
    if mspID != "ConstructeurMSP" {
        return fmt.Errorf("seul le constructeur valide, MSP reçu : %s", mspID)
    }

    donnees, err := ctx.GetStub().GetState(reference)
    if err != nil || donnees == nil {
        return fmt.Errorf("certificat %s introuvable", reference)
    }

    var cert CertificatConformite
    if err := json.Unmarshal(donnees, &cert); err != nil {
        return err
    }
    if cert.Statut != "emis" {
        return fmt.Errorf("statut incompatible : %s", cert.Statut)
    }
    cert.Statut = "valide"

    maj, _ := json.Marshal(cert)
    return ctx.GetStub().PutState(reference, maj)
}

Deux points appellent l'attention.

L'horodatage provient de GetTxTimestamp() et non de time.Now(). Chaque pair exécute le chaincode indépendamment ; si deux pairs produisent des horodatages différents, leurs jeux de lecture-écriture divergent et la transaction échoue à la validation.

Le contrôle du MSP appelant s'effectue dans le code. La politique d'endorsement garantit qu'un nombre suffisant d'organisations approuve, pas que la bonne organisation initie.

L'exposition réseau entre clusters

Deux clusters AKS dans deux abonnements distincts doivent communiquer en gRPC avec TLS mutuel. Bevel s'appuie sur Ambassador, qui expose chaque composant sur un port dédié avec passage TLS transparent.

yaml
apiVersion: getambassador.io/v3alpha1
kind: TCPMapping
metadata:
  name: peer0-constructeur
  namespace: constructeur-net
spec:
  port: 8443
  service: peer0.constructeur-net:7051
---
apiVersion: getambassador.io/v3alpha1
kind: Host
metadata:
  name: peer0-constructeur
  namespace: constructeur-net
spec:
  hostname: peer0.constructeur.aero
  acmeProvider: { authority: none }        # certificats issus du MSP Fabric
  tlsSecret: { name: peer0-tls }
  requestPolicy:
    insecure: { action: Reject }

Le point sensible tient au DNS. Les pairs se joignent par nom, et ce nom doit se résoudre identiquement depuis les deux clusters. Une zone DNS publique gérée par external-dns résout le problème proprement ; un fichier hosts bricolé le reporte au premier incident.

La réconciliation GitOps

Bevel écrit les manifestes Helm dans le dépôt GitOps. ArgoCD les applique. Le déploiement devient ainsi reproductible et auditable.

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: fabric-constructeur
  namespace: argocd
spec:
  project: reseau-conformite
  source:
    repoURL: https://gitlab.aero.io/gitops/reseau-conformite.git
    targetRevision: main
    path: platforms/hyperledger-fabric/releases/prod/constructeur
  destination:
    server: https://aks-constructeur-weu.hcp.westeurope.azmk8s.io:443
    namespace: constructeur-net
  syncPolicy:
    automated: { prune: false, selfHeal: true }   # prune désactivé : un pair supprimé perd son registre
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true
    retry:
      limit: 5
      backoff: { duration: 30s, factor: 2, maxDuration: 10m }

Notez prune: false. Sur un réseau Fabric, la suppression automatique d'une ressource devenue absente du dépôt détruirait le volume persistant d'un pair — et son exemplaire du registre avec. La synchronisation reste automatique, la suppression reste manuelle.

Les écueils rencontrés

La rotation des certificats TLS. Les certificats émis par l'autorité Fabric expirent, souvent à un an. Sans procédure de renouvellement automatisée, le réseau s'arrête du jour au lendemain. Prévoyez un CronJob qui surveille les échéances et alerte trente jours avant.

La croissance de CouchDB. La base d'état conserve la dernière valeur de chaque clé, mais les index se multiplient. Sur un réseau actif, prévoyez de l'espace disque et une politique de compaction.

Le versionnement du chaincode. Depuis Fabric 2.x, le cycle de vie exige l'approbation de chaque organisation avant validation. Une mise à jour non coordonnée bloque le canal. Traitez chaque version de chaincode comme un changement d'interface de programmation : contrat de compatibilité, période de recouvrement, plan de retour arrière.

L'ordonnancement en Raft et la latence. Les nœuds d'ordonnancement échangent en permanence. Répartis entre deux régions Azure distantes, la latence dégrade le débit de transactions. Mesurez avant de décider de la répartition géographique.

Ce que cette architecture apporte

Un certificat émis par le fournisseur, validé par le constructeur, inscrit dans un registre que ni l'un ni l'autre ne peut réécrire. Chaque modification laisse une trace horodatée et signée. En cas de litige sur une pièce, l'historique complet se reconstitue par un simple appel GetHistoryForKey.

Le tout déployé par GitOps, donc reproductible : reconstruire le réseau sur un nouvel abonnement Azure se résume à provisionner les clusters et laisser ArgoCD converger.