8000
Skip to content

Latest commit

 

History

3 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

PCAS mTLS POC — Step-by-Step Guide

Overview

Warning: This is a simulation for testing purposes only. Do not use in production.

This POC simulates an end-to-end mutual TLS (mTLS) scenario mimicking a Payment Network's Card Authentication Service (PCAS) integration pattern:

  • Client (your MacBook) → presents a client certificate signed by a mock "PayNet" CA
  • HAProxy (Docker) → terminates TLS, verifies client certificate, forwards to WSO2
  • WSO2 API Manager (Docker) → backend API gateway
┌──────────────┐          ┌──────────────────────────────────────────┐
│              │  mTLS    │  Docker Desktop                          │
│   Client     │◄────────►│  ┌───────────┐       ┌──────────────┐    │
│  (curl/      │  :443    │  │ HAProxy   │──────►│ WSO2 APIM    │    │
│   Postman)   │          │  │ (mTLS     │ :9443 │ (API Gateway)│    │
│              │          │  │  termination)     │              │    │
└──────────────┘          │  └───────────┘       └──────────────┘    │
                          └──────────────────────────────────────────┘

Certificate Chain

flowchart TB

    CA["test_ca.crt<br/>(Mock PayNet Root CA)"]

    SERVER[server.crt<br/>CN=apigw.example.com]
    CLIENT[client_test.crt<br/>CN=TestClient01]

    SERVER_ID["server_identity.pem<br/>(server.crt + server.key)"]
    TRUST["client_trust.crt<br/>(contains test_ca.crt)"]

    HAP[HAProxy]
    CURL["Client (curl/Postman)"]

    %% Signing relationships
    CA -->|Signs| SERVER
    CA -->|Signs| CLIENT

    %% Bundles / copies
    SERVER --> SERVER_ID
    CA --> TRUST

    %% Runtime usage
    SERVER_ID --> HAP
    TRUST --> HAP

    CLIENT --> CURL
Loading

Certificate Exchange Flow

sequenceDiagram
    autonumber
    participant C as Client
    participant H as HAProxy
    participant CA as test_ca.crt

    Note over H: Loads server_identity.pem<br/>(server.crt + server.key)
    Note over H: Loads client_trust.crt<br/>(contains test_ca.crt)

    %% -------------------------
    %% 1️⃣ Server Authentication
    %% -------------------------
    C->>H: ClientHello
    H-->>C: ServerHello
    H-->>C: Certificate (server.crt)

    Note over C: Validate server.crt<br/>against trusted CA store

    C->>CA: Verify server.crt signature
    CA-->>C: Valid (signed by test_ca.crt)

    C->>C: Generate PreMasterSecret
    C->>H: Encrypted PreMasterSecret<br/>(encrypted using server public key)

    H->>H: Decrypt using server.key

    %% -------------------------
    %% 2️⃣ Client Authentication
    %% -------------------------
    H-->>C: CertificateRequest

    C-->>H: client_test.crt
    Note over H: Validate client_test.crt<br/>using client_trust.crt

    H->>CA: Verify client_test.crt signature
    CA-->>H: Valid

    C->>H: CertificateVerify<br/>(signed using client_test.key)

    Note over H: Verify signature using<br/>public key in client_test.crt

    Note over H: ssl_c_verify = 0 if success

Loading

Prerequisites

  • Docker Desktop installed and running on macOS
  • OpenSSL available (pre-installed on macOS)
  • Terminal access

Project Structure

POC/
├── certs/
│   ├── test_ca.key              # CA private key (mock PayNet CA)
│   ├── test_ca.crt              # CA certificate (mock PayNet CA)
│   ├── server.key               # Server private key (HAProxy)
│   ├── server.crt               # Server certificate (HAProxy) — signed by CA
│   ├── server_identity.pem      # Combined server key + cert for HAProxy
│   ├── client_trust.crt         # CA cert used by HAProxy to verify clients
│   ├── client_test.key          # Client private key
│   ├── client_test.crt          # Client certificate — signed by CA
│   ├── client_test.csr          # Client CSR (intermediate artifact)
│   ├── server.csr               # Server CSR (intermediate artifact)
│   └── server_ext.cnf           # Server cert config with SAN
├── haproxy/
│   └── haproxy.cfg              # HAProxy configuration
├── docker-compose.yml           # Docker Compose for HAProxy + WSO2
├── instructions.txt             # Original instructions
└── STEP-BY-STEP-GUIDE.md        # This file

Step 1: Understand the Certificate Chain

All certificates have already been generated and are in the certs/ directory. If you want to generate them again, you can follow the instructions in the CERTIFICATE-GENERATION-GUIDE.md file. Click here to view the certificate generation guide

Here's what each one does:

File Role Description
test_ca.key CA Private Key Private key for the mock "PayNet" Certificate Authority
test_ca.crt CA Certificate Root cert (mock PayNet CA) that signs both server and client certs
server.key Server Private Key HAProxy's private key for TLS
server.crt Server Certificate HAProxy presents this to clients (CN=apigw.example.com)
server_identity.pem Server Bundle server.key + server.crt combined for HAProxy
client_trust.crt Client Trust Store Copy of test_ca.crt (mock PayNet CA) — HAProxy uses this to verify client certs
client_test.key Client Private Key The client's private key (used by curl)
client_test.crt Client Certificate The client's cert signed by the CA (used by curl)

Trust Chain Diagram

test_ca.crt (Root CA — "Test-PayNet-CA")
├── server.crt  (CN=apigw.example.com, SAN: localhost, 127.0.0.1)
└── client_test.crt (CN=TestClient01)

