Accounts and policy¶
This page covers what a host controls about who may register, what an account may order and what
the directory tells clients. All of it is configuration on Config plus two optional interfaces.
Directory metadata¶
Meta: acmeserver.DirectoryMeta{
TermsOfService: "https://ca.example.com/terms",
Website: "https://ca.example.com",
CAAIdentities: []string{"ca.example.com"},
ExternalAccountRequired: true,
},
The meta object is included in the directory when any field is set. TermsOfService also adds a
Link header with rel="terms-of-service" to newAccount responses. CAAIdentities is
informational. The library does not check CAA records, so a CA that must honor CAA does so in the
issuance policy, see Issuance policy.
Terms of service¶
RequireTermsOfServiceAgreed refuses a newAccount whose payload does not set
termsOfServiceAgreed with a userActionRequired problem. It needs Meta.TermsOfService, so
the client can show the URL. Agreement is stored on the account.
External account binding¶
External account binding ties an ACME account to an identity the CA already knows, such as a customer or a device. The client presents a MAC over its account key, signed with a key the CA issued out of band under a key identifier.
type ExternalAccountKeys interface {
MACKey(ctx context.Context, keyID string) ([]byte, error)
}
Implement it over your key registry and set Config.ExternalAccounts. Return ErrNotFound for an
unknown identifier, which becomes an unauthorized problem. Return any other error for a lookup
failure. A key identifier longer than 256 bytes is refused with malformed before the lookup.
HS256, HS384 and HS512 are accepted. RFC 7518 section 3.2 requires a key at least as long
as the hash, so a 256-bit key works with HS256 only and a 512-bit key with all three. A binding
whose algorithm needs a longer key is refused with badSignatureAlgorithm listing the algorithms
the key allows. A key shorter than 256 bits is refused with unauthorized and logged. Issue keys of
at least 512 bits if clients may choose HS512. A verified binding stores the key identifier as
Account.ExternalAccountID, which is available to Policy.NewAccount and to your own audit
queries.
Meta.ExternalAccountRequired advertises the requirement and refuses accounts without a binding
with externalAccountRequired. Without it a binding is verified when present and optional
otherwise.
SingleUseExternalAccounts binds each key identifier to at most one account. The claim is stored
with the account, so the store's uniqueness check enforces it under concurrency. A client that
retries newAccount with the same account key gets its existing account back. A different account
key with the same identifier is refused with unauthorized.
Account policy¶
type Policy interface {
NewAccount(ctx context.Context, account *Account) error
NewOrder(ctx context.Context, account *Account, order *Order) error
}
Config.Policy defaults to AllowAll. Embed it and override the methods you need:
type customerPolicy struct {
acmeserver.AllowAll
customers Registry
}
func (p customerPolicy) NewOrder(ctx context.Context, account *acmeserver.Account, order *acmeserver.Order) error {
for _, id := range order.Identifiers {
if !p.customers.Owns(ctx, account.ExternalAccountID, id.Value) {
return acmeserver.NewProblem(acmeserver.ErrorRejectedIdentifier, "not authorized for this name").
WithIdentifier(id)
}
}
return nil
}
NewAccount sees the key, the contacts, the terms of service flag and the external account
identity before the account is stored. NewOrder sees the account and the order after identifier
normalization and before any authorization is created. It may change NotBefore and NotAfter,
for example to cap the requested validity. Any other change, or a NotAfter that is not later than
NotBefore, is logged and answered with serverInternal.
A returned *Problem is sent to the client. Any other error is logged and answered with
serverInternal.
Rate limits¶
Config.Limits takes a Limiter, which decides how often clients may get nonces, create accounts,
place orders, start validations and look up renewal information. Without one only the bound on
validations in progress applies.
type Limiter interface {
Allow(ctx context.Context, req LimitRequest) error
}
Allow is asked once for each of these requests:
LimitRequest.Kind |
Asked | Fields set |
|---|---|---|
LimitNewNonce |
Before a nonce is issued to a request that spent none: a newNonce request or an error answer to a request for an unknown resource, with a wrong method, an unparsable JWS or a bad nonce |
Client |
LimitNewAccount |
After the contacts, terms of service and external account binding were checked, before Policy.NewAccount. A request that finds an existing account is not asked about |
Client, Account (not yet stored) |
LimitNewOrder |
After the identifiers, profile and replaces were checked, before Policy.NewOrder |
Client, Account, Identifiers (normalized), Replaces |
LimitChallengeResponse |
After a response to a pending challenge was checked and counted against MaxPendingValidations, before the validation is enqueued. A repeated response to a challenge that is already processing is not asked about |
Client, Account, Identifiers (the one identifier of the authorization, with its wildcard prefix), PendingValidations |
LimitRenewalInfo |
Before the certificate lookup of a renewalInfo request with a well-formed identifier |
Client |
Count the request when you return nil. To refuse, return a rateLimited problem with
WithRetryAfter. The client gets status 429 and a Retry-After header in whole seconds, rounded
up. Refused newNonce and renewalInfo answers carry no Replay-Nonce. Refused newAccount,
newOrder and challenge response answers carry one, since those requests spent a nonce. A refused
challenge response leaves the challenge pending, so the client can respond again later. Any
other error is logged and answered with serverInternal. Return nil on errors instead if clients
should keep working while your limit store is unavailable. New kinds may be added in later
versions, so allow kinds you do not know.
Every error answer carries a nonce, as RFC 8555 section 6.5 recommends. An answer to a request
that spent no valid nonce, such as 404, 405, 415, an unparsable JWS or badNonce, asks about
LimitNewNonce first. When refused, the answer keeps its status and carries no nonce. A refused
badNonce answer becomes the limiter's problem instead, because the RFC requires a nonce on every
badNonce answer. An answer to a request that spent its nonce carries the replacement without
asking. Without a limiter every error answer carries a nonce, so a client can collect nonces
faster than it uses them and push older nonces of other clients out of nonce.Options.Capacity.
func (l limits) Allow(ctx context.Context, req acmeserver.LimitRequest) error {
if req.Kind != acmeserver.LimitNewOrder || req.Replaces != "" {
return nil // renewals that name their predecessor are never held back
}
wait, err := l.buckets.Take(ctx, "orders:"+req.Account.ID, 300, time.Hour)
if err != nil || wait == 0 {
return err
}
return acmeserver.NewProblem(acmeserver.ErrorRateLimited, "too many new orders for this account").
WithRetryAfter(wait)
}
Client is the address of the connection from http.Request.RemoteAddr, with IPv4-mapped IPv6
addresses unmapped. It is the zero netip.Addr when RemoteAddr holds no IP address. Forwarded
headers such as X-Forwarded-For are never read, because any client can set them. Behind a reverse
proxy every client has the proxy's address. If you trust the proxy, rewrite RemoteAddr from its
header in a middleware before the handler. IPv6 clients usually control a whole /64, so key
per-address limits on the prefix rather than the full address.
Replaces is set only after the server has checked that the replaced certificate belongs to the
account and shares an identifier with the order, so a client cannot claim to renew someone else's
certificate to escape a limit.
Validations in progress¶
Every accepted challenge response becomes a validation task. A validation of a destination that
accepts connections and never answers holds a worker for the whole NetworkOptions.Timeout on
each attempt. Config.MaxPendingValidations bounds how many validations one account may have
waiting or running at the same time. It defaults to MaxIdentifiers, so one full order can
always be validated at once. A response beyond the bound is refused with rateLimited and a
Retry-After of WorkerConfig.TaskTimeout, by which time at least one attempt of an earlier
validation has ended. The challenge stays pending and the client responds again later.
The bound is per account. Use LimitChallengeResponse, which reports the account's
PendingValidations, together with limits on new accounts and orders to decide how much
validation work a client may start over time. Concurrent responses may pass the bound together,
so it is exact only up to the number of requests in flight.
Pre-approved identifiers¶
Some CAs let an administrator approve names in advance for an external account binding, because
the CA cannot reach internal hosts and the team cannot publish DNS records. A policy that also
implements PreApprover names the identifiers of a new order that need no challenge:
func (p customerPolicy) PreApprove(ctx context.Context, account *acmeserver.Account, order *acmeserver.Order) ([]acmeserver.Identifier, error) {
var approved []acmeserver.Identifier
for _, id := range order.Identifiers {
if p.customers.Approved(ctx, account.ExternalAccountID, id) {
approved = append(approved, id)
}
}
return approved, nil
}
PreApprove runs after NewOrder has accepted the order and must not change it. Each returned
identifier must be one of order.Identifiers, wildcard names included. Its authorization starts
valid with no challenges and expires with the order. An order whose identifiers are all
pre-approved starts ready, so the client can finalize at once. The issuer sees PreApproved set
on those entries of IssueRequest.Validations.
A pre-approved authorization never lets the account revoke a certificate another account
ordered, because it proves nothing about control of the name. A returned *Problem refuses the
order. Any other error, or an identifier the order does not contain, is answered with
serverInternal.
Accounts over their lifetime¶
- Registration returns the existing account when the key is already known, so a client that lost its account URL recovers it with
onlyReturnExisting. - Contacts must be
mailtoURLs with one bare address each, at most ten. Other schemes are refused withunsupportedContact. - Key rollover replaces the key through the inner JWS of RFC 8555 section 7.3.5. An in-flight validation keeps the thumbprint captured when the client responded.
- Deactivation is permanent. Pending work of the account stops: a challenge accepted but not yet validated becomes invalid without a proof being fetched. New orders are refused. Recovery of an issuance the CA may already have completed continues, because the certificate may exist.
- Order list is served at the account's
ordersURL in pages of 100 with arel="next"link.
Account IDs are random and appear only in the account URL. The store indexes accounts by ID and by key thumbprint.
Limits worth setting¶
| Field | Default | Reason to change |
|---|---|---|
MaxIdentifiers |
100 | Cap the size of one order for your CA |
OrderLifetime |
7 days | How long a client has to complete validation and finalization |
AuthorizationLifetime |
30 days | How long a validated authorization stays valid, which matters for revocation by another account |
MaxRequestBody |
64 KiB | Set 128 KiB when CSR keys or the CA use SLH-DSA SHA2-256f or SHAKE-256f. Otherwise raise only if a policy accepts unusually large CSRs |