This project demonstrates how to secure IoT device APIs using mutual TLS (mTLS) and certificate-to-JWT authentication in Kong Gateway.
Each device presents a client certificate issued by a device-specific Intermediate CA. Kong verifies the certificate, maps it to a trusted CA, and converts the certificate identity into a short-lived JWT that is passed to upstream services.
This provides:
- Hardware-bound device identity
- Zero shared secrets on devices
- Centralized trust and revocation
- Fine-grained authorization (roles and scopes)
+------------------+ mTLS +-------------------+ JWT +------------------+
| IoT Device | <----------------> | Kong Gateway | ------------> | IoT Backend |
| (cert + key) | | (cert-to-jwt) | | (JWT auth) |
+------------------+ +-------------------+ +------------------+
| |
| Trust via CA |
+-------------------------------------------+
Each device connects using a TLS client certificate. Kong validates the certificate against trusted Intermediate CAs and converts that cryptographic identity into a JWT that downstream services can verify.
Root CA
├── Server CA
│ └── Kong TLS certificate
└── Device Intermediate CAs
├── watertrt CA
│ └── watertrt devices (x00000001, x00000002, ...)
└── watch CA
└── watch devices (x00000001, x00000002, ...)
Each product line has its own Intermediate CA. Revoking a product line is done by removing its CA from Kong.
infra/
├── certs.script.sh
├── certs/
│ ├── ca/
│ │ ├── server/
│ │ └── deviceinter/
│ │ ├── watertrt.crt
│ │ └── watch.crt
│ └── devices/
│ ├── watertrt/
│ └── watch/
└── docker-compose.yml
Generate all PKI material:
make certsThis creates:
- Root CA
- Server CA
- Device Intermediate CAs
- Per-device certificates
Run database migrations:
make migrationStart Kong:
make runKong endpoints:
- Proxy (HTTPS):
https://localhost:8443 - Admin API:
http://localhost:8001
Register device CAs with Kong:
make uploadThis uploads:
| CA | Tag |
|---|---|
| watertrt | iot-watertrt |
| watch | iot-watch |
Kong uses these tags to associate certificates with device families.
make setupTwo services are created:
| Service | Host | Path | Roles | Scope |
|---|---|---|---|---|
| iot-watertrt | watertrt.local | /secure-iot-watertrt | watertrt, iot | write |
| iot-watch | watch.local | /secure-iot-watch | watch, iot | read,write |
Each service installs the cert-to-jwt plugin.
When a device connects:
- Kong validates the TLS client certificate
- Kong checks that it chains to a trusted CA
- Kong generates a signed JWT
Example JWT payload:
{
"sub": "CN=x00000002",
"iss": "dolomi.watertrt.kong.issuer",
"roles": ["watertrt", "iot"],
"scope": "write",
"exp": 300
}Kong injects the token into:
Authorization: Bearer <jwt>
Backends only validate JWTs — they never need to handle certificates.
make watercThis sends a request using:
- Watertrt device certificate
- Device private key
- Kong server CA
Host: watertrt.local
A valid device receives 200 OK and the request is forwarded with a JWT.
make watchcSame flow using Watch credentials.
| Property | How it is enforced |
|---|---|
| Device identity | X.509 certificate |
| No shared secret | Private key never leaves device |
| Revocation | Remove cert or CA from Kong |
| Isolation | Separate CA per product line |
| Short-lived auth | JWT expires in 300 seconds |
| Least privilege | Roles and scopes in JWT |
| API Keys | mTLS + cert-to-JWT |
|---|---|
| Copyable | Hardware-bound |
| Hard to rotate | Re-issue certs |
| Shared secrets | None |
| No identity | Per-device ID |
| Flat access | Roles + scopes |
This setup implements zero-trust IoT authentication:
Devices authenticate with cryptography, Kong acts as the trust and policy authority, backends receive standard JWTs, and compromised devices can be revoked instantly.