PI-GROUPE
BlockchainKubernetesSecurityGitOpsArchitectureIaCCloud

Hyperledger Fabric on AKS: tracing the aerospace supply chain with Bevel and GitOps

A manufacturer and its supplier exchange conformity certificates on an immutable ledger. Here is the complete model, from MSP to chaincode, automated by Hyperledger Bevel 1.3.0 on managed AKS clusters.

Aerospace traceability poses a problem classic databases handle badly. When a manufacturer receives a critical part from a supplier, the conformity certificate that comes with it must stay verifiable for thirty years — and neither party may alter it after the fact.

A distributed ledger answers that requirement. What remains is deploying it without turning the project into an operational nightmare, because Hyperledger Fabric combines two difficulties: a complex network topology and uncompromising cryptographic management.

The architecture that follows rests on two imperatives. Infrastructure immutability, obtained through GitOps: no component gets configured by hand. And rigour in the Membership Service Provider, because one mishandled private key compromises the whole network.

The use case and its model

Two organisations share a channel.

Org1 is the manufacturer. It issues conformity requests and approves the certificates it receives. Org2 is the supplier. It produces the parts and issues the matching certificates.

Each organisation runs its own AKS cluster, in its own Azure subscription. Neither delegates its cryptography to the other — a non-negotiable condition for the ledger to keep its evidential value.

ComponentOrg1 — ManufacturerOrg2 — Supplier
Certificate authorityca.manufacturer.aeroca.supplier.aero
Peers2 (endorsement + query)2
State databaseCouchDBCouchDB
Ordering service3 Raft nodes, shared—
ClusterAKS aks-manufacturer-weuAKS aks-supplier-neu

The Raft ordering service deserves a clarification. Three nodes are the minimum to tolerate one failure. Placing them all at a single organisation creates a governance imbalance: that party could censor transactions. In our model Org1 hosts two and Org2 one — an acceptable compromise when the manufacturer carries the regulatory responsibility.

The MSP, the heart of the matter

An MSP defines who belongs to which organisation and in what role. Concretely, it is a tree of X.509 certificates whose structure tolerates no approximation.

text
crypto-config/
└── peerOrganizations/
    └── manufacturer.aero/
        ├── ca/                       # CA private key — never in Git
        ├── msp/
        │   ├── cacerts/              # certificate racine
        │   ├── tlscacerts/           # TLS root, separate from the identity root
        │   ├── config.yaml           # organisational unit definitions
        │   └── admincerts/           # obsolète depuis Fabric 2.x
        ├── peers/
        │   └── peer0.manufacturer.aero/
        │       ├── msp/              # peer identity
        │       └── tls/              # certificate serveur TLS
        └── users/
            └── Admin@manufacturer.aero/

The config.yaml file carries the role logic. Since Fabric 2.x, membership is determined by organisational unit rather than by a list of administrator certificates.

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

A certificate carrying OU=admin grants administration rights. Revoking an administrator therefore means revoking their certificate at the authority, then publishing the revocation list — an operation many teams discover on the day they need it.

Never version private keys. Bevel generates the cryptographic material then deposits it in HashiCorp Vault. The Git repository holds nothing but references to Vault paths. A certificate-authority key exposed in Git history forces a complete rebuild of the network.

Automation with Bevel

Hyperledger Bevel provides Ansible playbooks and Helm charts that deploy a complete network. Version 1.3.0 builds on the operator pattern and on Argo CD for reconciliation.

The network configuration file describes the entire topology.

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 }}"

  # Shared channel configuration
  channels:
    - channel_name: channel-conformity
      consortium: ConsortiumAeronautique
      channel_status: new
      chaincode:
        name: "certificate-conformity"
        version: "1.0"
        lang: "golang"
        repository:
          username: "ci-bevel"
          password: "{{ vault_git_token }}"
          url: "gitlab.aero.io/chaincode/certificate-conformity.git"
          branch: main
          path: "chaincode/"
        arguments: '"init"'
        endorsements: "AND('ManufacturerMSP.peer','SupplierMSP.peer')"
      participants:
        - organization: manufacturer
          type: creator
          org_status: new
          peers:
            - peer: peer0
              gossipAddress: peer0.manufacturer-net:7051
              peerAddress: peer0.manufacturer.aero:8443
        - organization: supplier
          type: joiner
          org_status: new

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

The endorsements key — AND('ManufacturerMSP.peer','SupplierMSP.peer') — requires every transaction to be approved by both organisations. That is what gives the ledger its value: neither party can record a certificate unilaterally.

The chaincode, written to last

The contract handles three operations: issuing a certificate, reading it, and tracing its history. Here is the core, in Go with the contract API.

go
package main

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

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

// ConformityCertificatee represents the attestation tied to a delivered part.
type ConformityCertificatee struct {
    Reference     string    `json:"reference"`
    NumeroSerie   string    `json:"numberSerie"`
    Supplier   string    `json:"supplier"`
    NormeAppliquee string   `json:"normeAppliquee"`   // EN 9100, NADCAP…
    DateEmission  time.Time `json:"dateEmission"`
    EmpreinteDoc  string    `json:"fingerprintDoc"`      // SHA-256 of the signed PDF
    Statut        string    `json:"status"`            // issued | approved | conteste
}

