Skip to content

Fix CA ordering so fresh installs boot with etcd mTLS - #964

Merged
phinze merged 1 commit into
mainfrom
phinze/mir-1464-fix-ca-ordering-so-fresh-server-install-clusters-boot-with
Jul 24, 2026
Merged

Fix CA ordering so fresh installs boot with etcd mTLS#964
phinze merged 1 commit into
mainfrom
phinze/mir-1464-fix-ca-ordering-so-fresh-server-install-clusters-boot-with

Conversation

@phinze

@phinze phinze commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

When we turned on etcd mTLS for distributed runners, we introduced a chicken-and-egg problem that only bites on a truly fresh machine. SetupEtcdTLS reads the server CA off disk and gives up with "CA certificate not found" if it isn't there. Trouble is, the only code that creates the CA when it's missing is Coordinator.LoadCA, and that doesn't run until Coordinator.Start, several hundred lines and a bunch of component starts later.

Existing clusters never noticed, because a prior boot had already written the CA, and make dev sidesteps it by running auth generate first. But a production server install starts the systemd service before it generates the CA. Registration runs first and creates a service-account keypair for cloud auth, but nothing writes the server CA until the very end, when configureLocalClient shells out to auth generate. So with distributed runners on by default the first boot fails, crash-loops on Restart=always, and only self-heals once that auth generate writes the CA. The install's readiness check reports failure the whole time. Worse, a bare miren server on an empty data dir with no out-of-band bootstrap fails permanently: the etcd TLS block always errors before LoadCA is ever reached.

The fix is to make the binary self-sufficient. We pulled the create-if-missing logic out of LoadCA into a standalone EnsureCA and call it right before the etcd TLS setup, so the server materializes its own CA on cold boot. It's scoped to the distributed-runners branch, so every other boot path stays byte-for-byte the same. There's a cold-boot test that pins the ordering invariant: a fresh data dir makes SetupEtcdTLS fail, and EnsureCA is what makes it succeed, with the issued etcd certs verified to chain back to the CA.

While extracting the logic, we also tightened the read path so a CA cert that exists but can't be read (permissions, transient IO) is treated as an error rather than absence, instead of silently regenerating over a valid CA and invalidating every cert it issued.

Closes MIR-1464

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 33 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19ab6023-7a5b-4b33-a4d7-ef26de433a75

📥 Commits

Reviewing files that changed from the base of the PR and between 7d41b25 and dfa58c6.

📒 Files selected for processing (3)
  • cli/commands/server.go
  • components/coordinate/coordinate.go
  • components/coordinate/etcd_tls_test.go
📝 Walkthrough

Walkthrough

Adds coordinate.EnsureCA to load or generate the coordinator CA under the server data directory. Embedded etcd startup now ensures the CA exists before configuring TLS and returns errors immediately when materialization fails. LoadCA delegates to the new helper. Tests cover cold-boot TLS setup, certificate validation, expected usages and hostname, and idempotent CA reuse.


Comment @coderabbitai help to get the list of available commands.

@miren-code-agent miren-code-agent Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🍪 biscuit: ✅ ready to merge — auto-review, non-blocking

This is a well-targeted fix for a real bootstrap ordering problem. On a fresh install with DistributedRunners enabled, SetupEtcdTLS was called before Coordinator.Start had a chance to run LoadCA — meaning there was no CA on disk yet, causing a "CA certificate not found" crash-loop. The fix is clean: extract the create-if-missing CA logic into a package-level EnsureCA, then call it in server.go immediately before SetupEtcdTLS. Coordinator.LoadCA now delegates to EnsureCA instead of duplicating the logic, which is a genuine improvement.

The logic is correct. EnsureCA is idempotent (reads existing file first, only generates+writes when absent), so calling it twice — once early in Server(), once again when Coordinator.StartLoadCAEnsureCA runs — is harmless. The both-CA-and-key-on-disk cases are handled correctly, and file permissions are preserved (0644 for certs, 0600 for keys).

