Embedding the server¶
This page covers the pieces every host needs regardless of its CA or database: constructing the
Server, mounting it, running the worker, checking readiness and shutting down. The
Getting started program shows all of them together.
Construct the server¶
import (
acmeserver "github.com/misiektoja/go-acme-server"
"github.com/misiektoja/go-acme-server/memstore"
"github.com/misiektoja/go-acme-server/nonce"
)
srv, err := acmeserver.New(acmeserver.Config{
BaseURL: "https://ca.example.com/acme/",
Store: store,
Nonces: nonce.New(nonce.Options{}),
Issuer: myCA,
Revoker: myCA,
Validators: map[acmeserver.ChallengeType]acmeserver.Validator{acmeserver.ChallengeHTTP01: http01},
})
New validates the configuration and returns an error naming the first problem. It starts no
background work. Store, Nonces, Issuer, Revoker and at least one validator are required.
Every other field has a default listed in Configuration.
The base URL¶
Config.BaseURL is the absolute URL clients use to reach the handler, including any path prefix.
Every URL the server hands out is built from it and every signed request carries the URL the client
posted to, which the server compares with the one it expects. The two must match exactly, so:
- Use the public scheme, host and port that clients see, not the address the process listens on.
- Keep the path prefix in place through any reverse proxy. Do not strip it. See Deployment.
- Use
https.AllowInsecureBaseURLacceptshttpfor local tests only.
A missing trailing slash is added. User info, a query string, a fragment or a path that needs percent-encoding, such as one with a space or a non-ASCII character, are refused.
Mount the handler¶
Server implements http.Handler and expects requests whose path starts with the base URL path:
mux := http.NewServeMux()
mux.Handle("/acme/", srv)
Requests outside the base path receive a 404 problem document. The handler sets
Cache-Control: no-store and a Link to the directory on every protocol response, so a proxy in
front of it needs no special caching rules. The paths below the base URL are listed in
Endpoints.
The host owns TLS. Terminate it in the same process with ListenAndServeTLS or in front of it,
as long as the origin the client connects to is the one the base URL names.
Run the worker¶
go func() {
if err := srv.Run(ctx); err != nil {
log.Print(err)
}
}()
Run claims validation and issuance tasks from the store and processes them until ctx is
canceled. It is not optional. Without a running worker challenges stay processing forever and
finalized orders never issue. Run returns an error if it is already active on the same Server.
The worker may live in another process than the handler as long as both use the same store. Set
WorkerConfig.External in the handler process so it does not warn every time work is accepted
while no local worker is running. Several workers may share one store. A task run stops when its
lease ends and fences refuse a stale commit, so no task is processed twice at the same time. A
worker that dies is replaced once its lease lapses.
WorkerConfig tunes concurrency, poll interval, lease length, per-phase timeouts, retry policy
and the staleness threshold. The defaults are listed in
Configuration.
Readiness¶
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
if err := srv.Ready(r.Context()); err != nil {
http.Error(w, err.Error(), http.StatusServiceUnavailable)
return
}
w.WriteHeader(http.StatusNoContent)
})
Ready counts tasks that have waited longer than WorkerConfig.StaleAfter without being claimed.
It returns an error when there are any and no task is leased, which means no worker is picking up
work. A queue that is deep because workers are busy passes, so client load cannot fail the probe.
It reports the state of the store, so it is correct in the handler process even when the worker
runs elsewhere. Wire it into the readiness or liveness probe of whichever process should be
restarted when work stalls.
Logging¶
Config.Logger takes a *slog.Logger. Without one, nothing is logged. The server logs failed
store, validator and CA calls at error level with the resource and operation IDs involved, and
retried validations at warn level. Problem documents sent to clients are not logged. For an audit
trail of refusals, see Recording refusals.
Recording refusals¶
Most refusals reach the host anyway, because they come from its own Policy, IssuancePolicy,
Issuer or Revoker. The server refuses some requests itself: a CSR that does not match its
order, a revocation by an account without authority or a revocation reason that is not allowed.
Config.Refusals takes a RefusalRecorder that is told about these.
type RefusalRecorder interface {
RecordRefusal(ctx context.Context, refusal Refusal) error
}
RecordRefusal is called for every finalize and revokeCert request refused after the
requester is known, including refusals that came from the Revoker. A Refusal carries the
Kind, the AccountID and the Problem the client receives. A refused finalization adds the
order, its identifiers and the CSR. A refused revocation adds the certificate and the requested
reason. AccountID is empty for a revocation signed with the certificate key. Requests that fail authentication, a finalize for another account's order and
serverInternal problems are not reported, because they say nothing about what a known client
asked for.
The call runs before the answer is sent, detached from the client request and bounded by
Config.DetachedWriteTimeout. A returned error is logged as refusal could not be recorded and
the client still gets the same answer. Refusals of new accounts and orders come from your
Policy, which can record them itself.
Time¶
Config.Clock supplies the current time. It defaults to the system clock. Tests use
acmeserver.ClockFunc to control expiry and retry timing.
Shutdown¶
Stop accepting requests first, then cancel the worker context and wait for Run to return:
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
_ = httpServer.Shutdown(shutdownCtx)
workerCancel()
<-workerDone
Canceling the context stops new claims but does not interrupt a task in flight. A validator or
issuer call that has already started runs to its TaskTimeout. The store operations that
record its outcome get their own budget. Run can therefore take up to WorkerConfig.Lease to
return. Waiting is optional. A task whose worker disappears is leased. Another worker picks it
up once the lease ends.
A revocation the CA already carried out is recorded even if the client disconnects, within
Config.DetachedWriteTimeout. Give the process at least that long to exit after the HTTP server
stops.
Checklist¶
BaseURLis the public origin and path, over HTTPS.- The handler is mounted at the base path and the prefix survives the proxy.
Runis active in at least one process on the same store.- A health check calls
Ready. Loggeris set in production.- Shutdown cancels the worker after the HTTP server and waits.