This guide lets an API client (a bank with a FAPI 2.0 integration, say) obtain its client certificate and renew it on its own, with a key it generated. At the end the client authenticates at /token with a certificate that central issued, renews it before it expires, and you know what to do when its key is out. The concept, the chain rules and the deviations from the RFC are on Client certificates and their renewal and RFC 7030. If you issue client certificates from your own CA instead, read Keep your own PKI and skip the rest.
You need auth with a mutual-tls listener, central with a CA backend (client-certificates), and a tenant that sets mtlsCertificateHeader (as for any tls_client_auth client).
1. Enable EST in auth
EST is off until enabled. Add the block to auth.conf:
est {
enabled = true
ca-certificates = "/app/mtls/client-ca.crt" # the client CA alone
}
ca-certificates is required and must hold the CA that issues clients’ certificates alone, never the whole mutual-tls.trusted-certificates bundle, which may hold the edge CA (Configuration reference). On the generated targets set EST_ENABLED=true and, except on local and Kubernetes (where the chart mounts the client CA alone at /app/mtls/client-trust/ca.crt), EST_CA_CERTIFICATES=<path in auth's container> for versola-tools; on Kubernetes see Kubernetes PKI. The routes are:
| Where | Routes |
|---|---|
Public name of auth (the issuer) | /.well-known/est/cacerts, csrattrs, simpleenroll |
| Public name of the mutual-TLS listener | /.well-known/est/cacerts, csrattrs, simplereenroll |
The client therefore needs two base URLs: enrolment on the issuer origin, renewal on the mutual-TLS origin. A stock EST client configured with one base URL gets 404 for simplereenroll on the issuer origin and a TLS handshake error for simpleenroll on the mutual-TLS one, so run it once per origin. cacerts and csrattrs need no credentials on the issuer origin, which is where a client without a certificate fetches them; on the mutual-TLS origin they need a valid client certificate.
2. Register the client
Register a client (web or service kind) with authMethod: tls_client_auth and estEnrollment: true. It holds no certificate yet. Its certificate subject is its mtlsAuth (for example subject_dn) or, where it names none, CN=<client>,OU=<tenant>,O=Versola. The subject is never taken from the client’s request and stays the same through renewals, so the registration does not change. estEnrollment cannot be combined with issueEdgeClientCertificate, enrollEdgeClientCertificate or a certificate you supply. See Client.
3. Create an enrolment token
curl -u central:<central secret> -X POST \
"https://central.example.com/configuration/clients/est/enrollment-tokens?clientId=bank-a&validityHours=48"
The answer has the token, its expiresAt and the subject the request must say. The token is shown once; only its hash is stored. It lives 24 hours by default (validityHours up to 168) and works once. A new token replaces the client’s unused earlier ones. Give the token and the subject to the client’s operator over a secure channel.
4. The client enrols
The client generates its own key, builds a PKCS#10 for exactly the subject it was given and sends it to simpleenroll, with the client id and the token as HTTP Basic (or the token as a Bearer token). OpenSSL lists a subject from the most general part to the most specific, the opposite of the RFC 4514 string you were given:
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout bank-a.key -subj "/O=Versola/OU=acme/CN=bank-a" -outform DER | base64 > request.b64
curl -sS -u "bank-a:<enrolment token>" \
-H "Content-Type: application/pkcs10" --data-binary @request.b64 \
-D headers.txt https://id.example.com/.well-known/est/simpleenroll > response.b64
base64 -d response.b64 | openssl pkcs7 -inform DER -print_certs -out chain.pem
The keys accepted are EC P-256 or RSA of at least 2048 bits. The answer is a certs-only PKCS#7 with the chain and no key; pick your certificate by its subject. A request refused with 400 does not spend the token. simpleenroll is limited to 30 attempts a minute per client id and replica (429, Retry-After). Fetch the CA to trust from /.well-known/est/cacerts.
5. The client renews
A certificate lives for client-certificates.validity (14 days by default) and renews only in its last third (client-identity.renew-window); earlier, simplereenroll answers 403 and says when the window opens. The client sends a new PKCS#10 with a new key to the mutual-TLS origin, presenting its current certificate:
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 -nodes \
-keyout bank-a.next.key -subj "/O=Versola/OU=acme/CN=bank-a" -outform DER | base64 > request.b64
curl -sS --cert bank-a.crt --key bank-a.key \
-H "Content-Type: application/pkcs10" --data-binary @request.b64 \
https://id-mtls.example.com/.well-known/est/simplereenroll > response.b64
The subject and alternative names must be those of the certificate being renewed, and the key must differ from the old one: a client whose HSM cannot generate a new key for each certificate cannot use this profile. A request carrying a challengePassword attribute (channel binding, RFC 7030 §3.5) is refused with 400; send the request without it. A certificate that has already expired is not renewed with itself: create a new token and enrol again.
Tokens do not follow the certificate. A token issued while the client presented the old certificate carries its thumbprint (cnf.x5t#S256) and does not validate with the renewed one. The client keeps presenting the old certificate with the old tokens until they expire, and uses the new one for new tokens. The old certificate stays valid, and a certificate renews in the last third of its life by default, so that third is the overlap to count on. Plan the switch so that the new certificate is used from the next token request on (Sender-constrained tokens). The client should also reload the new key into its TLS stack without a restart.
6. Chains, extension and revocation
Renewals form a chain that began at the enrolment. At client-identity.max-age (a year by default) central refuses a renewal (403) until you extend it:
curl -u central:<central secret> -X POST \
"https://central.example.com/configuration/clients/est/extend?clientId=bank-a"
With client-identity.auto-extend = true central does it itself. The end is soft: a certificate issued right before it lives its whole validity, so the client can run past Versola-Chain-Ends-At, which every answer carries. GET /configuration/clients shows est.pending, est.certificateExpiresAt and est.chainEndsAt.
If a key may be out:
curl -u central:<central secret> -X POST \
"https://central.example.com/configuration/clients/est/revoke?clientId=bank-a"
What is revoked is the right to renew, not the certificate: every certificate issued so far belongs to a generation of the chain and none can renew (401). Enrolling again with a new token starts a new generation. The certificates themselves keep authenticating until they expire; change the client’s mtlsAuth subject or delete the client to cut that too. A certificate that a renewal replaced and that comes to renew again after a 3-minute grace means the key is in two hands: the chain is closed (403, est_chain_fork_detected) and you enrol the client again. A client that is no longer registered for EST (estEnrollment turned off) gets 401 on renewal.
What to watch
| Where | What |
|---|---|
| The client | Days to expiry, the last renewal, a reload after renewal |
auth metric | est_requests_total{operation,outcome}: a rise of forbidden, unauthorized or chain_fork |
central metric | est_certificate_requests_total{operation,outcome} |
| Client state | est.certificateExpiresAt and est.chainEndsAt in GET /configuration/clients; alert on a client whose certificate is in its last third and has not renewed |
See Observability. Revocation is expiry: Versola reads no CRL or OCSP, so the short validity is what bounds a stolen certificate.
Keep your own PKI
If you do not use EST, issue the new certificate from your own CA with the same subject the client is registered with, give it to the client and let it request new tokens. Nothing changes in Versola. A change of subject or CA is not part of EST: what is not covered.
Next steps
- RFC 7030 — EST: what is implemented and where Versola deviates.
- Calling an API with a certificate-bound token
- Client