The tests are solid for what they cover. TestEtcdTLSColdBoot faithfully reproduces MIR-1464: it fails SetupEtcdTLS with no CA, calls EnsureCA, then verifies the issued certs chain properly. TestEnsureCAIdempotent pins that the second call doesn't regenerate. That's the right set of tests for this change.

One stale comment remains: SetupEtcdTLS's doc says "The CA must already exist (created by the coordinator's LoadCA)" at line 179, but that's no longer true — the callsite in server.go now ensures it via EnsureCA. The comment should be updated to reflect the new invariant (callers must call EnsureCA or LoadCA first). It's not a correctness issue, but it will confuse the next person to read that function.


🍪 full review note · comment /biscuit review to run biscuit again.

Inline comments

components/coordinate/coordinate.go:179

This doc comment is now stale. The CA is no longer guaranteed by Coordinator.LoadCA; it's guaranteed by EnsureCA, which server.go calls before this function. Consider updating to something like: "The CA must already exist on disk; call EnsureCA (or LoadCA) before SetupEtcdTLS."

🤖 Prompt for AI Agents
In components/coordinate/coordinate.go at line
179, the doc comment on SetupEtcdTLS says 'The CA
must already exist (created by the coordinator's
LoadCA).' This is stale — the caller in server.go
now uses EnsureCA directly. Update the comment to
say the CA must already exist on disk, and that
callers should call EnsureCA or Coordinator.LoadCA
before invoking this function.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@components/coordinate/coordinate.go`:
- Around line 472-514: Update the CA loading and generation flow around the
existing certificate/key reads so both files are inspected before deciding to
create a CA: load the pair only when both exist, create only when both return
os.ErrNotExist, and propagate any other read error. Publish the generated
certificate and key using a recoverable atomic pair strategy that prevents a
successful cert write with a failed key write from being treated as a valid or
permanently broken CA; add failure-injection coverage for partial publication.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: b5ec191b-e4ac-437a-8efa-8a00b6378f4b

📥 Commits

Reviewing files that changed from the base of the PR and between 8a3afae and 7d41b25.

📒 Files selected for processing (3)
  • cli/commands/server.go
  • components/coordinate/coordinate.go
  • components/coordinate/etcd_tls_test.go

Comment thread components/coordinate/coordinate.go Outdated
Comment on lines +472 to +514
if data, err := os.ReadFile(cert); err == nil {
c.Log.Info("loading existing CA", "path", cert)
log.Info("loading existing CA", "path", cert)

key, err := os.ReadFile(keyPath)
if err != nil {
return fmt.Errorf("missing key for CA: %w", err)
return nil, fmt.Errorf("missing key for CA: %w", err)
}

ca, err := caauth.LoadFromPEM(data, key)
if err != nil {
return fmt.Errorf("failed to load CA: %w", err)
return nil, fmt.Errorf("failed to load CA: %w", err)
}

c.authority = ca
} else {
c.Log.Info("generating new CA", "path", cert)
return ca, nil
}

ca, err := caauth.New(caauth.Options{
CommonName: "miren-server",
Organization: "miren",
ValidFor: 10 * year,
})
if err != nil {
return fmt.Errorf("failed to generate CA: %w", err)
}
log.Info("generating new CA", "path", cert)

err = os.MkdirAll(filepath.Dir(cert), 0755)
if err != nil {
return fmt.Errorf("failed to create CA directory: %w", err)
}
ca, err := caauth.New(caauth.Options{
CommonName: "miren-server",
Organization: "miren",
ValidFor: 10 * year,
})
if err != nil {
return nil, fmt.Errorf("failed to generate CA: %w", err)
}

cd, kd, err := ca.ExportPEM()
if err != nil {
return fmt.Errorf("failed to export CA: %w", err)
}
if err := os.MkdirAll(filepath.Dir(cert), 0755); err != nil {
return nil, fmt.Errorf("failed to create CA directory: %w", err)
}

err = os.WriteFile(cert, cd, 0644)
if err != nil {
return fmt.Errorf("failed to write CA cert: %w", err)
}
cd, kd, err := ca.ExportPEM()
if err != nil {
return nil, fmt.Errorf("failed to export CA: %w", err)
}

err = os.WriteFile(keyPath, kd, 0600)
if err != nil {
return fmt.Errorf("failed to write CA key: %w", err)
}
if err := os.WriteFile(cert, cd, 0644); err != nil {
return nil, fmt.Errorf("failed to write CA cert: %w", err)
}

c.authority = ca
if err := os.WriteFile(keyPath, kd, 0600); err != nil {
return nil, fmt.Errorf("failed to write CA key: %w", err)
}

@coderabbitai coderabbitai Bot Jul 23, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Preserve CA-pair integrity on failed or interrupted creation.

A successful ca.crt write followed by a failed ca.key write leaves later boots permanently failing at the “missing key” path. Also, any non-NotExist certificate read error is treated as absence and can start CA replacement. Read both files first, create only when both are definitively absent, and use a recoverable atomic publish strategy for the pair. Add failure-injection coverage for partial writes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/coordinate/coordinate.go` around lines 472 - 514, Update the CA
loading and generation flow around the existing certificate/key reads so both
files are inspected before deciding to create a CA: load the pair only when both
exist, create only when both return os.ErrNotExist, and propagate any other read
error. Publish the generated certificate and key using a recoverable atomic pair
strategy that prevents a successful cert write with a failed key write from
being treated as a valid or permanently broken CA; add failure-injection
coverage for partial publication.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the read-error case. Tightened EnsureCA to only regenerate when the cert is genuinely absent (os.ErrNotExist); any other read error now propagates instead of silently overwriting a valid CA, with a test that pins it.

