Skip to content
Pages in Guides

Certificates

Where certificates come from, how a name is proven, uploaded certificates and what warns before one expires.

Guide Updated View as Markdown

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.

Shell
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:

Shell
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 served

Changing 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:

Shell
noust cert use wiki.example.com --acme --dns cf
noust cert create -d wiki.example.com

A 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:

Shell
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 one

The 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:

  1. 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;
  2. 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;
  3. 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 --reduce retries 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

Shell
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 origin

For 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:

Shell
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.com to the people who installed the root. By default it signs lan, internal, home.arpa, local, localhost, test and the private, tailnet and loopback ranges; --permit-dns and --permit-network add to them and --only replaces 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 health and 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 remove deletes 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 trust has 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-here at init): 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, with pathlen: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.

Something on this page is wrong or unclear? Report it on GitHub.

Help, contributing and the project’s policies are under Project.

Search the documentation

Type to search every page.

↑ ↓ to move, Enter to open, Esc to close