Step 2: Review the HAProxy Configuration

The HAProxy config is at haproxy/haproxy.cfg. Key settings:

frontend https-in
    bind *:443 ssl crt /etc/haproxy/certs/server_identity.pem ca-file /etc/haproxy/certs/client_trust.crt verify optional

    acl is_pcas_path path_beg /pcas/v1/
    acl client_verified ssl_c_verify 0

    # Deny if client cert not verified on /pcas/v1/ paths
    http-request deny status 403 if is_pcas_path !client_verified

    default_backend wso2_apim

backend wso2_apim
    server wso2 wso2-apim:8243 ssl verify none

Key points:

  • verify optional — HAProxy requests a client cert but doesn't force it for all paths
  • ssl_c_verify 0 — means client cert verification succeeded (0 = success)
  • /pcas/v1/ paths require a valid client cert (mTLS enforced)
  • Other paths work without a client cert (standard TLS)
  • Client cert details are forwarded to WSO2 via X-SSL-* headers

Step 3: Add Hostname to /etc/hosts

Add the hostname apigw.example.com to your local hosts file so it resolves to 127.0.0.1:

sudo sh -c 'echo "127.0.0.1 apigw.example.com" >> /etc/hosts'

Verify:

ping -c 1 apigw.example.com

Expected output: PING apigw.example.com (127.0.0.1)


Step 4: Start the Docker Services

From the POC/ directory:

docker compose up -d

Check that both containers are running:

docker-compose ps

Expected output:

NAME            STATUS
haproxy-mtls    Up
wso2-apim       Up

Note: WSO2 APIM takes ~2-3 minutes to fully start. Wait for it before testing.

Check WSO2 readiness:

docker logs wso2-apim 2>&1 | grep -i "started"

Step 5: Test mTLS — The Fun Part!

Test 5a: Access WITHOUT client certificate (should get 403)

curl -k https://apigw.example.com/pcas/v1/products

Expected: HTTP 403 Forbidden — HAProxy denies because no client cert was presented on a /pcas/v1/ path.

Test 5b: Access WITH valid client certificate (should get through)

curl -k \
  --cert certs/client_test.crt \
  --key certs/client_test.key \
  https://apigw.example.com/pcas/v1/products

Expected: The request passes through HAProxy to WSO2 APIM. You may get a 404 from WSO2 (no API deployed yet), but not a 403 — that means mTLS succeeded!

Test 5c: Access a NON-PCAS path without client cert (should work)

curl -k https://apigw.example.com/

Expected: This should pass through to WSO2 without requiring a client cert (HAProxy verify optional allows it).

Test 5d: Verbose mode to see TLS handshake details

curl -kv \
  --cert certs/client_test.crt \
  --key certs/client_test.key \
  https://apigw.example.com/pcas/v1/test

Look for these lines in the output:

* TLSv1.2 (OUT), TLS handshake, Certificate (11):
* TLSv1.2 (IN), TLS handshake, Request CERT (13):

This confirms the server requested a client cert and the client provided one.


Step 6: Inspect SSL Headers Forwarded to WSO2

HAProxy forwards these headers to WSO2 APIM:

Header Value
X-SSL-Client-Verify 0 (success)
X-SSL-Client-DN /CN=TestClient01/O=Test Client/C=US
X-SSL-Client-CN TestClient01
X-SSL-Issuer /CN=Test-PayNet-CA/O=Test Payment Network Certificate Authority/C=US

You can verify these by deploying a simple echo API in WSO2 or checking HAProxy logs:

docker logs haproxy-mtls

Step 7: Verify Certificates Independently (Optional)

Verify client cert is signed by CA

openssl verify -CAfile certs/test_ca.crt certs/client_test.crt

Expected: certs/client_test.crt: OK

Verify server cert is signed by CA

openssl verify -CAfile certs/test_ca.crt certs/server.crt

Expected: certs/server.crt: OK

View certificate details

openssl x509 -in certs/client_test.crt -noout -text
openssl x509 -in certs/server.crt -noout -text
openssl x509 -in certs/test_ca.crt -noout -text

Step 8: Stopping the Services

docker-compose down

Troubleshooting

HAProxy won't start

docker logs haproxy-mtls

Common causes:

  • Certificate file permissions — ensure files are readable
  • Invalid PEM format — check server_identity.pem has both key and cert

Connection refused on port 443

  • Ensure Docker Desktop is running
  • Check no other service is using port 443: lsof -i :443

403 even WITH client cert

  • Verify cert is signed by the correct CA: openssl verify -CAfile certs/test_ca.crt certs/client_test.crt
  • Ensure client_trust.crt in HAProxy matches test_ca.crt

WSO2 not responding

  • Wait 2-3 minutes after container start
  • Check logs: docker logs wso2-apim

Going Live — Swapping to Real Certificates

Once the POC works, transition to production by:

  1. Replace server_identity.pem with your real production server cert + key
  2. Replace client_trust.crt with the real Payment Network Root/Intermediate CA certificates
  3. The real Payment Network client will present their cert signed by the real CA
  4. Change verify optional to verify required if all paths need mTLS

Summary of Roles

Role File Description
Server Identity server_identity.pem HAProxy presents this to clients during TLS handshake
Client Verification client_trust.crt HAProxy uses this CA to verify incoming client certs
The Client client_test.crt + client_test.key Used by curl/Postman to authenticate to the server
Certificate Authority test_ca.crt + test_ca.key Mock "PayNet" CA that signs both server and client certs

About

WSO2 mTLS POC — Step-by-Step Guide

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

0