Certificates
Where certificates come from, how a name is proven, uploaded certificates and what warns before one expires.
Noust orders, renews, serves and warns about the certificates of the sites it writes. Certbot does the ordering; Noust decides from where, how the name is proven, which certificate a site serves, and puts the result in front of the web server.
The certificate authority
Certificates are ordered from Let's Encrypt unless you choose another authority.
noust cert acme # show the authority and the account
noust cert acme --ca letsencrypt-staging # try a setup without spending the rate limit
noust cert acme --ca zerossl --email ops@example.com --eab-kid KID --eab-hmac-key-stdin --register
printf %s "$HMAC" | noust cert acme --ca zerossl --eab-kid KID --eab-hmac-key-stdin
noust cert acme --ca custom --server https://acme.example.com/directory--ca |
What it is |
|---|---|
letsencrypt |
Let's Encrypt (the default). |
letsencrypt-staging |
Let's Encrypt's staging environment: certificates browsers do not trust, with generous rate limits. The place to try DNS, ports and hooks before spending the real ones. |
zerossl, google |
Authorities that ask for an external account (EAB): create it at the authority and give its key id and HMAC key. |
custom |
Another ACME directory, with --server. |
The HMAC key is read from standard input, never an argument (everything on a command line is
visible in ps), is kept in the secret store (a 0600 file, sealed with the rest on a sealed
central) and is passed to certbot through a configuration file, never on its command line. It is
never in an API response or in the console. --register registers the account with the authority
now, so a wrong key fails where you are looking and not at the next order. The settings live in
ssl.ca, ssl.acme_server, ssl.eab_kid and ssl.email (CONFIG.md); the console
has them under Domains, Certificates, More actions, Certificate authority.
How a name is proven
| Challenge | How | Needs |
|---|---|---|
http-01 (the default) |
The authority fetches a file from port 80. | The name points at this server, port 80 reachable. |
dns-01 |
The authority looks up _acme-challenge.<name>. |
A DNS account that holds the zone. Port 80 can stay closed. A wildcard needs it. |
For a Cloudflare, DigitalOcean, Hetzner, Gandi or Amazon Route 53 account Noust answers the
challenge itself, through two hooks certbot keeps in the lineage's renewal configuration
(noust dns challenge auth|cleanup --account NAME), so every renewal runs them again with nobody
there. The credential stays in the secret store; no file with it is left for certbot to point at.
The record is published with a marker where the provider keeps notes and removed afterwards, and
only the record that order published is removed. After publishing, Noust waits for the record to
be served: 20 seconds, 30 for Hetzner and 60 for Route 53, or what the account sets with
noust dns provider set NAME --propagation SECONDS. For the other providers (Linode, OVH,
RFC 2136) certbot's own plugin answers, from a credentials file Noust writes 0600; the plugin
package must be installed (noust dns provider kinds says which).
Where an application's certificate comes from
Each application chooses, and the choice is kept in the store:
noust cert use shop.example.com --acme # ordered, proven over HTTP
noust cert use shop.example.com --acme --dns cf # ordered, proven with a DNS record in cf
noust cert use shop.example.com --upload origin # serve an uploaded certificate
noust cert source shop.example.com # where it comes from, and what is servedChanging how a certificate that exists is proven orders it again, once: certbot remembers the method in the lineage's renewal configuration. A failed order puts the choice back, and the certificate that served keeps serving. A name that a certificate you already have covers (a wildcard from another lineage) can be served without ordering one.
A site that serves no TLS yet (deployed with --no-ssl, or whose first certificate could not be
ordered over a port 80 nobody can reach) only keeps the choice. Its first certificate is
noust cert create -d APP, which proves the name the way the choice says (DNS included), and then
writes the site with TLS, so the same two commands take a name behind a closed port 80 to HTTPS:
noust cert use wiki.example.com --acme --dns cf
noust cert create -d wiki.example.comA certificate for one name
An application's names share one certificate by default: the lineage under its primary domain names every one of them. That is the right shape until it is not: a name that should not sit beside the others in a certificate transparency log, or one whose DNS may stop pointing here and would then fail the renewal of all its neighbours. A name can hold a certificate of its own:
noust domain certificate shop.example.com admin.example.com --own # ordered over HTTP-01
noust domain certificate shop.example.com admin.example.com --own --dns cf # proven with a DNS record in cf
noust domain add shop.example.com api.example.com --own-certificate # a new name, with its own
noust domain certificate shop.example.com admin.example.com # which certificate serves it
noust domain certificate shop.example.com admin.example.com --shared # back to the shared oneThe name gets a certbot lineage named after it (admin.example.com; a wildcard's is
_wildcard.example.com, a name no host can have) that holds that name alone, and a server block
of its own in the site that presents it. The shared certificate stops naming it, and is ordered
again without it, so the renewal of the others no longer depends on it. A redirect can hold one
too, since it is served over HTTPS as well. The primary cannot: the shared certificate is the one
kept under it (give every other name its own and it covers the primary alone). The www and bare
pair of a canonical name is served together and keeps one. A monorepo orders its certificate in
its own deploy, for subdomains only it knows, and keeps one.
The service is never cut. Taking a certificate of its own goes in this order, and a failure at a step changes nothing before it:
- the name's certificate is ordered, while the site goes on serving the name as it did. A refusal ends it here, in certbot's own words, and nothing has changed;
- the site is written again with the name's server block, and the web server tests the whole configuration before it reloads. A refusal puts the choice and the file back;
- the shared certificate is ordered again without the name. This is housekeeping: the site is
already right, so a failure is reported (
noust domain certificate APP --reduceretries it) and undoes nothing.
Giving it back runs the other way: the shared certificate is extended to cover the name first, the
site is written with the name back in the shared block, and the lineage the name held is deleted
last, unrevoked. A site is set apart only once the name's lineage is on disk, so a name whose order
failed is still served with the shared certificate and the web server is never pointed at a file it
lacks. On a site that serves no TLS yet only the choice is kept; its first certificate
(noust cert create) orders the name's own as well.
How the name is proven is kept per name, the same record an application's certificate has: the
application's own way when the name takes its certificate (HTTP-01 or a DNS account), or what
--dns ACCOUNT / --http say. Changing it orders the name's certificate again once, because
certbot keeps the way in the lineage's renewal configuration. A wildcard needs a DNS account.
A DNS account's hooks are kept in the name's lineage, so each renewal publishes and removes the
record of that name alone.
Renewal. noust cert renew --domain APP renews every lineage the application has, each by its
own certbot command, so one that cannot renew is named, with certbot's output, and does not keep the
others from it. certbot's own timer renews every lineage, and the hook that reloads the web server
after a renewal (renewal-hooks/deploy/noust-reload) runs for each. Removing a name from an
application deletes only its lineage and its server block; noust domain disable keeps the
lineage (the name comes back). A lineage still presented by a name is never deleted.
A site you wrote is never rewritten. The certificate is ordered and the choice recorded, and the
answer names the two files your site has to present for the name; the shared certificate is not
ordered again without the name, because your site may still present it. Point the site at the new
files, then noust domain certificate APP --reduce.
Which certificate serves each name. noust domain list APP has a Certificate column,
noust cert list a Serves column, and the console's Domains tab marks the names that hold one of
their own. API: GET /api/apps/{domain}/domains gives each name certificate (shared or own),
served_by and certificate_pending; GET /api/certs gives each certificate serves;
PUT /api/apps/{domain}/domains/{name}/certificate with {"certificate": "own"} (and optionally
challenge and dns_provider) queues the change as a job, because certbot never runs on a
request.
A certificate you bring
noust cert upload origin --cert origin.pem --key origin.key [--chain chain.pem]
noust cert uploads # names, expiry, issuer, who serves it
noust cert remove-upload originFor a Cloudflare Origin Certificate, a corporate CA's or a purchased one. Before anything is
written the key must be the certificate's and have no passphrase, the certificate must not have
expired, and it must name something. The key may come from standard input (--key -). The files
are kept in /etc/noust/certs/<name>/ (the key 0600 in a 0700 directory), the web server is
tested before it is reloaded, and a certificate an application serves cannot be deleted. It is
never renewed or revoked by Noust: it appears in noust cert list and in the health report, and
you are warned as it expires. Upload the new one over it.
An authority of your own, for private names
Let's Encrypt signs a name it can prove, and wiki.home.arpa or grafana.lan (a name that only
resolves on a home network, a VPN or a tailnet) has no such proof. The usual answers are a browser
warning for ever or an authority of your own. This server can be its own:
noust cert ca init # make it (once)
noust cert ca init --permit-dns corp.example.com --permit-network 10.20.0.0/16
noust cert use wiki.home.arpa --internal # sign for every name the app answers on, and serve it
noust cert ca issue gitlab gitlab.lan 10.0.0.5 --days 60 # a certificate by hand (kept as internal-gitlab)
noust cert ca list | status | renew internal-gitlab | renew --due
noust cert ca root --out ca.crt # the public certificate to install (--der for ca.cer)
noust cert ca trust # how, on each kind of device
noust cert ca trust --here # trust it on this server (--remove to stop)- It can only sign what it was made for. The limit is a name constraints extension in the
authority's certificate, marked critical, so every device that trusts it enforces it: a leaked key
cannot be used to impersonate
bank.comto the people who installed the root. By default it signslan,internal,home.arpa,local,localhost,testand the private, tailnet and loopback ranges;--permit-dnsand--permit-networkadd to them and--onlyreplaces them. Noust refuses a top-level domain (com) and a public address range before making anything, and refuses a name outside the constraints before openssl is asked. The integration harness proves the other half with a real client: a leaf for a public name signed with the very key is rejected by openssl itself. - It is made once and never again by itself. A new root has to be trusted again on every device,
so the authority that is about to expire is a warning (six months before, in
noust healthand the console; an issue in the last month), not a surprise. Coolify's own authority expires after ten years without a word.noust cert ca removedeletes it and its key. - A certificate it signs is P-256, can authenticate a server and nothing else, is valid 90 days
unless asked (up to 397) and never outlives the authority. It is kept as an uploaded certificate
named
internal-<name>, so everything that serves, lists, warns about and backs up an upload does the same for it, and an application serves it through the one path that serves an upload. - It renews itself. The hourly network upkeep signs it again for the same names and reloads the sites that serve it when thirty days are left (a third of its term for one made for under three months, so a short one is not signed again every hour). One that cannot be renewed does not keep the others from it, and a site that cannot be reloaded is named, because it still serves the old certificate.
- Trust is yours to install, per device.
noust cert ca trusthas the steps for Debian and Ubuntu, RHEL and its family, openSUSE, Alpine, Arch, macOS, Windows, iOS, Android, Firefox, Java, Node.js and curl, each with the file it wants and the fingerprint to compare before trusting it. Noust changes only this server (--trust-here, or--trust-hereatinit): its own store, rebuilt through the runner, so curl, git and a health probe here accept the names. A program with a store of its own (Java, Node, a container) needs the steps for it. - It is not the database authority. Engines that serve TLS (
noust db) have a root of their own in/etc/noust/tls, withpathlen:0, signing for engines and clients; this one is in/etc/noust/ca, name-constrained and signing for web servers. A certificate of one does not verify against the other, and installing this root on a device does not make it trust an engine. - There is no revocation list. A leaf is replaced, not revoked: take the certificate out of the
site (
noust cert use APP --acme) and delete it (noust cert remove-upload NAME). For a root whose key may be out, remove it and make another; every device has to trust the new one.
API: GET|POST|DELETE /api/certs/ca (making and deleting ask for sudo mode),
GET /api/certs/ca/root?format=pem|der (public), GET /api/certs/ca/trust,
POST|DELETE /api/certs/ca/trust-here (sudo mode), POST /api/certs/ca/issued and
POST /api/certs/ca/issued/{name}/renew; an application chooses it with
PUT /api/apps/{domain}/certificate and {"source": "internal"}. They are the server's, not a
team's.
When a name goes
Removing a domain from an application orders its certificate again with only the names it still
answers on (a name that held a certificate of its own takes only that one with it) (noust domain certificate APP --reduce retries it) and lets go of the DNS record Noust
made for it. Coolify's issue #6800, a certificate that kept a deleted domain, is this case.
Nothing is revoked: the old certificate expires on its own and the new one never covered the name.
Renewal
Certbot's timer renews. A deploy hook (/etc/letsencrypt/renewal-hooks/deploy/noust-reload)
reloads the web server afterwards, so a renewed certificate is the one being served without anyone
running anything. Every order writes it, whatever proves the name: certbot's own nginx plugin
reloads after a renewal it proves over HTTP, and a DNS order has no such plugin, so without the
hook a renewed certificate would sit on disk while the site went on serving the old one until it
expired. A hook you wrote under that name is left alone. noust cert renew is a command somebody
waits on, so it does not take the random pause (up to eight minutes) certbot adds to a renewal that
no terminal started; the timer's own runs keep it. The monitor's expiry warnings reach the notification channels, and an uploaded certificate is
warned about as well.
Backing it up
/etc/letsencrypt (accounts, keys, renewal configuration, the hook above) is part of
noust self-backup (see disaster-recovery.md), with its links, taken on a
schedule, with retention, proven restorable and put back on a fresh install. So are the uploaded
certificates (/etc/noust/certs) and the secrets of the DNS accounts and of the external account.