type Contrat struct {
    contractapi.Contract
}

// Issue records a certificatee. Only the supplier may call it.
func (c *Contrat) Issue(ctx contractapi.TransactionContextInterface,
    reference, numberSerie, norme, fingerprint string) error {

    // Caller MSP check: business logic is never delegated to the network.
    mspID, err := ctx.GetClientIdentity().GetMSPID()
    if err != nil {
        return fmt.Errorf("identité illisible : %w", err)
    }
    if mspID != "SupplierMSP" {
        return fmt.Errorf("only the supplier issues a certificate, MSP received: %s", mspID)
    }

    existant, err := ctx.GetStub().GetState(reference)
    if err != nil {
        return fmt.Errorf("cannot read the ledger: %w", err)
    }
    if existant != nil {
        return fmt.Errorf("certificate %s already exists", reference)
    }

    // The timestamp comes from the transaction, never from the peer clock:
    // two peers must produce an identical result.
    timestamp, err := ctx.GetStub().GetTxTimestamp()
    if err != nil {
        return fmt.Errorf("timestamp indisponible : %w", err)
    }

    cert := ConformityCertificatee{
        Reference:      reference,
        NumeroSerie:    numberSerie,
        Supplier:    mspID,
        NormeAppliquee: norme,
        DateEmission:   time.Unix(timestamp.Seconds, 0).UTC(),
        EmpreinteDoc:   fingerprint,
        Statut:         "issued",
    }

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

    // Event consumable by the manufacturer upstream systems.
    if err := ctx.GetStub().SetEvent("CertificateEmis", data); err != nil {
        return fmt.Errorf("émission de l'événement impossible : %w", err)
    }

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

// Approve flips the status. Reserved for the manufacturer.
func (c *Contrat) Approve(ctx contractapi.TransactionContextInterface, reference string) error {
    mspID, _ := ctx.GetClientIdentity().GetMSPID()
    if mspID != "ManufacturerMSP" {
        return fmt.Errorf("only the manufacturer approves, MSP received: %s", mspID)
    }

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

    var cert ConformityCertificatee
    if err := json.Unmarshal(data, &cert); err != nil {
        return err
    }
    if cert.Statut != "issued" {
        return fmt.Errorf("status incompatible : %s", cert.Statut)
    }
    cert.Statut = "approved"

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

Two points call for attention.

The timestamp comes from GetTxTimestamp(), not from time.Now(). Every peer runs the chaincode independently; if two peers produce different timestamps, their read-write sets diverge and the transaction fails validation.

The caller MSP check happens in the code. The endorsement policy guarantees that enough organisations approve, not that the right organisation initiates.

Network exposure between clusters

Two AKS clusters in two separate subscriptions have to talk gRPC with mutual TLS. Bevel relies on Ambassador, which exposes each component on a dedicated port with transparent TLS passthrough.

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

The sensitive point is DNS. Peers reach one another by name, and that name must resolve identically from both clusters. A public DNS zone managed by external-dns solves it cleanly; a hand-patched hosts file defers the problem to the first incident.

GitOps reconciliation

Bevel writes the Helm manifests into the GitOps repository. Argo CD applies them. Deployment thereby becomes reproducible and auditable.

yaml
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: fabric-manufacturer
  namespace: argocd
spec:
  project: reseau-conformity
  source:
    repoURL: https://gitlab.aero.io/gitops/reseau-conformity.git
    targetRevision: main
    path: platforms/hyperledger-fabric/releases/prod/manufacturer
  destination:
    server: https://aks-manufacturer-weu.hcp.westeurope.azmk8s.io:443
    namespace: manufacturer-net
  syncPolicy:
    automated: { prune: false, selfHeal: true }   # prune disabled: a deleted peer loses its ledger
    syncOptions:
      - CreateNamespace=true
      - ServerSideApply=true
    retry:
      limit: 5
      backoff: { duration: 30s, factor: 2, maxDuration: 10m }

Note prune: false. On a Fabric network, automatically deleting a resource that has disappeared from the repository would destroy a peer persistent volume — and its copy of the ledger with it. Synchronisation stays automatic, deletion stays manual.

The pitfalls we met

TLS certificate rotation. Certificates issued by the Fabric authority expire, often after a year. Without an automated renewal procedure, the network stops overnight. Plan a CronJob that watches expiry dates and alerts thirty days ahead.

CouchDB growth. The state database keeps the latest value of every key, but indexes multiply. On an active network, plan for disk space and a compaction policy.

Chaincode versioning. Since Fabric 2.x, the lifecycle requires every organisation to approve before commitment. An uncoordinated upgrade blocks the channel. Treat every chaincode version as an API change: compatibility contract, overlap period, rollback plan.

Raft ordering and latency. Ordering nodes exchange constantly. Spread across two distant Azure regions, latency degrades transaction throughput. Measure before deciding on geographic distribution.

What this architecture delivers

A certificate issued by the supplier, approved by the manufacturer, recorded in a ledger neither of them can rewrite. Every change leaves a timestamped, signed trail. Should a part be disputed, the full history is reconstructed by a single GetHistoryForKey call.

All of it deployed through GitOps, therefore reproducible: rebuilding the network on a new Azure subscription comes down to provisioning the clusters and letting Argo CD converge.