One Keystore, Two Merchants
A shared keystore file had no notion of whose key was whose — so once a second merchant showed up, the gateway started authenticating outbound calls under the wrong identity.
- System
- Archway API Gateway
- Company
- Airtel Africa Digital Labs
- Impact
- Fixed before it caused a partner-facing incident; became the template for all subsequent per-partner mTLS onboarding.
Context
Archway is the API gateway sitting in front of Airtel's payment orchestration layer, routing outbound calls to biller, bank, and switch integrations across 14+ African markets. Several of those partners required mutual TLS — the gateway had to present a client certificate to prove its identity before the partner would talk to it.
The original implementation kept every merchant's client certificate in a single shared Java keystore file, loaded once at startup into one `KeyManager`.
What went wrong
A Java keystore is just a bag of aliased key entries — it doesn't inherently know which entry a given outbound call should use. The existing HTTP client code never told it. When the SSL layer needed a client certificate, it took whatever the default `KeyManager` handed it — in practice, the first matching private key entry in the store.
That's invisible with one merchant in the keystore, because there's only one key to pick. It becomes a live bug the moment a second merchant's certificate is added: calls destined for merchant B could get authenticated with merchant A's client cert, or vice versa depending on load order, with no error thrown — TLS handshakes don't fail just because you presented the "wrong" valid certificate, they fail (or silently succeed against the wrong endpoint identity) depending entirely on how the partner's TLS termination validates the caller.
It's the kind of bug that survives code review, because nothing about a single-key setup looks wrong until a second tenant exists.
The fix
I rebuilt certificate selection to be explicit rather than incidental: each merchant's key was stored under an alias matching its merchant ID, and I wrote a custom `KeyManager` wrapper that selects the alias based on the outbound request's merchant context before the handshake begins, instead of trusting the JDK's default "first match" behavior.
This became the dynamic Keystore/Truststore layer that shipped as a standard part of the HTTP client — every new partner onboarding since then declares its own aliased entry and gets per-partner mutual TLS without touching shared state.
The lesson
A shared secret store with no concept of "whose secret is this" is a landmine with the pin already half-pulled. It works fine for tenant #1 and fails silently for tenant #2 — which means the bug ships clean and waits for scale to trigger it.
Next case study
Speaking the Bank's Language →