@@ -124,9 +124,11 @@ func (c *CA) Init(ctx context.Context) error {
124124 // point rather than an oversight: InitHMAC runs on every start, and the
125125 // fast path a few lines down deliberately loads an already-bootstrapped CA
126126 // without taking a distributed lock. Moving it inside would make every
127- // replica's every start contend for `bootstrap`, and would enlarge #201's
128- // re-entrancy hazard rather than avoid it. Once the key exists EnsureHMACKey
129- // takes no lock either, so a warm start still costs no lock at all.
127+ // replica's every start contend for `bootstrap`, and would enlarge the
128+ // bootstrap critical section that #201's re-entrancy was found inside. That
129+ // gap is fixed below; the contention argument stands on its own. Once the
130+ // key exists EnsureHMACKey takes no lock either, so a warm start still costs
131+ // no lock at all.
130132 hmacCtx , cancelHMAC := context .WithTimeout (ctx , LockTimeout )
131133 errHMAC := c .Storage .InitHMAC (hmacCtx )
132134 cancelHMAC ()
@@ -147,7 +149,7 @@ func (c *CA) Init(ctx context.Context) error {
147149 // distributed lock. Once a CA exists, all replicas can read it.
148150 loadErr := c .loadCA (ctx )
149151 if loadErr == nil {
150- return c .finishLoadExisting (ctx )
152+ return c .finishLoadExisting (ctx , c . seedSupportingState )
151153 }
152154
153155 // When using an external signer (key isolation mode), the frontend must
@@ -166,7 +168,9 @@ func (c *CA) Init(ctx context.Context) error {
166168 return c .Storage .WithLock (ctx , lockNameBootstrap , func () error {
167169 if err := c .loadCA (ctx ); err == nil {
168170 slog .Info ("Loaded CA bootstrapped by another replica" , "cert" , c .Storage .CACertPath ())
169- return c .finishLoadExisting (ctx )
171+ // ...Locked: we are inside the bootstrap critical section, and
172+ // WithLock is not reentrant. See finishLoadExisting's comment.
173+ return c .finishLoadExisting (ctx , c .seedSupportingStateLocked )
170174 }
171175 hasCert , errCert := c .Storage .HasCACert (ctx )
172176 if errCert != nil {
@@ -237,9 +241,24 @@ func (c *CA) Init(ctx context.Context) error {
237241// cache). c.mu must be held by the caller. When the CRL is absent from the
238242// backend but the cert+key loaded successfully — the common case when an
239243// existing CA cert/key is mounted via an overlay against a fresh remote
240- // backend — seed the CRL, inventory, and serial counter under the bootstrap
241- // lock so startup can complete.
242- func (c * CA ) finishLoadExisting (ctx context.Context ) error {
244+ // backend — the CRL, inventory and serial counter are seeded so startup can
245+ // complete.
246+ //
247+ // That seeding is the only part of this function that takes a distributed
248+ // lock, and only the caller knows whether the bootstrap lock is already held,
249+ // so the caller passes the variant to use. Init's fast path holds nothing and
250+ // passes seedSupportingState, which acquires bootstrap; Init's slow path is
251+ // already inside the bootstrap critical section and passes
252+ // seedSupportingStateLocked, which does the same work without acquiring it
253+ // again. finishLoadExisting itself acquires nothing either way.
254+ //
255+ // Making that the caller's choice is not a stylistic preference. WithLock is
256+ // not reentrant at any tier — every implementation takes a plain per-name
257+ // sync.Mutex before it touches the network or the filesystem, and a plain
258+ // sync.Mutex ignores the context — so a second acquisition on this goroutine
259+ // does not fail at LockTimeout, it hangs startup outright. The slow path used
260+ // to call the acquiring variant from inside the lock, which is #201.
261+ func (c * CA ) finishLoadExisting (ctx context.Context , seed func (context.Context ) error ) error {
243262 slog .Info ("Loaded existing CA" , "cert" , c .Storage .CACertPath ())
244263 if err := c .buildSerialIndex (ctx ); err != nil {
245264 slog .Warn ("Failed to build OCSP serial index" , "error" , err )
@@ -257,7 +276,7 @@ func (c *CA) finishLoadExisting(ctx context.Context) error {
257276 if c .ExternalSigner != nil {
258277 return fmt .Errorf ("failed to load CRL into memory: %w" , err )
259278 }
260- if err := c . seedSupportingState (ctx ); err != nil {
279+ if err := seed (ctx ); err != nil {
261280 return fmt .Errorf ("seeding CA supporting state: %w" , err )
262281 }
263282 if err := c .loadCRLCache (ctx ); err != nil {
@@ -271,61 +290,70 @@ func (c *CA) finishLoadExisting(ctx context.Context) error {
271290
272291// seedSupportingState writes the public key, CRL, inventory, and serial counter
273292// that bootstrapCA would normally create, for the case where the cert+key
274- // already exist (e.g. mounted via an overlay against an empty backend). Runs
275- // under the bootstrap lock so concurrent replicas don't race to seed.
276- //
277- // The public key is also a backfill: a bootstrap that failed on that blob
278- // leaves a cert and key that load cleanly forever after, so this is the only
279- // path that would ever write it again.
293+ // already exist (e.g. mounted via an overlay against an empty backend). It
294+ // takes the bootstrap lock so concurrent replicas don't race to seed, so the
295+ // caller must NOT already hold that lock — one that does wants
296+ // seedSupportingStateLocked below, because WithLock is not reentrant.
280297func (c * CA ) seedSupportingState (ctx context.Context ) error {
281298 ctx , cancel := context .WithTimeout (ctx , LockTimeout )
282299 defer cancel ()
283300 return c .Storage .WithLock (ctx , lockNameBootstrap , func () error {
284- // The public key first, and before the CRL short-circuit below, because
285- // this is the one blob nothing else ever rewrites: bootstrapCA writes it
286- // before the CRL, so a bootstrap that failed on it leaves a CA whose key
287- // and certificate load cleanly forever after while ca_pub.pem stays
288- // missing. Taken from the certificate rather than the key because it is
289- // the public component every consumer already trusts, and because it is
290- // available whether the key is local or at a provider. Idempotent, so a
291- // replica that lost the race simply rewrites identical bytes.
292- if err := savePubKeyPEM (ctx , c .Storage , c .CACert .PublicKey ); err != nil {
293- return fmt .Errorf ("writing CA public key: %w" , err )
294- }
301+ return c .seedSupportingStateLocked (ctx )
302+ })
303+ }
295304
296- // Another replica may have seeded between our initial check and the
297- // lock acquisition.
298- if _ , err := c .Storage .GetCRL (ctx ); err == nil {
299- return nil
300- } else if ! errors .Is (err , fs .ErrNotExist ) {
301- return fmt .Errorf ("re-checking CRL: %w" , err )
302- }
303- crl , err := newEmptyCRL (c .CACert , c .CAKey , c .CRLValidityDuration ())
304- if err != nil {
305- return fmt .Errorf ("creating initial CRL: %w" , err )
306- }
307- crlPEM := pem .EncodeToMemory (& pem.Block {Type : "X509 CRL" , Bytes : crl .Raw })
308- // Bootstrap-only write: runs before any CRL consumer exists, so it
309- // deliberately skips the crlNotify signal (see signCRLLocked).
310- if err := c .Storage .UpdateCRL (ctx , crlPEM ); err != nil {
311- return fmt .Errorf ("writing initial CRL: %w" , err )
312- }
313- if err := c .Storage .TouchInventory (ctx ); err != nil {
314- return fmt .Errorf ("creating inventory: %w" , err )
315- }
316- hasSerial , err := c .Storage .HasSerial (ctx )
317- if err != nil {
318- return fmt .Errorf ("checking serial: %w" , err )
319- }
320- if ! hasSerial {
321- if err := c .Storage .WriteSerial (ctx , "0001" ); err != nil {
322- return fmt .Errorf ("writing serial: %w" , err )
323- }
324- }
325- slog .Info ("Seeded CA supporting state for existing cert+key" ,
326- "cert" , c .Storage .CACertPath ())
305+ // seedSupportingStateLocked does the seeding itself. The caller must hold the
306+ // bootstrap lock, and c.mu along with it — Init is the only caller of either
307+ // variant, and it holds c.mu across the whole of itself.
308+ //
309+ // The public key is also a backfill: a bootstrap that failed on that blob
310+ // leaves a cert and key that load cleanly forever after, so this is the only
311+ // path that would ever write it again.
312+ func (c * CA ) seedSupportingStateLocked (ctx context.Context ) error {
313+ // The public key first, and before the CRL short-circuit below, because
314+ // this is the one blob nothing else ever rewrites: bootstrapCA writes it
315+ // before the CRL, so a bootstrap that failed on it leaves a CA whose key
316+ // and certificate load cleanly forever after while ca_pub.pem stays
317+ // missing. Taken from the certificate rather than the key because it is
318+ // the public component every consumer already trusts, and because it is
319+ // available whether the key is local or at a provider. Idempotent, so a
320+ // replica that lost the race simply rewrites identical bytes.
321+ if err := savePubKeyPEM (ctx , c .Storage , c .CACert .PublicKey ); err != nil {
322+ return fmt .Errorf ("writing CA public key: %w" , err )
323+ }
324+
325+ // Another replica may have seeded between our initial check and the
326+ // lock acquisition.
327+ if _ , err := c .Storage .GetCRL (ctx ); err == nil {
327328 return nil
328- })
329+ } else if ! errors .Is (err , fs .ErrNotExist ) {
330+ return fmt .Errorf ("re-checking CRL: %w" , err )
331+ }
332+ crl , err := newEmptyCRL (c .CACert , c .CAKey , c .CRLValidityDuration ())
333+ if err != nil {
334+ return fmt .Errorf ("creating initial CRL: %w" , err )
335+ }
336+ crlPEM := pem .EncodeToMemory (& pem.Block {Type : "X509 CRL" , Bytes : crl .Raw })
337+ // Bootstrap-only write: runs before any CRL consumer exists, so it
338+ // deliberately skips the crlNotify signal (see signCRLLocked).
339+ if err := c .Storage .UpdateCRL (ctx , crlPEM ); err != nil {
340+ return fmt .Errorf ("writing initial CRL: %w" , err )
341+ }
342+ if err := c .Storage .TouchInventory (ctx ); err != nil {
343+ return fmt .Errorf ("creating inventory: %w" , err )
344+ }
345+ hasSerial , err := c .Storage .HasSerial (ctx )
346+ if err != nil {
347+ return fmt .Errorf ("checking serial: %w" , err )
348+ }
349+ if ! hasSerial {
350+ if err := c .Storage .WriteSerial (ctx , "0001" ); err != nil {
351+ return fmt .Errorf ("writing serial: %w" , err )
352+ }
353+ }
354+ slog .Info ("Seeded CA supporting state for existing cert+key" ,
355+ "cert" , c .Storage .CACertPath ())
356+ return nil
329357}
330358
331359// loadCA reads and validates the CA key and certificate from disk.
0 commit comments