Feature Specification: Resource ssl
Created: 2026-10-07
Status: Implemented
Input: Reconstructed from the code at 093e68b6f (core/resources/ssl/default.nix, core/resources/ssl/cross-node.nix, core/resources/ssl/self-signed.nix, core/resources/ssl/acme.nix, core/resources/ssl/openbao-pki.nix, core/resources/ssl/openbao-pki-login.nix, core/resources/ssl/openbao-pki-trust.nix, core/setups/wiring/tls-routing.nix, core/setups/wiring/tls-trust.nix, core/setups/wiring/served-domains.nix, core/setups/hosts-common.nix), pull requests #1184, #1205, #1236, #1238, #1241, #1246, #1266, #1361, #1383, #1405, #1433 and #1690, and issue #527.
User Scenarios & Testing (mandatory)
User Story 1 - Serve a domain over TLS (Priority: P1)
A component serves one or more domains. It states a request with domains.
The policy routes each domain to a provider of the ssl contract. The
consumer node gets an nginx vhost with forceSSL and the certificate and
key from the provider. A service that terminates TLS itself, such as
PostgreSQL or maddy, gets the files but no vhost.
Why this priority: each public and internal endpoint needs a certificate. Without one, clients refuse to connect.
Independent Test: contracts-ssl-self-signed and
integrations-openbao-pki. nix-unit-setups-ssl-key-accounts checks the key
owners. nix-unit-resources-ssl evaluates the apply of the resource as a
node: the nginx vhost and port for each domain, and no vhost for a domain
whose key another service owns.
Acceptance Scenarios:
- Given a request with
domains = [ "a.example.org" ], When the consumer side is evaluated, Then nginx gets a vhost fora.example.orgwithsslCertificateandsslCertificateKeyfromcontracts.ssl.results.fediversity."a.example.org", and the firewall opens port443. - Given a domain whose provider request sets a
keyOwnerother thannginx, When the consumer side is evaluated, Then nginx gets no vhost for that domain. - Given a domain without an explicit key group, When the consumer side is evaluated, Then
keyGroupdefaults tonginxandrestartServicesdefaults to[ "nginx.service" ]. - Given a request with no domains, When the policy is evaluated, Then
applygives no consumer side.
User Story 2 - Pick a certificate authority per domain (Priority: P1)
A domain that browsers open needs a certificate from a public CA. A domain
that only the platform opens can use the private CA of OpenBao, or a
self-signed certificate. tls-routing.nix sorts each domain into one of
these routes. hosts-common.nix turns the result into per-domain provider
overrides.
Why this priority: a wrong route gives either a browser warning or a failed ACME order for a name the internet cannot reach.
Independent Test: nix-unit-setups-tls-routing,
nix-unit-setups-wildcard-tls and nix-unit-setups-served-domains.
Acceptance Scenarios:
- Given a domain of a component with
certAudience = "browser", Whenpartitionruns, Then the domain goes toacmeDomains. - Given a domain of a component with
certAudience = "bootstrap"that is not a UI domain, Whenpartitionruns, Then the domain goes topkiDomains. - Given the group has a PKI, When
pkiPlanruns, Then the valkey, postgresql and forward-auth UI domains go to theopenbao-pkiprovider. Without a PKI, they go toself-signed.pkiPlanreads whether the group has a provider of each resource throughhasProvider, keyed by resource name. - Given a wildcard domain routed to
acme, When the node is evaluated, Then the node gets asecurity.acme.certsentry with a DNS-01 provider. A non-wildcard domain uses the nginxenableACME.
User Story 3 - Issue and renew private certificates from OpenBao (Priority: P2)
A node logs in to OpenBao with its SPIFFE identity. It issues each certificate from the PKI mount. Every hour, it renews each certificate that is in the last third of its lifetime, and restarts the services that use that certificate.
Why this priority: private certificates expire after 72h. Without
renewal, the internal endpoints break after three days.
Independent Test: integrations-openbao-pki and nix-unit-resources-ssl-openbao-pki-login.
Acceptance Scenarios:
- Given a domain routed to
openbao-pki, Whenopenbao-pki-<certName>runs, Then it runsbao write <mount>/issue/<role>and writes/run/openbao-pki/<certName>/cert.pem(mode0644) andkey.pem(modekeyMode, default0640). - Given a new certificate, When the unit writes it, Then the unit runs
systemctl reset-failedandsystemctl reload-or-restart --no-blockon each unit inrestartServices. - Given a node without its own SPIRE login, When
vars-openbao-loginruns, Then it fetches an SVID, runsbao login -method=certand writes/run/vars-openbao/token. - Given the SPIRE agent is not healthy before the deadline, When
vars-openbao-logintimes out, Then it exits with code3and systemd does not restart it. - Given a node with
vars-openbao-loginfromopenbao-pki-login.nix, When the node is evaluated, Then the login is ordered before theopenbao-pki-<certName>unit of each domain routed toopenbao-pkiand before no other unit. Each of these units also orders itself after the login and wants it.
User Story 4 - Trust certificates that other nodes serve (Priority: P2)
A node connects to services on other nodes over TLS. It must trust their
certificates. For a self-signed domain, the node bakes the certificate into
security.pki.certificateFiles. For a private CA, the node fetches the CA and
builds a trust bundle.
Why this priority: without trust, every cross-node TLS connection fails the certificate check.
Independent Test: nix-unit-setups-pki-trust.
Acceptance Scenarios:
- Given a trust domain routed to
self-signed, When the node is evaluated, Then the certificate of that domain is insecurity.pki.certificateFiles. - Given
caCertFileandcaFetchUrlare set, Whenopenbao-pki-ca-fetchruns, Then it fetches the CA over plain HTTP, and a timer fetches it again every 15 minutes. - Given
caCertFileis set, Whenopenbao-pki-trustruns, Then it writes/etc/ssl/certs/openbao-pki-bundle.crtfromsecurity.pki.caBundleplus the CA, andSSL_CERT_FILEpoints to that bundle.
Edge Cases
- A wildcard domain routes to
acmeandfediversity.acme.dnsProvideris null: an assertion fails at eval. - A self-signed certificate is built at eval through IFD. Its derivation name carries 12 characters of the certificate digest, so a new certificate changes the name.
- The CA fetch fails: the unit fails soft, and the timer tries again.
- OpenBao is not reachable at issue time:
openbao-pki-<certName>restarts after 10 seconds without a start limit. - The OpenBao address is
httpsandcaCertFileis set: the issue unit setsVAULT_CACERT.
Requirements (mandatory)
Functional Requirements
- FR-001: The policy MUST route each domain through
contracts.ssl.want.<consumer>.<domain>.request.domain. - FR-002: The policy MUST make nginx vhosts only for domains that nginx terminates.
- FR-003: The policy MUST set
enableACMEonly for a non-wildcard domain routed toacme, and MUST setuseACMEHostfor a wildcard domain. - FR-004:
crossNodeMUST give the results, thebakeableDomains(the domains routed toself-signed) and thepkiCaCertFiles. - FR-005: The
self-signedprovider MUST make an RSA 4096 certificate withCN=<domain>, a SAN for the domain plusextraSubjectAltNames, and a validity of 36500 days. - FR-006: The
acmeprovider MUST use Let's Encrypt whenserveris null, and HTTP-01 only whendnsProvideris null. - FR-007: The
openbao-pkiprovider MUST issue withttl(default72h). Every hour, it MUST renew a certificate that is missing, that is in the last third of its lifetime, or that does not chain tocaCertFile, and only then restart the services that use it. - FR-008:
vars-openbao-loginMUST refresh the OpenBao token every 30 minutes. - FR-009: When
systemd-creds-openbaois enabled,vars-openbao-loginMUST reload or restart it after a login. - FR-010:
openbao-pki-trust.nixMUST setSSL_CERT_FILEboth as an environment variable and in the systemd managerDefaultEnvironment.
Key Entities
- ssl request:
domains(served here) andtrustDomains(served elsewhere, trusted here). - ssl policy:
routingand the read-onlycrossNode. - ssl providers:
self-signed,acme(options underfediversity.acme) andopenbao-pki(options underfediversity.openbao-pki). - Provider request fields:
domain,keyOwner,keyGroup,keyModeandrestartServices. - Routing plan:
pkiDomains,forwardAuthPkiDomains,acmeDomains,challengeNodes,selfSignedDomainsandallPkiDomainsfromtls-routing.nix.
Success Criteria (mandatory)
Measurable Outcomes
- SC-001:
contracts-ssl-self-signedandintegrations-openbao-pkipass. - SC-002:
nix-unit-setups-tls-routing,nix-unit-setups-pki-trust,nix-unit-setups-wildcard-tls,nix-unit-setups-ssl-key-accounts,nix-unit-setups-served-domainsandnix-unit-resources-sslpass. - SC-003: A browser opens each public domain without a certificate warning.
Assumptions
- The wiring and the setup flow are as in 005. The request and policy model is as in 001.
- The private CA is the PKI mount of the
openbaocomponent. The node identity comes from thespirecomponent. - The DNS-01 challenge needs a zone that the platform publishes, see the
bindcomponent. - The
smtp,oidcandpostgresqlresources declare their ownsslDomains. Seesmtp,oidcandpostgresql. - No check runs the
acmeprovider against an ACME server.
Known Deviations
None.