rgoussu@goussu: ~/library/security/exercises
~/library/security/exercises cat personal-ca-and-mtls-subject.md

Stand up your own CA + mTLS between two services — subject

# The concrete lab spec for a private PKI — root + intermediate CA, leaf certs with SANs/EKUs, two toy services doing mutual TLS, and revocation — runnable offline with openssl or step-ca.

Subjectsaved 2026-08-08source #tls#pki#certificates#mtls#security#exercise#subject

Brief

Two internal services need to talk, and "it's on the private network" is not an answer your future self accepts. You are the PKI. You will mint a root of trust, delegate to an intermediate, issue the leaf certificates your services present, and configure both ends so that each refuses to talk to anyone who cannot prove — with a certificate your CA signed — that they are who they claim. By the end you can point at a rejected handshake and say exactly which extension, which chain link, or which expiry caused it.

Instructions

Use openssl throughout, or smallstep/step-ca if you prefer a real CA daemon; the spec is tool-agnostic. Keep everything in a scratch lab directory. Two "services" can be as small as two openssl s_server / s_client instances or two toy HTTP servers on localhost with different ports/hostnames (e.g. alice.local and bob.local via /etc/hosts).

1. Root and intermediate CA

  • Generate a root CA key and self-signed root certificate. Treat the root key as offline: after signing the intermediate, set it aside and never use it to sign leaves.
  • Generate an intermediate CA key and CSR, and sign it with the root. Give it basicConstraints = CA:TRUE, pathlen:0 and keyUsage = keyCertSign, cRLSign.
  • Inspect both with openssl x509 -text -noout and confirm basicConstraints, key usage, and validity windows read as intended.

2. Server leaf

  • Generate a server key + CSR and sign it with the intermediate (not the root).
  • Set subjectAltName to the server's DNS name(s) — the CN is ignored by modern clients, the SAN is what is checked. Set extendedKeyUsage = serverAuth.
  • Serve it and build the chain to send: leaf + intermediate (not the root). Confirm with openssl s_client -connect host:port -showcerts that the full chain is presented. Deliberately reproduce the missing-intermediate bug (serve leaf only, watch verification fail) and then fix it.

3. Client leaf and mutual TLS

  • Issue a client leaf from the intermediate with extendedKeyUsage = clientAuth and an identifying subject/SAN.
  • Configure the server to require and verify a client certificate against your CA chain (s_server -Verify / verify with the CA file; SSL_VERIFY_PEER | FAIL_IF_NO_PEER_CERT in code). Now both directions authenticate — this is mTLS.
  • Confirm rejection paths: a client with no cert is refused, and a client presenting a cert from a different CA is refused at the handshake.

4. Identity enforcement

  • Have the server read the presented client certificate's subject/SAN and authorize on it (e.g. allow only client.alice.local). Show that an untrusted issuer breaks the connection during the TLS handshake — before any application code runs — not at the app layer.

5. Operate it: short lifetimes + revocation

  • Issue short-lived leaf certs (hours) and script renewal; add a check that alerts on imminent expiry. Observe why short lifetimes force automation and bound compromise.
  • Exercise revocation: either publish a CRL, revoke a leaf, and have the verifier honor it, or lean on short-lived certs as the revocation strategy and demonstrate the window. Show a revoked/expired leaf being rejected.

Examples

Representative verification commands (adapt paths):

# Walk a chain by hand
openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt

# See what a server actually presents
openssl s_client -connect bob.local:8443 -showcerts

# mTLS: succeeds with a client cert, fails without one
curl --cacert root.crt --cert client.crt --key client.key https://bob.local:8443/
curl --cacert root.crt https://bob.local:8443/     # expect handshake failure

Constraints

  • Build the trust yourself with openssl or step-ca — no public CA, no Let's Encrypt.
  • The root key stays offline once the intermediate is signed; leaves are signed by the intermediate only.
  • Leaves carry the right extensions: SANs for names, serverAuth/clientAuth EKU for role. A missing or wrong extension should be diagnosable, not glossed over.

Acceptance

  • Milestone 1 (root the trust): openssl x509 -text on root and intermediate shows correct basicConstraints (CA:TRUE, pathlen), key usage, and validity; the intermediate verifies against the root.
  • Milestone 2 (server leaf): openssl s_client -showcerts shows leaf + intermediate; openssl verify -CAfile root.crt -untrusted intermediate.crt server.crt returns OK; the missing-intermediate failure is reproduced and then fixed.
  • Milestone 3 (client leaf + require): a curl/client with a valid client cert connects; one with no cert and one with a foreign-CA cert are both rejected at the handshake.
  • Milestone 4 (identity enforced): the server authorizes on the client cert's subject/SAN; swapping to an untrusted issuer fails during the handshake, not in application logic.
  • Milestone 5 (operate): short-lived leaves issue and renew via script with an expiry alert; a revoked (CRL) or expired leaf is rejected by the verifier.

Related