Punting the atomic cert/key publish + partial-write failure injection as out of scope here: it's pre-existing behavior lifted verbatim from LoadCA, and it fails loudly ("missing key for CA") rather than silently. Worth a follow-up if we want the belt-and-suspenders.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@phinze, thanks—the read-error fix addresses that portion of the finding. The partial CA-pair publication risk remains deferred as noted, which is reasonable for this narrowly scoped bootstrap fix.

Would you like me to open a follow-up GitHub issue for recoverable/atomic CA cert-and-key publication plus failure-injection coverage?

SetupEtcdTLS reads the server CA from disk and hard-errors if it's
missing, but the only create-if-missing path (Coordinator.LoadCA) runs
much later inside Coordinator.Start. With distributed runners enabled,
a fresh `server install` starts the service before anything has
generated a CA, so the first boot fails "CA certificate not found" and
crash-loops until the install's later auth generate lands. A bare
`miren server` on an empty data dir never recovers at all.

Extract the CA create-if-missing logic out of LoadCA into a standalone
EnsureCA, and call it right before SetupEtcdTLS so the binary bootstraps
its own CA. Scoped to the distributed-runners branch, so every other
boot path is unchanged.

Also tighten the read path while it's being extracted: distinguish a
missing CA from an unreadable one, so a transient read error can't be
mistaken for absence and silently regenerate over a valid CA.
@phinze
phinze force-pushed the phinze/mir-1464-fix-ca-ordering-so-fresh-server-install-clusters-boot-with branch from 7d41b25 to dfa58c6 Compare July 23, 2026 21:38
@phinze
phinze marked this pull request as ready for review July 23, 2026 21:45
@phinze
phinze requested a review from a team as a code owner July 23, 2026 21:45
@phinze
phinze merged commit 835a68c into main Jul 24, 2026
23 checks passed
@phinze
phinze deleted the phinze/mir-1464-fix-ca-ordering-so-fresh-server-install-clusters-boot-with branch July 24, 2026 15:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants