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.
| Component | Org1 — Manufacturer | Org2 — Supplier |
|---|---|---|
| Certificate authority | ca.manufacturer.aero | ca.supplier.aero |
| Peers | 2 (endorsement + query) | 2 |
| State database | CouchDB | CouchDB |
| Ordering service | 3 Raft nodes, shared | — |
| Cluster | AKS aks-manufacturer-weu | AKS 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.
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.
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: ordererA 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.
# 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.
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.
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.
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.