ACME — Automatic Certificate Management Environment

iya-sts is an ACME server (RFC 8555) with RFC 9773 renewal information, the ip (RFC 8738) and email (RFC 8823) identifier types, certificate profiles and the permanent-identifier type of device attestation. Every trust realm has its own, issuing from the realm’s ACME Issuing CA — one of the Issuing CAs of the realm’s certificate authority, under the realm’s Intermediate and the service Root.

Two things make it different from a public CA, and both are deliberate:

  • An account is bound to one directory entry, for life. A new account REQUIRES an External Account Binding (RFC 8555 section 7.3.4), and an EAB key is issued for exactly one person or application. Every certificate the account is issued names that entry and is written onto it.
  • No challenge ever reaches out to you. An identifier is authorized from the directory entry: a host name an administrator registered on it, a person’s own mail, or the entry’s own identifier. Those authorizations are created valid, so a client has nothing to do; an identifier the entry does not own is refused when the order is placed.

The endpoints

All under the realm’s base URL (https://host:8081 in the default realm, https://host:8081/realm/<id> in another). A client needs only the first.

Method Path What it is
GET /enroll/acme/directory the directory (section 7.1.1)
HEAD, GET /enroll/acme/new-nonce a fresh Replay-Nonce (7.2)
POST /enroll/acme/new-account create or find an account (7.3)
POST /enroll/acme/account/{id} read, update or deactivate it (7.3.2, 7.3.6)
POST /enroll/acme/account/{id}/orders its orders (7.1.2.1)
POST /enroll/acme/new-order place an order (7.4)
POST /enroll/acme/order/{id} read an order
POST /enroll/acme/order/{id}/finalize finalize it with a CSR (7.4)
POST /enroll/acme/authz/{id} an authorization (7.5)
POST /enroll/acme/challenge/{id} its challenge (7.5.1)
POST /enroll/acme/cert/{id} download the certificate (7.4.2)
POST /enroll/acme/revoke-cert revoke a certificate (7.6)
POST /enroll/acme/key-change roll the account key over (7.3.5)
GET /enroll/acme/renewal-info/{certID} renewal information (RFC 9773)

The path is /enroll/acme and not /acme, because the first segment of a path is also where a trust realm’s id goes.

Getting an External Account Binding key

An EAB key is issued for ONE entry:

  • by the person themselves, on the user portal;
  • by an administrator, for any person or application in the realm — on Protocols → Cert issuance → ACME (/admin/acme), or through the management API:
curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
     -H 'Content-Type: application/json' \
     -d '{"kind":"person","identifier":"alice"}' \
     https://host:8081/realm/acme-demo/admin-api/acme/create-eab
{
  "ok": true,
  "kid": "eab-p-YWxpY2U-a770cbcdf277212f",
  "hmacKey": "fTOM027GWjv59Gz4qXrudgK7hbtsdWRmOoIW5VZEZ1w",
  "alg": "HS256",
  "expiresAt": "2026-09-20T17:36:05.195Z",
  "directory": "https://host:8081/realm/acme-demo/enroll/acme/directory",
  "certbot": "certbot register --server … --eab-kid … --eab-hmac-key …"
}

The HMAC key is shown once. It is stored sealed on the entry and nothing reads it back. A key binds one account and no other, and must bind within acme.eabLifetimeS (a week by default). This is also how an administrator issues for somebody else: create the key for their entry and hand it to whoever runs the client — the account, and every certificate it gets, belongs to that entry.

Host names

A dns or ip identifier is authorized only when it is registered on the entry the account is bound to. An administrator registers one on /admin/acme, or:

curl -s -H "Authorization: Bearer $ADMIN_API_TOKEN" \
     -H 'Content-Type: application/json' \
     -d '{"kind":"application","identifier":"web1","hostName":"web1.example.com"}' \
     https://host:8081/realm/acme-demo/admin-api/acme/add-host-name

A registered *.example.com authorizes that wildcard, and only when written so. The same registration serves EST and SCEP.

Configuring ACME

For the realm: Protocols → Cert issuance → ACME

The ACME page of the console (/admin/acme) holds everything about the realm’s server: the directory URL, the endpoints, the ACME Issuing CA, the profiles, the External Account Binding keys (create one for a person or an application, delete one), the accounts, the certificates issued (each with a Revoke), the registered host names, and the acme.* 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 → ACME enrollments shows what the server has done.

To get a client running, for any entry:

  1. Turn the server on (acme.enabled, on by default) and narrow acme.allowedProfiles if needed.
  2. Register the host names the entry may be issued under Registered host names (a dns or ip identifier is refused otherwise).
  3. Create an EAB key for the entry under External Account Binding keys, and hand the kid and HMAC key to whoever runs the client.

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 **ACME** and press **Save**. In **product** mode an application is issued a certificate over ACME 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 External Account Binding keys — 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 | |---|---|---| | `acmeAllowedProfiles` | `acme.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. An order naming another profile is refused `invalidProfile`. | | `acmeDefaultProfile` | `acme.defaultProfile` | The profile of an order that names none. Used only when the list in force for it allows it. | | `acmeCertificateLifetimeDays` | `acme.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). | An application's account is made the same way as anybody's: an administrator creates an EAB key for the application on the ACME page and hands it to the client. Its host names are registered there too. ## Authenticating the caller ACME has no passwords and no bearer tokens. Every request that changes anything is a **JWS** (RFC 8555 section 6.2) signed by a key the server knows, and the account behind that key is bound to one directory entry. | What | How the caller is authenticated | |---|---| | `GET /directory`, `HEAD`/`GET /new-nonce`, `GET /renewal-info/{id}` | Not authenticated. | | Creating an account (`new-account`) | A JWS signed by the new account key (a `jwk` header), carrying a **required External Account Binding**: an inner JWS over that key, MAC'd with the EAB key's HMAC secret (HS256, HS384 or HS512). The EAB key was made for one person or application, by that person on `/portal/certificates` or by an administrator. It binds one account, once, within `acme.eabLifetimeS`. That entry is the account's for life. | | Finding an existing account (`new-account` with `onlyReturnExisting`) | A JWS signed by that account's key; no EAB. | | Every other request (orders, finalize, authorizations, certificate download, account update) | A JWS signed by the **account key**, naming the account by its URL (a `kid` header). Each carries a fresh `Replay-Nonce` (accepted once) and its own URL. | | Changing the account key (`key-change`) | An outer JWS signed by the current account key, around an inner JWS signed by the new key. | | Revoking a certificate (`revoke-cert`) | Either an account bound to the certificate's entry (`kid`), or the **certificate's own private key** (`jwk`). | An account key may be RSA (2048 bits or more; RS256 to RS512, PS256 to PS512), ECDSA (P-256, P-384, P-521) or Ed25519. A post-quantum account key is refused: it has no RFC 7638 thumbprint. In **product** mode every request must arrive over TLS. Refused requests are rate-limited per account or EAB key (`acme.attemptsPerIdentity`) and per address (`acme.attemptsPerAddress`). ## Using a real client The service's TLS certificate is issued by its own Root CA; point the client at it (`/pki/ca/service/root.cer`, converted to PEM), for example with `REQUESTS_CA_BUNDLE` for certbot. ### certbot ```bash certbot register --server https://host:8081/realm/acme-demo/enroll/acme/directory \ --eab-kid eab-a-d2ViMQ-0123456789abcdef --eab-hmac-key \ --agree-tos --register-unsafely-without-email certbot certonly --server https://host:8081/realm/acme-demo/enroll/acme/directory \ --standalone -d web1.example.com ``` The authorization for `web1.example.com` is already valid, so certbot performs no challenge. certbot names no profile, and an order whose identifiers are all host names gets **`tls-server`** (serverAuth) — see *Orders* below. `--required-profile` or `--preferred-profile` (certbot 4.0 and later) names a profile; `certbot renew` keeps it, consults `renewalInfo` (RFC 9773), and reissues the same names. certbot has **no key rollover** (RFC 8555 section 7.3.5) at all; lego below does. The suite drives certbot 5.8.0 through registration, issuance, renewal, revocation, `update_account` and `unregister` (`tests/vendored/sts_acme_certbot.js`). ### lego ```bash export LEGO_CA_CERTIFICATES=sts-root.pem lego accounts register --server https://host:8081/realm/acme-demo/enroll/acme/directory \ --accept-tos -m you@example.com --eab --eab.kid eab-a-… --eab.hmac lego run --server https://host:8081/realm/acme-demo/enroll/acme/directory \ -m you@example.com --profile tls-server -d web1.example.com --http ``` lego logs that the authorization is already valid and skips the challenge; it still wants a solver named (`--http`), which binds nothing. It consults `renewalInfo` before a renewal, `accounts keyrollover` changes the account key, and `certificates revoke --reason N` revokes. `--not-after` and `--not-before` are refused (`malformed`): the realm sets a certificate's lifetime. A registration refused once leaves lego holding a key with no account, and it then asks for the account BY that key (`onlyReturnExisting`) rather than registering — use a fresh `--path`. The suite drives lego v5.5.2 (`tests/vendored/sts_acme_lego.js`). ### acme.sh ```bash acme.sh --register-account --server https://host:8081/realm/acme-demo/enroll/acme/directory \ --eab-kid eab-a-d2ViMQ-0123456789abcdef --eab-hmac-key acme.sh --issue --server https://host:8081/realm/acme-demo/enroll/acme/directory \ --standalone -d web1.example.com ``` ## Orders | Identifier `type` | `value` | Authorized when | |---|---|---| | `dns` | a host name, or `*.name` | it is registered on the entry | | `ip` | an IPv4 or IPv6 address | it is registered on the entry | | `email` | an address | it is the person's own `mail` | | `permanent-identifier` | the entry's username or application identifier (or its `urn:sts:` URN) | it is the account's own entry | An identifier the entry does not own refuses the whole order with `rejectedIdentifier`, one subproblem per identifier. An order may name a `profile`, and a named profile is always the one used (or refused `invalidProfile` when the realm does not allow it). **An order naming none** gets its profile from its identifiers: | The order's identifiers | Profile | |---|---| | all `dns` or `ip` (one or more) | `tls-server`, when `acme.allowedProfiles` holds it; otherwise `acme.defaultProfile` | | any `email` or `permanent-identifier`, alone or mixed with host names | `acme.defaultProfile` (`tls-client` unless changed) | A host-only order is asking for a server certificate whether or not it says so (a bare `certbot certonly -d www.example.com` was issued a clientAuth-only certificate until 2026-09-26). A mixed order names an entry as well as a host, and which of the two it is for is what it did not say, so it keeps the realm's default. `notBefore` and `notAfter` are refused: a certificate is valid for `acme.certificateLifetimeDays` (90 by default), shortened to the Issuing CA's own expiry. An order may name the certificate it `replaces` (RFC 9773); the replaced certificate is not revoked. **The CSR must name exactly the order's identifiers** (section 7.4): every subjectAltName and the common name must each be one of them, and all of them must be named. The certificate is built from the order and the entry, not copied from the CSR: its subject is `CN=, O=` — or, when it names a host, `CN=, UID=, O=`, because certbot and lego read a certificate's names back as its CN plus its dNSNames and every renewal asked for the entry's name as a host until this was so (#207) — it always carries the entry's `urn:sts:person:` or `urn:sts:application:` URI, and its key usages are the profile's. ## Profiles The nine leaf profiles of [`/admin/pki`](/iya-sts/pki.html), each allowed unless `acme.allowedProfiles` leaves it out (the directory's `meta.profiles` lists the allowed ones): | Profile | Needs | |---|---| | `tls-server` | a `dns` or `ip` identifier registered on the entry | | `tls-client` | — | | `tls-server-client` | a `dns` or `ip` identifier registered on the entry | | `digital-signature` | — | | `key-encipherment` | — (an RSA key is what the key usage means) | | `code-signing` | — | | `email` | an `email` identifier: the person's own `mail` | | `timestamping` | — | | `smartcard-logon` | a person with `userPrincipalName` or `mail` — the certificate carries it as the UPN | **Never issued over ACME**, whatever the setting says, answered `invalidProfile` with the reason: `root-ca`, `intermediate-ca`, `issuing-ca` (the holder could issue certificates for anybody), `ocsp-responder` (it could sign `good` about a revoked certificate) and `kdc` (it could impersonate the realm's KDC). ## Revocation and renewal `revokeCert` may be signed by an account bound to the certificate's entry, or by the certificate's own key (a `jwk` request). The reasons accepted are 0, 1, 3, 4, 5 and 9 of RFC 5280; the serial goes on the ACME Issuing CA's CRL at `/pki/crl//acme.crl` and its OCSP responder at `/pki/ocsp//acme` answers `revoked`. An administrator can revoke any of them on `/admin/acme`. `GET /enroll/acme/renewal-info/` answers a window in the last third of the certificate's validity, or — for a revoked certificate — a window in the past, which tells a client to renew now. ## Configuration Every `acme.*` setting is runtime and per trust realm, on **Protocols → Cert issuance → ACME** (`/admin/acme`). **Monitoring → Cert issuance → ACME enrollments** shows what the server has done. | Setting | Environment variable | Default | Runtime? | What it does | |---|---|---|---|---| | `acme.enabled` | `STS_ACME_ENABLED` | `true` | yes | Off makes every `/enroll/acme` endpoint answer 503 with a `serverInternal` problem naming the setting; accounts, orders and certificates are kept. | | `acme.allowedProfiles` | `STS_ACME_ALLOWED_PROFILES` | all nine leaf profiles | yes | The `/admin/pki` profiles an order may name and the directory advertises; the five CA, OCSP and KDC profiles are never issued whatever this says. | | `acme.defaultProfile` | `STS_ACME_DEFAULT_PROFILE` | `tls-client` | yes | The profile of an order that names none and is not host names only (those get `tls-server`); it must also be in `acme.allowedProfiles`. | | `acme.certificateLifetimeDays` | `STS_ACME_CERTIFICATE_LIFETIME_DAYS` | `90` | yes | The validity of a certificate issued at finalize, shortened to the ACME Issuing CA's own expiry. | | `acme.maxRequestBytes` | `STS_ACME_MAX_REQUEST_BYTES` | `65536` | yes | A flattened JWS larger than this is refused (413) before it is parsed; a post-quantum CSR is the largest legitimate request. | | `acme.attemptsPerIdentity` | `STS_ACME_ATTEMPTS_PER_IDENTITY` | `30` | yes | Refused requests one account or EAB key id may make in a web-security window before `rateLimited`. | | `acme.attemptsPerAddress` | `STS_ACME_ATTEMPTS_PER_ADDRESS` | `120` | yes | Refused requests one client address may make in a web-security window before `rateLimited`. | | `acme.nonceLifetimeS` | `STS_ACME_NONCE_LIFETIME_S` | `300` | yes | How long a `Replay-Nonce` may wait before it is presented; each is accepted once. | | `acme.maxSpentNonces` | `STS_ACME_MAX_SPENT_NONCES` | `10000` | yes | Spent nonces a realm remembers. A history still full of live ones answers the next request `badNonce` rather than forget one, which would let it be replayed. **At the default that is about 33 requests a second, sustained; past it every ACME request in the realm is refused until nonces expire**, and any client able to fetch a nonce and sign a request can cause it. Raise it for a busier realm. | | `acme.orderLifetimeS` | `STS_ACME_ORDER_LIFETIME_S` | `86400` | yes | How long an order stays pending or ready before it expires with its authorizations. | | `acme.eabLifetimeS` | `STS_ACME_EAB_LIFETIME_S` | `604800` | yes | How long an EAB key may wait before it binds an account; each binds one account and no other. | The nine leaf profiles are `tls-server`, `tls-client`, `tls-server-client`, `digital-signature`, `key-encipherment`, `code-signing`, `email`, `timestamping` and `smartcard-logon`. 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`. ## Development and product mode In **product** mode a request that does not arrive over TLS is refused. In **development** mode ACME also answers over plain HTTP and logs that it did. Everything else is the same in both: the EAB MAC is verified, an account may be issued only for its own entry, and a host name must be registered. ## Design decisions * **An account is bound to one directory entry, for life, through a required External Account Binding.** Every certificate therefore names an entry and is kept on it, and an administrator issues for somebody else only by creating an EAB key for their entry — ACME has no other administrator's door. See [above](#getting-an-external-account-binding-key). * **No challenge dials out.** This service never fetches from an address a caller supplied, so `http-01`, `dns-01`, `tls-alpn-01` and `email-reply-00` are not offered; an identifier is authorized from the entry and its authorization is created `valid`. The challenge type, `sts-entry-binding-01`, says what happened. * **An identifier the entry does not own fails the order at once.** It is refused at `newOrder` with `rejectedIdentifier`, rather than leaving a `pending` authorization no client could ever complete. * **A host name is issued only when an administrator registered it.** The same registration serves ACME, EST and SCEP, and a wildcard only when written as a wildcard — see [above](#host-names). * **The certificate is built from the order and the entry, never the CSR.** The CSR must name exactly the order's identifiers, and the subject, the `urn:sts:` name and the key usages come from the entry and the profile. * **ACME is CSR-only, and a private key is never kept.** The CA never sees the key; a server-generated key pair is EST's `/serverkeygen`. * **The CA, OCSP-responder and KDC profiles are never issued.** Their holder could issue certificates for anybody, sign `good` about a revoked certificate or impersonate the realm's KDC — see [above](#profiles). * **`replaces` does not revoke.** RFC 9773's `replaces` is a statement about renewal, and a client rolling over needs the old certificate to keep working until it deploys the new one. * **A subscriber may give only the revocation reasons that are a subscriber's.** Reasons 2 and 10 are an authority's, 6 (`certificateHold`) is the one reversible reason, and 7 and 8 are unassigned or for delta CRLs; an administrator on the console may use any reason. * **A nonce is spent only after the signature verifies.** Cheap checks run first, so a forged request cannot burn a nonce a client is holding, and two copies of one signed request cannot both pass. * **A nonce carries its own proof.** It is MAC'd with a secret every process — and, on a shared store, every node — holds, so any process can check it with no lookup; one from before a restart fails and is answered `badNonce` with a fresh one, which a client retries. * **A post-quantum account key is refused.** An account is found by its key's RFC 7638 thumbprint, and those key types have none; a post-quantum *certificate* key is fine. ## What is not implemented * The `http-01`, `dns-01`, `tls-alpn-01`, `email-reply-00` and `device-attest-01` challenges — ownership comes from the directory. * `notBefore`/`notAfter`, `newAuthz`, terms of service, alternate chains, and a `processing` wait at finalize (issuance is immediate). * Post-quantum **account** keys (their key type has no RFC 7638 thumbprint). A certificate for a post-quantum **signing** key is fine; a KEM key cannot sign a CSR and is refused `badPublicKey` — use EST `/serverkeygen`. * A server-generated key pair: ACME is CSR-only. Errors are RFC 7807 problem documents with `urn:ietf:params:acme:error:*` types; the operator-facing `STS-ACME-*` codes are on the [error code page](/iya-sts/error-codes.html) and in the audit log, never in a response. ## Related * [PKI](/iya-sts/pki.html) — the realm's certificate authority, its profiles, CRLs and OCSP responders * [EST](/iya-sts/est.html) and [SCEP](/iya-sts/scep.html) — the other two enrollment protocols, over the same rules * [TLS and mutual TLS](/iya-sts/tls.html) — where an issued `tls-client` certificate signs a person in * [Trust realms](/iya-sts/trust-realms.html) * [What is not checked](/iya-sts/what-is-not-checked.html) * [Configuration](/iya-sts/configuration.html) and [error codes](/iya-sts/error-codes.html)