You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Registrar certificates: self-issue and renew both leaves from the daemon
Summary
The registrar authenticates to bootroot's endpoint with a client certificate and holds no OpenBao credential at all; the endpoint in turn presents a server certificate the registrar pins. Neither leaf exists yet, and neither can be produced by any path in this repository today. This group delivers both. It is a grouping issue and carries no work of its own.
That neither leaf is mintable today is by construction rather than by omission. The client-identity issue defines two reserved names — one for the registrar's client leaf and one for the endpoint's server leaf — and its reserved-prefix guard makes both unmintable through service add. Without this group the endpoint has no certificate to present in production and the registrar has no identity to present to it.
The producer must be the daemon, under the bootroot-internal privileged credential, and not the per-service ACME agent loop. That loop authenticates with a role_id + secret_id, so routing either leaf through it would make the effort's top-level claim — no registrar secret_id at all — false one layer down, reintroducing exactly the expiring-secret trap the certificate form was chosen to escape, and hiding it where it would surface months later as an apparently unrelated failure.
The seam between the children is issuance versus staying valid. The first child mints both leaves on demand under the internal credential and writes them to the paths the endpoint and the registrar read, exposing the issuance step as a callable unit. The second child keeps them valid over time: it drives that unit from the daemon's existing certificate-renewal mechanism rather than adding a second scheduler, and adds the three things renewal needs to be trustworthy — the reload contract, the health surface, and the lapse classification. They are strictly ordered: renewal depends on issuance.
Children
Self-issue the registrar surface's two certificates from the daemon — Surface
Renew the registrar surface's certificates, with a reload contract and a lapse surface — Surface
Shared background
No AppRole, and the claim holds one layer down. Neither issuance nor renewal may read a role_id or a secret_id anywhere. This is satisfiable in the strict form because the bootroot-internal credential the work runs under is itself not an AppRole — its settled form is OpenBao TLS certificate auth — so neither child has to argue the assertion down to "not the per-service AppRole". An implementation that finds itself provisioning a secret_id to issue or renew either leaf has violated the design and should say so rather than proceed.
There is no bootstrap paradox, and neither child may invent one. Issuance runs over the daemon's existing outbound ACME path to the local step-ca; it does not go through the registrar endpoint. The daemon can therefore mint its own server leaf on first start, before the endpoint has ever accepted a connection. The endpoint must never become a prerequisite for obtaining the certificate the endpoint presents.
Every issuance generates a fresh key pair, so every invariant a renewal preserves is a name invariant. Never a key or a fingerprint invariant. After a renewal the client leaf must still satisfy the issuance issue's identity invariants with the same instance, host and configured domain, and the recognition predicate must return the same parsed triple. The server leaf must still satisfy the pinning helper's exact-SAN check and still chain to an anchor already listed in the pin file — and because that pin is over trust anchors rather than leaves, a renewal leaves the pin file untouched. Neither child writes it.
Renewal without a reload contract fails in a well-known way, and it is closed here rather than discovered in production. The daemon writes a renewed certificate to disk, nothing tells the reader to reload it, the reader keeps presenting or accepting the expired leaf, and a fleet-wide enrollment outage gets reported as a credential problem. That is the nginx-reload class of bug. Both sides are the renewal child's obligation: server-side it drives the swap seam the mTLS issue exposes rather than rebuilding the listener, and caller-side the registrar re-reads the certificate on every dial — preferred because it needs no cross-process signal — with a bounded retry over the non-atomic window in which the pair on disk is torn.
A lapse is permanent-until-an-operator-acts, and both remediations point at the daemon. A lapsed registrar stops every enrollment in the deployment and the registrar cannot re-mint itself, so a lapse must be reported as a typed non-retryable error and never retried. Recovery is the daemon: starting it if it is down, which repairs an already-expired leaf at start before the endpoint loads, or letting it reach its next renewal tick if it is already running. It is never re-running the provisioning tool, which supplies only the initial client certificate and nothing more. There is an ordering trap worth stating once for both children, and it is the naive behaviour the RFC describes, not what this group builds: a caller that simply dials and reports whatever fails would, with the daemon down through the renewal deadline, report the endpoint unreachable first and only after an operator started the daemon report the credential invalid — one cause, two remediations, the second historically the wrong one. What this group implements instead is per-leaf and reports once. The caller checks its own client leaf before dialing, so a client leaf that expired while the daemon was down is reported as the credential lapse straight away, with the daemon as the remedy, rather than as unreachable; and because starting the daemon repairs an expired leaf before the endpoint's loader runs, the operator never then meets a second, different symptom for the same cause. The trap survives only in the sub-case where issuance itself fails at start: the daemon refuses to come up, the caller goes on reporting the endpoint unreachable, and the remedy is the failure named in the startup diagnostic. The remaining lifetime is surfaced before it becomes a lapse, so a failing renewal is reported rather than discovered when the next install fails.
Extend the existing renewal mechanism; do not build a second one. By the time the renewal child starts, the daemon is already renewing its own OpenBao client certificate on a schedule over the same outbound path. Three daemon-held certificates renewed by three independently written schedulers is the outcome to avoid — they drift on lead time, retry and failure reporting. Keep the distinction in the reporting though: the internal credential's certificate lapsing costs the daemon its OpenBao access and is recovered by that issue's re-provisioning command, whereas these two lapsing costs the endpoint its callers and is recovered by a renewal tick.
Both children run only where the endpoint runs.bootroot-agent is a fleet-wide binary on every service host, and the endpoint is off by default and enabled by explicit configuration on the bootroot host alone. Issuance and renewal read that same setting: on a host where the endpoint is disabled, neither leaf is issued, neither is renewed, no material path is created and no health entry is reported. Getting this wrong is not cosmetic — every agent in the deployment would otherwise try to mint two reserved-name certificates it has no use for and no credential to obtain.
Two certificate/key pairs, at four configured paths, and only one pair already has configuration settings. The mTLS issue introduces the server pair's two settings because its startup loader reads them. The client pair's two settings do not exist yet; the issuance child adds them, and they are a published contract rather than an internal detail, because the process that reads them is the co-located registrar in another repository and the provisioning tool places the initial client certificate there. The endpoint pin file lives in that client certificate's directory, so the client path settings are also what fix where the pin file is found. (Read "pin" throughout this group as the anchor pin in that file; the configured paths are settings, not pins.)
The daemon is not the only writer of the client leaf, and the division is by lifecycle rather than by leaf. The provisioning tool places the initial client certificate at install and does nothing further; the daemon issues that leaf when it is unusable and renews it from then on. So the safety argument for replacing material is not "only the daemon writes here" — it is that neither reserved name can be minted through service add, so no operator-authored service identity can be sitting at either path to clobber. Whatever an issuance replaces is either this daemon's own earlier leaf or the provisioning tool's initial one, both carrying the same reserved name for the same purpose. The server leaf does have one writer only, the daemon, because nothing else in the deployment can produce that name.
"Unusable" includes expired and chain-drifted, and that is what keeps the lifecycle statements in this group from contradicting each other. The issuance child defines a pair as usable only when it is present, readable, key-matched, correctly named, inside its validity window and still chaining to the configured trust anchors, and repairs every other state at daemon start, ahead of the endpoint's loader. Read the group's lifecycle rules against that one definition and they agree: a leaf that became unusable while the daemon was down — expired, or left behind by a trust-anchor rotation — is repaired by starting the daemon; a leaf approaching expiry while it runs is renewed by the renewal child on its lead time, and one that drifts off the anchors while it runs is reissued by the same tick on its second trigger; and observing a lapse never repairs anything, which is the reporting child's rule about the request path. There is no arrangement in which the endpoint comes up enabled and unable to serve because a leaf had expired — either issuance repaired it, or issuance failed and the daemon refused to start with a diagnostic. Neither renewal nor reporting adds a second start-time repair path.
[DEPARTURE FROM THE ACCEPTED RFC.] That start-time repair is a deliberate refinement, and this group records it rather than letting it read as decomposition. The RFC assigns lapse recovery to bootrootd "reaching its next renewal tick", and its ordering trap has the registrar reporting the endpoint unreachable and then, after an operator starts the daemon, reporting the credential invalid. Repairing at start collapses that second symptom for that scenario. The reasons are in the issuance child, which owns the departure and the obligation to amend the RFC's §4 ordering-trap paragraph rather than merely flagging it — a discrepancy left "raised" with nobody holding the edit is what this group is avoiding; what the group needs stated is the consequence for its other two children. What the RFC requires still holds: recovery is bootrootd's and never re-provisioning the host, and both symptoms of the trap still point at bootrootd — with a new sub-case, the daemon that refuses to start because issuance failed, keeping the first symptom alive for an honest reason. What changes for this group, stated per leaf because the two are not symmetric: what start-time repair removes is the server-side arrival path — an endpoint can no longer come up serving or holding an expired leaf, so no test in this group may assume one, and a server-side lapse is exercised by swapping an expired leaf into an already-running endpoint. It does not remove the client-side one: the caller reads its own configured client material directly and refuses before dialing, so a daemon that was down through the client leaf's not_after and has not yet been started still produces an observable lapse, in production and not only in a test. That case is deliberately kept and is the reporting child's highest-precedence condition. What start-time repair changes for it is the sequence, not the reachability: starting the daemon repairs the leaf before the endpoint loads, so the operator sees one credential-lapse report with the daemon as the remedy rather than unreachable first and credential invalid after. The other reachable arrival path is unchanged — a running daemon whose renewals keep failing from the lead-time point through not_after.
A lapse is reported in two different shapes, and only one of them is on the wire. The health member the reporting child populates rides ordinary responses, so it is wire-visible and fixture-pinned. The lapse error is not: a lapsed client leaf is caught before dialing and a lapsed server leaf fails the handshake, and in neither case does a response exist to carry an identifier. So the lapse error is a local typed error in this repository's client, asserted by variant; translating it into the ecosystem's permanent unavailability reason happens in the co-located registrar's repository, out of what it observes. Neither child adds a wire identifier or a reason for it.
Health is one disjoint member of a shared container this group does not own. The endpoint protocol issue pins the container's name, its shape and its placement on all three response shapes — mint success, deregister success and refusal — and assigns the certificates member to this group. This group defines and populates that member and nothing else: it does not name the container, does not decide which responses carry it, and adds no second status surface. The limiter's and the audit store's members belong to other issues; all three are disjoint, none may reshape the container or narrow its placement, and there is no ordering edge in any direction. One consequence is worth stating because it is the point of the placement: a renewal that is failing stays visible on responses that are refusals for entirely unrelated reasons, which is exactly when an operator needs it.
The docs/ protocol reference has two halves and this group writes only one. The wire-contract issue owns the transcribed half — every externally owned field name, error identifier, class and RegistrarUnavailable reason — and that half is read-only here. The health member's schema is a bootroot-owned decision and goes in the local half beside it, on the same footing as the container itself and the other two members' schemas. Neither child may add, rename, re-spell or reclassify anything external; the lapse error stays a local typed error rather than acquiring a transcribed reason, for the reason given above.
Execution order
Issues in the same wave have no unmet dependencies among these children and can run in parallel.
graph TD
issue767["#767 Self-issue the registrar surface's two certificates from the daemon [phase: Surface]"]
issue770["#770 Renew the registrar surface's certificates, with a reload contract and a lapse surface [phase: Surface]"]
issue766["#766 Provision the bootroot-internal privileged credential and run the verbs under it [phase: Surface]"]
issue766 -.-> issue767
issue760["#760 Issue a distinguishable registrar client identity and pin the endpoint's server identity [phase: Surface]"]
issue760 -.-> issue767
issue764["#764 Terminate mTLS on the registrar endpoint and scope it to the registrar identity [phase: Surface]"]
issue764 -.-> issue767
issue767 --> issue770
issue762["#762 Implement the registrar endpoint's versioned wire protocol and its codec [phase: Surface]"]
issue762 -.-> issue770
Registrar certificates: self-issue and renew both leaves from the daemon
Summary
The registrar authenticates to bootroot's endpoint with a client certificate and holds no OpenBao credential at all; the endpoint in turn presents a server certificate the registrar pins. Neither leaf exists yet, and neither can be produced by any path in this repository today. This group delivers both. It is a grouping issue and carries no work of its own.
That neither leaf is mintable today is by construction rather than by omission. The client-identity issue defines two reserved names — one for the registrar's client leaf and one for the endpoint's server leaf — and its reserved-prefix guard makes both unmintable through
service add. Without this group the endpoint has no certificate to present in production and the registrar has no identity to present to it.The producer must be the daemon, under the bootroot-internal privileged credential, and not the per-service ACME agent loop. That loop authenticates with a
role_id+secret_id, so routing either leaf through it would make the effort's top-level claim — no registrarsecret_idat all — false one layer down, reintroducing exactly the expiring-secret trap the certificate form was chosen to escape, and hiding it where it would surface months later as an apparently unrelated failure.The seam between the children is issuance versus staying valid. The first child mints both leaves on demand under the internal credential and writes them to the paths the endpoint and the registrar read, exposing the issuance step as a callable unit. The second child keeps them valid over time: it drives that unit from the daemon's existing certificate-renewal mechanism rather than adding a second scheduler, and adds the three things renewal needs to be trustworthy — the reload contract, the health surface, and the lapse classification. They are strictly ordered: renewal depends on issuance.
Children
Shared background
No AppRole, and the claim holds one layer down. Neither issuance nor renewal may read a
role_idor asecret_idanywhere. This is satisfiable in the strict form because the bootroot-internal credential the work runs under is itself not an AppRole — its settled form is OpenBao TLS certificate auth — so neither child has to argue the assertion down to "not the per-service AppRole". An implementation that finds itself provisioning asecret_idto issue or renew either leaf has violated the design and should say so rather than proceed.There is no bootstrap paradox, and neither child may invent one. Issuance runs over the daemon's existing outbound ACME path to the local step-ca; it does not go through the registrar endpoint. The daemon can therefore mint its own server leaf on first start, before the endpoint has ever accepted a connection. The endpoint must never become a prerequisite for obtaining the certificate the endpoint presents.
Every issuance generates a fresh key pair, so every invariant a renewal preserves is a name invariant. Never a key or a fingerprint invariant. After a renewal the client leaf must still satisfy the issuance issue's identity invariants with the same instance, host and configured domain, and the recognition predicate must return the same parsed triple. The server leaf must still satisfy the pinning helper's exact-SAN check and still chain to an anchor already listed in the pin file — and because that pin is over trust anchors rather than leaves, a renewal leaves the pin file untouched. Neither child writes it.
Renewal without a reload contract fails in a well-known way, and it is closed here rather than discovered in production. The daemon writes a renewed certificate to disk, nothing tells the reader to reload it, the reader keeps presenting or accepting the expired leaf, and a fleet-wide enrollment outage gets reported as a credential problem. That is the nginx-reload class of bug. Both sides are the renewal child's obligation: server-side it drives the swap seam the mTLS issue exposes rather than rebuilding the listener, and caller-side the registrar re-reads the certificate on every dial — preferred because it needs no cross-process signal — with a bounded retry over the non-atomic window in which the pair on disk is torn.
A lapse is permanent-until-an-operator-acts, and both remediations point at the daemon. A lapsed registrar stops every enrollment in the deployment and the registrar cannot re-mint itself, so a lapse must be reported as a typed non-retryable error and never retried. Recovery is the daemon: starting it if it is down, which repairs an already-expired leaf at start before the endpoint loads, or letting it reach its next renewal tick if it is already running. It is never re-running the provisioning tool, which supplies only the initial client certificate and nothing more. There is an ordering trap worth stating once for both children, and it is the naive behaviour the RFC describes, not what this group builds: a caller that simply dials and reports whatever fails would, with the daemon down through the renewal deadline, report the endpoint unreachable first and only after an operator started the daemon report the credential invalid — one cause, two remediations, the second historically the wrong one. What this group implements instead is per-leaf and reports once. The caller checks its own client leaf before dialing, so a client leaf that expired while the daemon was down is reported as the credential lapse straight away, with the daemon as the remedy, rather than as unreachable; and because starting the daemon repairs an expired leaf before the endpoint's loader runs, the operator never then meets a second, different symptom for the same cause. The trap survives only in the sub-case where issuance itself fails at start: the daemon refuses to come up, the caller goes on reporting the endpoint unreachable, and the remedy is the failure named in the startup diagnostic. The remaining lifetime is surfaced before it becomes a lapse, so a failing renewal is reported rather than discovered when the next install fails.
Extend the existing renewal mechanism; do not build a second one. By the time the renewal child starts, the daemon is already renewing its own OpenBao client certificate on a schedule over the same outbound path. Three daemon-held certificates renewed by three independently written schedulers is the outcome to avoid — they drift on lead time, retry and failure reporting. Keep the distinction in the reporting though: the internal credential's certificate lapsing costs the daemon its OpenBao access and is recovered by that issue's re-provisioning command, whereas these two lapsing costs the endpoint its callers and is recovered by a renewal tick.
Both children run only where the endpoint runs.
bootroot-agentis a fleet-wide binary on every service host, and the endpoint is off by default and enabled by explicit configuration on the bootroot host alone. Issuance and renewal read that same setting: on a host where the endpoint is disabled, neither leaf is issued, neither is renewed, no material path is created and no health entry is reported. Getting this wrong is not cosmetic — every agent in the deployment would otherwise try to mint two reserved-name certificates it has no use for and no credential to obtain.Two certificate/key pairs, at four configured paths, and only one pair already has configuration settings. The mTLS issue introduces the server pair's two settings because its startup loader reads them. The client pair's two settings do not exist yet; the issuance child adds them, and they are a published contract rather than an internal detail, because the process that reads them is the co-located registrar in another repository and the provisioning tool places the initial client certificate there. The endpoint pin file lives in that client certificate's directory, so the client path settings are also what fix where the pin file is found. (Read "pin" throughout this group as the anchor pin in that file; the configured paths are settings, not pins.)
The daemon is not the only writer of the client leaf, and the division is by lifecycle rather than by leaf. The provisioning tool places the initial client certificate at install and does nothing further; the daemon issues that leaf when it is unusable and renews it from then on. So the safety argument for replacing material is not "only the daemon writes here" — it is that neither reserved name can be minted through
service add, so no operator-authored service identity can be sitting at either path to clobber. Whatever an issuance replaces is either this daemon's own earlier leaf or the provisioning tool's initial one, both carrying the same reserved name for the same purpose. The server leaf does have one writer only, the daemon, because nothing else in the deployment can produce that name."Unusable" includes expired and chain-drifted, and that is what keeps the lifecycle statements in this group from contradicting each other. The issuance child defines a pair as usable only when it is present, readable, key-matched, correctly named, inside its validity window and still chaining to the configured trust anchors, and repairs every other state at daemon start, ahead of the endpoint's loader. Read the group's lifecycle rules against that one definition and they agree: a leaf that became unusable while the daemon was down — expired, or left behind by a trust-anchor rotation — is repaired by starting the daemon; a leaf approaching expiry while it runs is renewed by the renewal child on its lead time, and one that drifts off the anchors while it runs is reissued by the same tick on its second trigger; and observing a lapse never repairs anything, which is the reporting child's rule about the request path. There is no arrangement in which the endpoint comes up enabled and unable to serve because a leaf had expired — either issuance repaired it, or issuance failed and the daemon refused to start with a diagnostic. Neither renewal nor reporting adds a second start-time repair path.
[DEPARTURE FROM THE ACCEPTED RFC.] That start-time repair is a deliberate refinement, and this group records it rather than letting it read as decomposition. The RFC assigns lapse recovery to bootrootd "reaching its next renewal tick", and its ordering trap has the registrar reporting the endpoint unreachable and then, after an operator starts the daemon, reporting the credential invalid. Repairing at start collapses that second symptom for that scenario. The reasons are in the issuance child, which owns the departure and the obligation to amend the RFC's §4 ordering-trap paragraph rather than merely flagging it — a discrepancy left "raised" with nobody holding the edit is what this group is avoiding; what the group needs stated is the consequence for its other two children. What the RFC requires still holds: recovery is bootrootd's and never re-provisioning the host, and both symptoms of the trap still point at bootrootd — with a new sub-case, the daemon that refuses to start because issuance failed, keeping the first symptom alive for an honest reason. What changes for this group, stated per leaf because the two are not symmetric: what start-time repair removes is the server-side arrival path — an endpoint can no longer come up serving or holding an expired leaf, so no test in this group may assume one, and a server-side lapse is exercised by swapping an expired leaf into an already-running endpoint. It does not remove the client-side one: the caller reads its own configured client material directly and refuses before dialing, so a daemon that was down through the client leaf's
not_afterand has not yet been started still produces an observable lapse, in production and not only in a test. That case is deliberately kept and is the reporting child's highest-precedence condition. What start-time repair changes for it is the sequence, not the reachability: starting the daemon repairs the leaf before the endpoint loads, so the operator sees one credential-lapse report with the daemon as the remedy rather than unreachable first and credential invalid after. The other reachable arrival path is unchanged — a running daemon whose renewals keep failing from the lead-time point throughnot_after.A lapse is reported in two different shapes, and only one of them is on the wire. The health member the reporting child populates rides ordinary responses, so it is wire-visible and fixture-pinned. The lapse error is not: a lapsed client leaf is caught before dialing and a lapsed server leaf fails the handshake, and in neither case does a response exist to carry an identifier. So the lapse error is a local typed error in this repository's client, asserted by variant; translating it into the ecosystem's permanent unavailability reason happens in the co-located registrar's repository, out of what it observes. Neither child adds a wire identifier or a reason for it.
Health is one disjoint member of a shared container this group does not own. The endpoint protocol issue pins the container's name, its shape and its placement on all three response shapes — mint success, deregister success and refusal — and assigns the
certificatesmember to this group. This group defines and populates that member and nothing else: it does not name the container, does not decide which responses carry it, and adds no second status surface. The limiter's and the audit store's members belong to other issues; all three are disjoint, none may reshape the container or narrow its placement, and there is no ordering edge in any direction. One consequence is worth stating because it is the point of the placement: a renewal that is failing stays visible on responses that are refusals for entirely unrelated reasons, which is exactly when an operator needs it.The
docs/protocol reference has two halves and this group writes only one. The wire-contract issue owns the transcribed half — every externally owned field name, error identifier, class andRegistrarUnavailablereason — and that half is read-only here. The health member's schema is a bootroot-owned decision and goes in the local half beside it, on the same footing as the container itself and the other two members' schemas. Neither child may add, rename, re-spell or reclassify anything external; the lapse error stays a local typed error rather than acquiring a transcribed reason, for the reason given above.Execution order
Issues in the same wave have no unmet dependencies among these children and can run in parallel.
External dependencies: