EST — Enrollment over Secure Transport
iya-sts is an EST server (RFC 7030,
with the clarifications of RFC 8951).
A device, a person or an application authenticates, sends a PKCS#10 request,
and receives a certificate from the trust realm’s EST Issuing CA — one of
the Issuing CAs of the realm’s certificate authority, under the
realm’s Intermediate and the service Root.
Every certificate issued over EST names a directory entry, a person or an
application, and is written onto that entry. A person may enroll only for
themselves, an application only for itself, and a holder of Admin Write for
any person or application in the realm.
The endpoints
All under the realm’s base URL (https://host:8081 in the default realm,
https://host:8081/realm/<id> in another):
| Method |
Path |
RFC 7030 |
Authenticated |
GET |
/.well-known/est/cacerts |
4.1 |
no |
POST |
/.well-known/est/simpleenroll |
4.2.1 |
yes |
POST |
/.well-known/est/simplereenroll |
4.2.2 |
yes |
POST |
/.well-known/est/serverkeygen |
4.4 |
yes |
GET |
/.well-known/est/csrattrs |
4.5 |
no |
POST |
/.well-known/est/fullcmc |
4.3 |
answers 501 |
The same six answer under a label, /.well-known/est/<profile>/…, where
the label is a certificate profile. The unlabelled path issues the realm’s
est.defaultProfile (tls-client unless changed).
Reaching a realm through the label
A realm other than the default is also reached by naming it in the label
position, at the root of the origin — for EST clients that are given a host,
a port and at most one label and can put nothing in front of
/.well-known/est (RFC 8615 puts a well-known URI at the root; libest’s
estclient is one such client):
| Path |
Reaches |
/.well-known/est/<realm>/<operation> |
realm <realm>, its est.defaultProfile |
/.well-known/est/<realm>/<profile>/<operation> |
realm <realm>, that profile |
/realm/<realm>/.well-known/est/[<profile>/]<operation> |
the same, by the prefix |
The two forms reach the same server with the same settings, CA and
certificates. The rules:
- A realm may not be called by a label’s name — any of the nine profiles
above or the five never issued. Creating one is refused (
STS-CORE-0107), so
one segment always means one thing. (A realm created with such a name before
2026-09-26 keeps it, is reached by its prefix only, and the segment still
means the profile.)
- A request names its realm once. A label that names a realm after the
realm was already named —
/realm/a/.well-known/est/b/cacerts, or
/.well-known/est/a/b/cacerts — is refused 404 (STS-EST-0022), never
read as a second realm or as a profile.
- An unknown name in the label position is an unknown label: 404.
/admin/est in a realm, and GET /realms for every realm (estLabelUrl),
show the label-form address.
| Profile (label) |
Needs |
tls-server |
a dNSName or iPAddress registered on the entry |
tls-client |
— |
tls-server-client |
a dNSName or iPAddress registered on the entry |
digital-signature |
— |
key-encipherment |
— |
code-signing |
— |
email |
a person with a mail attribute |
timestamping |
— |
smartcard-logon |
a person with userPrincipalName or mail |
device |
a DEVICE entry: the one the request’s urn:sts:device:<id> names (its owner, or an administrator), or a new one owned by the requester; a TPM key attestation in product. See Devices |
Never issued over EST (403): root-ca, intermediate-ca, issuing-ca,
ocsp-responder and kdc. Each makes its holder an authority over everybody
else in the realm; /admin/est gives the reason for each. An unknown label is
est.allowedProfiles narrows the nine further per realm.
Configuring EST
For the realm: Protocols → Cert issuance → EST
The EST page of the console (/admin/est) holds the realm’s server: the
endpoints and every profile’s labelled URLs, the EST Issuing CA, the
profiles, the credentials EST accepts, Issue a certificate with a
server-generated key, the certificate host names, the enrolled
certificates (each with a Revoke), and the est.* settings (see
Configuration). Viewing it needs Admin Read; changing
anything needs Admin Write. A realm’s own administrators manage their realm’s
page. Monitoring → Cert issuance → EST enrollments shows what the server
has done.
To get a client running:
- Turn the server on (
est.enabled, on by default), narrow
est.allowedProfiles if needed, and choose which credentials it accepts
(est.basicAuthentication, est.certificateAuthentication).
- Register the host names an entry may be issued under Certificate host
names (a dNSName or iPAddress it does not own refuses the request).
- Give the client a credential (see below) and the EST URL. A client that
can name only one label reaches a realm through the label (see above).
For one application
An application’s own rules are set on its page, **Directory → Applications →
**:
1. On the **Configuration** tab, **Protocol families** sub-tab, tick
**EST** and press **Save**. In **product** mode an application is
issued a certificate over EST only when it is ticked here; the
refusal is `STS-ENROLL-0094`. In development nothing is refused, and an
application with no families ticked is not refused in either mode.
2. On the **Certificate enrollment** sub-tab, set any of the overrides
below and press **Save**. **A value set here overrides the realm's setting for this application**, in either direction; a field left empty takes the realm's value.
The same attributes can be written with
`POST /admin-api/applications/update-fields` or `/admin-api/applications/set`.
The application's **Credentials** tab and its Certificate enrollment tab
also list the certificates it was issued, each with a Revoke button, and
generate its certificates with a server-generated key — the same actions as
on this protocol's page. See [Applications](/iya-sts/applications.html#certificate-enrollment-acme-est-scep).
| Attribute | Overrides | What it does for this application |
|---|---|---|
| `estAllowedProfiles` | `est.allowedProfiles` | The profiles it may be issued. Where it lists something, it replaces the realm's list for this application, wider or narrower. The CA, OCSP and KDC profiles are never issued. A request for another is refused (HTTP 403). |
| `estDefaultProfile` | `est.defaultProfile` | The profile of an unlabelled request. Used only when the list in force for it allows it. |
| `estCertificateLifetimeDays` | `est.certificateLifetimeDays` | The certificate lifetime, in place of the realm's (longer or shorter). No certificate outlives its Issuing CA. |
| `enrollMaxCertificates` | `pki.enrollmentMaxCertificatesPerEntry` | How many certificates it may hold across ACME, EST and SCEP, in place of the realm's (higher or lower). |
| `estBasicAuthentication` | `est.basicAuthentication` | TRUE accepts and FALSE refuses (HTTP 403, `STS-ENROLL-0096`) the application's own `client_id` and secret. |
| `estCertificateAuthentication` | `est.certificateAuthentication` | TRUE accepts and FALSE refuses the application's own TLS client certificate. |
| `estServerKeyGeneration` | `est.serverKeyGeneration` | TRUE allows and FALSE refuses `/serverkeygen` for its certificates, whoever asks. |
TRUE turns a method on for this application even where the realm has turned
it off, and FALSE turns it off even where the realm has it on; unset leaves
the realm's switch in force. The two authentication switches apply when the
application authenticates as itself, and the server-key switch whoever asks
for the application's certificate.
## Authenticating the caller
`/cacerts` and `/csrattrs` are not authenticated. `/simpleenroll`,
`/simplereenroll` and `/serverkeygen` accept three credentials, all ones the
service already has; EST has none of its own:
| Credential | Who | Checked |
|---|---|---|
| **HTTP Basic, a person's directory password** | a person | In product, verified. In development, any password is accepted for a person who exists. In product a person who holds, or must hold, a second factor is refused their own password (with the same 401 a wrong one gets) and uses an [app password](/iya-sts/authentication.html#the-password-only-doors-and-app-passwords) scoped to `est` instead. |
| **HTTP Basic, an application's `client_id` and `client_secret`** | an application | In product, the secret is verified. In development, it is not. |
| **A TLS client certificate this realm issued** | a person or an application | Always verified: issued by this realm (for example over EST with `tls-client`), still on the entry it names and not revoked. A certificate from another realm is refused. Mapped to its entry by its `urn:sts:person:` or `urn:sts:application:` name. |
* A Basic username is looked up as a **person first**, then as an
application's `client_id`.
* No other Authorization scheme is accepted. A request with no credential
gets `401` with `WWW-Authenticate: Basic realm="EST"`.
* `est.basicAuthentication` and `est.certificateAuthentication` turn each
method off for the realm; an application's own `estBasicAuthentication`
and `estCertificateAuthentication` override them for that application.
* `/simplereenroll` with no Basic credential needs the certificate being
renewed.
* A person may enroll only for themselves and an application only for
itself. A holder of **Admin Write** may enroll for any person or
application in the realm.
* In **product** mode a request that did not arrive over TLS is refused.
## Requesting a certificate with curl and openssl
Get the CA certificates first (unauthenticated):
```sh
BASE=https://localhost:8081
curl -s --cacert sts-root.pem $BASE/.well-known/est/cacerts \
| base64 -d | openssl pkcs7 -inform DER -print_certs > est-ca.pem
```
Make a key and a request, and enroll for the `tls-client` profile:
```sh
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout alice.key -subj "/CN=alice" -outform DER -out alice.csr.der
base64 alice.csr.der | curl -s --cacert sts-root.pem \
-u alice:password \
-H 'Content-Type: application/pkcs10' --data-binary @- \
$BASE/.well-known/est/tls-client/simpleenroll \
| base64 -d | openssl pkcs7 -inform DER -print_certs > alice.pem
```
A **server certificate** needs its host name registered on the entry first
(on `/admin/est`, or `POST /admin-api/est/add-host-name`), and the request must
ask for it:
```sh
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
-keyout web.key -subj "/CN=webapp1" \
-addext "subjectAltName=DNS:web.example.com" -outform DER -out web.csr.der
base64 web.csr.der | curl -s --cacert sts-root.pem -u webapp1:secret \
-H 'Content-Type: application/pkcs10' --data-binary @- \
$BASE/.well-known/est/tls-server/simpleenroll
```
**Renew** with the certificate itself. The request must repeat the subject and
subjectAltName of the certificate being renewed; the old one goes on the EST
CRL as `superseded`:
```sh
openssl req -new -key alice-new.key -subj "/CN=alice/O=sts" \
-addext "subjectAltName=URI:urn:sts:person:alice" -outform DER \
-out renew.csr.der
base64 renew.csr.der | curl -s --cacert sts-root.pem \
--cert alice.pem --key alice.key \
-H 'Content-Type: application/pkcs10' --data-binary @- \
$BASE/.well-known/est/simplereenroll
```
With Basic instead of the certificate, the certificate to renew is found among
the entry's valid EST certificates by the subject and names the request repeats.
**An administrator** enrolls for somebody else by naming them in a
`urn:sts:person:` (or `urn:sts:application:`) subjectAltName, or
by their username as the common name.
## With libest's estclient
Cisco's `estclient` (libest, the reference implementation) bootstraps from the
Root and then uses the `/cacerts` answer as its trust anchors, as RFC 7030
section 4.1.1 has a client do:
```sh
export EST_OPENSSL_CACERT=sts-root.pem
estclient -g -s host -p 8081 -o out # out/cacert-0-0.pkcs7
base64 -d out/cacert-0-0.pkcs7 | openssl pkcs7 -inform DER -print_certs > est-ca.pem
export EST_OPENSSL_CACERT=est-ca.pem
estclient -e -s host -p 8081 -o out -u alice -h "$PASSWORD" --common-name alice --pem-output
estclient -r -s host -p 8081 -o out -c out/cert-0-0.pem -k out/key-x-x.pem --pem-output
estclient -q -s host -p 8081 -o out -x key.pem -u alice -h "$PASSWORD" --common-name alice --pem-output
estclient -e -s host -p 8081 -o out -y web.csr --path-seg tls-server -u alice -h "$PASSWORD"
```
Things to know, each found by the suite's run of it
(`tests/vendored/sts_est_libest.js`):
* **It reaches another realm through the label** (#251): estclient builds its
URL as `https://host:port/.well-known/est[/label]/op` and accepts ONE label
segment, so `--path-seg ` names the realm (see *Reaching a realm
through the label*) and the profile is that realm's `est.defaultProfile`.
A labelled profile inside a realm needs two segments, which estclient
refuses to send; `/realm//.well-known/est` it cannot build at all.
* With only the Root as `EST_OPENSSL_CACERT`, every `-r` warns "unable to get
local issuer certificate": estclient verifies what it was issued against its
trust anchors, which must hold the Issuing CA and the Intermediate — the
`/cacerts` answer.
* `-q` prints `OSSL error: (null)` on success: libest dumps OpenSSL's (empty)
error queue whether or not anything failed. The key it writes
(`key-0-0.key`) is base64 PKCS#8 without PEM armour.
* It exits 0 whether or not it enrolled; read what it wrote.
* It builds against OpenSSL 1.1 only (`FIPS_mode()` is gone from 3.0).
* `-z` enrolls, but the challengePassword is not read (see *Not implemented*),
and `--srp` is refused at the handshake.
## Server-generated keys
`/serverkeygen` takes the request as a **template**: the key pair is generated
here in the template key's algorithm (an ML-KEM template gets an ML-KEM key, for
`key-encipherment` only), and the response is `multipart/mixed` with the PKCS#8
private key and the certificate, each base64. A sealed copy of the key is kept
on the entry. Turn it off with `est.serverKeyGeneration`.
The console's **Issue a certificate with a server-generated key** and
`POST /admin-api/est/issue-server-key` do the same for any entry, and show the
private key once.
## What `csrattrs` says
A DER `SEQUENCE OF AttrOrOID`: the signature algorithms a request may be signed
with (ECDSA with SHA-256 and SHA-384, RSA with SHA-256, Ed25519, ML-DSA-44/65/87),
the `extensionRequest` attribute, the profile's extended key usages, and — for
the server, email and smartcard profiles — a hint naming the subjectAltName kind
it needs (`dNSName`, `iPAddress`, `rfc822Name`, or the UPN otherName).
## What the CA decides, not the request
The subject is `CN=, O=` — or, for a
certificate that names a host, `CN=<the first dNSName, else iPAddress>,
UID=, O=` (#207: a client that reads a
certificate's names back as its CN and its dNSNames, as certbot and lego do,
asked for the entry's name as a host on every renewal; the UID keeps the
subject naming exactly one entry). The subjectAltName always carries the entry's `urn:sts:` name plus any requested
name the entry owns — a name it does not own **refuses the request**. Key usage,
extended key usage and basic constraints come from the profile, whatever the
request asked for. The lifetime is `est.certificateLifetimeDays`.
## Not implemented
* **Full CMC** (RFC 7030 section 4.3) answers `501`.
* **tls-unique channel binding** (section 3.5): TLS 1.3 has no tls-unique, so a
request's `challengePassword` is not read as one and is ignored.
* **An encrypted server-generated key** (section 4.4.1.2): a template carrying
`DecryptKeyIdentifier` or `AsymmetricDecryptKeyIdentifier` is refused `501`
rather than answered with an unencrypted key.
* **Deferred issuance** — `202` with `Retry-After` (section 4.2.3) is never
sent; every request is issued or refused at once.
* A **KEM key** (ML-KEM) cannot be enrolled with `/simpleenroll`: it cannot sign
the proof of possession. Use `/serverkeygen`.
* An EST credential of its own: EST uses passwords, client secrets and
certificates the service already has.
## Configuration
Every `est.*` setting is runtime and per trust realm, on **Protocols → Cert issuance → EST**
(`/admin/est`).
| Setting | Environment variable | Default | Runtime? | What it does |
|---|---|---|---|---|
| `est.enabled` | `STS_EST_ENABLED` | `true` | yes | Off makes every `/.well-known/est` endpoint answer 503 in this realm; certificates already issued are kept. |
| `est.allowedProfiles` | `STS_EST_ALLOWED_PROFILES` | all nine leaf profiles | yes | The `/admin/pki` profiles a label may name; the five CA, OCSP and KDC profiles are never issued whatever this says. |
| `est.defaultProfile` | `STS_EST_DEFAULT_PROFILE` | `tls-client` | yes | What the unlabelled `/.well-known/est/simpleenroll` issues. |
| `est.certificateLifetimeDays` | `STS_EST_CERTIFICATE_LIFETIME_DAYS` | `365` | yes | The validity of an EST certificate, shortened to the EST Issuing CA's own expiry. |
| `est.maxRequestBytes` | `STS_EST_MAX_REQUEST_BYTES` | `65536` | yes | A PKCS#10 body larger than this is refused (413) before it is decoded. |
| `est.attemptsPerIdentity` | `STS_EST_ATTEMPTS_PER_IDENTITY` | `10` | yes | Refused authentications or enrollments one username, `client_id` or certificate may make in a web-security window before 429. |
| `est.attemptsPerAddress` | `STS_EST_ATTEMPTS_PER_ADDRESS` | `60` | yes | Refused requests one client address may make in a web-security window before 429. |
| `est.basicAuthentication` | `STS_EST_BASIC_AUTHENTICATION` | `true` | yes | Accepts HTTP Basic with a person's password or an application's `client_id` and secret; whether the password is checked follows `global.mode`. |
| `est.certificateAuthentication` | `STS_EST_CERTIFICATE_AUTHENTICATION` | `true` | yes | Accepts a TLS client certificate this realm issued, mapped to its entry; required for `/simplereenroll` with no Basic credential. |
| `est.serverKeyGeneration` | `STS_EST_SERVER_KEY_GENERATION` | `true` | yes | Whether `/serverkeygen` generates the key pair — the one enrollment path in which this service holds a private key, kept sealed on the entry. |
The nine leaf profiles are those in the table under
[The endpoints](#the-endpoints). How many certificates one entry may hold
across ACME, EST and SCEP is `pki.enrollmentMaxCertificatesPerEntry`
([PKI](/iya-sts/pki.html#configuration)). See [Configuration](/iya-sts/configuration.html) for how a
value resolves and where it is changed — the console page, or
`POST /admin-api/config/set`.
## Design decisions
* **A label is a profile, not a CA.** RFC 7030 lets a server label its CAs;
here there is one EST Issuing CA per realm, and the label says what kind of
certificate is asked for. An unknown label is 404, a refused or disallowed
one 403.
* **Authentication uses credentials the service already has.** A person's
password, an application's client secret or a certificate this realm issued —
EST gets no credential of its own. See [above](#authenticating).
* **A Basic username is a person first.** Then an application's `client_id`,
so a person and an application sharing a name authenticate as the person.
* **Nothing is parsed for an unauthenticated client.** The query string,
whether EST is on, the transport, the label, the rate limit, the media type
and the size are all decided before a credential is read, and the
credential before the body, so an unauthenticated client is never why a CSR
is decoded and verified.
* **The certificate is decided by the profile and the entry.** Key usage,
extended key usage and basic constraints come from the profile, and a
requested name the entry does not own refuses the request rather than being
dropped — see [above](#what-the-ca-decides-not-the-request).
* **A private key is kept only when this service generated it.**
`/simpleenroll` never sees one; `/serverkeygen` keeps a sealed copy on the
entry the certificate names — see [above](#server-generated-keys).
* **A re-enrollment must repeat what it renews.** The subject and
subjectAltName must match the certificate being renewed (RFC 7030 section
4.2.2's "identical"), and the renewed certificate goes on the EST CRL as
`superseded`.
* **A KEM key is certified for `key-encipherment` only.** An ML-KEM key cannot
sign a proof of possession, so it comes only from `/serverkeygen`, and never
under a profile whose key usage it cannot perform.
* **A request body is decoded strictly.** RFC 8951 made whitespace legal and no
other stray byte, so anything outside the base64 alphabet is refused rather
than skipped the way a lenient decoder would.
* **The CA, OCSP-responder and KDC profiles are never issued.** Each would make
its holder an authority over everybody else in the realm.
* **Refusals are one plain-text sentence, and the code stays behind.** RFC 7030
asks for a human-readable message; the `STS-EST-*` code is on the audit row
and the monitor, never in the body.
## Console and management API
* **Protocols → Cert issuance → EST** (`/admin/est`): the endpoints and every profile's
labelled URLs, the EST Issuing CA, the credentials EST accepts, issuing with a
server-generated key, certificate host names, the enrolled certificates with a
Revoke on each, and the `est.*` settings.
* **Monitoring → Cert issuance → EST enrollments** (`/admin/est/monitor`): requests, issuances
and refusals by operation, profile, principal, error code and status.
* `GET /admin-api/est`, `GET /admin-api/est/monitor`, and
`POST /admin-api/est/{issue-server-key,revoke-certificate,add-host-name,remove-host-name}`.
Refusals are recorded under `STS-EST-*` and `STS-ENROLL-*` codes on the audit
log (see [error codes](/iya-sts/error-codes.html)); a client never sees a code.
## Related
* [PKI](/iya-sts/pki.html) — the realm's certificate authority, its profiles, CRLs and
OCSP responders
* [ACME](/iya-sts/acme.html) and [SCEP](/iya-sts/scep.html) — the other two enrollment protocols,
over the same rules
* [TLS and mutual TLS](/iya-sts/tls.html) — the client certificate EST accepts, and what
it signs in
* [Trust realms](/iya-sts/trust-realms.html)
* [What is not checked](/iya-sts/what-is-not-checked.html) — which passwords are checked
in which mode
* [Configuration](/iya-sts/configuration.html) and [error codes](/iya-sts/error-codes.html)