Siyao Meng created HDDS-16912:
---------------------------------
Summary: Leader loss between root CA rotation commit and committed
leaves the rotation unfinished, and the next rotation stops every SCM
Key: HDDS-16912
URL: https://issues.apache.org/jira/browse/HDDS-16912
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-3-complete-root-ca-rotation-at-commit.patch,
TestBugMC3CommitWithoutCommitted.java, TestBugMC3LeaderLossOnRing.java
h3. Mechanism
{{RootCARotationManager.WaitSubCARotationPrepareAckTask}} finishes a rotation
with two Ratis requests in a row, {{rotationCommit}} and {{rotationCommitted}}.
{{RootCARotationHandlerImpl.rotationCommit}} moves {{scm/sub-ca}} to
{{scm/sub-ca-previous}} and {{scm/sub-ca-next}} to {{scm/sub-ca}} and persists
the new certificate ID. Only {{rotationCommitted}} reloads the key and
certificates in memory and deletes {{scm/sub-ca-previous}}.
If the leader loses leadership after the first request is committed and before
the second one is replicated, its task ends in {{catch (Throwable)}}, which
only cleans up the leader's own state. Nothing records that the second request
is owed: {{notifyStatusChanged}} on the new leader only checks the
post-processing state, which is saved after {{rotationCommitted}}. Every SCM is
left with the new sub-CA on disk, the old root certificate in memory and a
non-empty {{scm/sub-ca-previous}}.
{{MonitorTask}} on the new leader reads the root certificate from memory, finds
it still inside the rotation window and starts another rotation. The first step
of that rotation's {{rotationCommit}} is a {{Files.move}} of {{scm/sub-ca}}
onto {{scm/sub-ca-previous}} with {{ATOMIC_MOVE}} and {{REPLACE_EXISTING}},
which cannot replace a directory that is not empty. It fails with "Directory
not empty" on every SCM and each SCM calls {{scm.shutDown}}.
h3. Trigger
Secure SCM HA with {{hdds.x509.ca.rotation.enabled=true}} (off by default).
# The leader collects all prepare acks and submits {{rotationCommit}}, which is
committed.
# The leader loses leadership before {{rotationCommitted}} is replicated (run:
leader cut off from its followers; a leader crash leaves the followers with the
same log). The window is the replication of {{rotationCommit}}, the leader
applying it, and the replication of {{rotationCommitted}}.
# The new leader starts the next rotation at its next check
({{hdds.x509.ca.rotation.check.interval}}, default 1 day, then
{{hdds.x509.ca.rotation.time-of-day}}).
An admin leadership transfer is not a trigger,
{{SCMClientProtocolServer.transferLeadership}} is rejected while a rotation is
in progress (read, not run).
The same two requests in {{WaitSubCARotationPrepareAckTask}} and the same
handler code are in the release tags ozone-1.4.0, 1.4.1, 2.0.0, 2.1.0, 2.1.1,
2.1.2, 2.2.0 and 2.2.1 (read, not run).
h3. Impact
* Between the two rotations every SCM signs with a sub-CA of the new root while
it serves only the old root from {{getAllRootCaCertificates}}. On the mini
cluster the new leader's sub-CA certificate does not verify under the only root
it publishes. No datanode or OM was run against it.
* The next rotation stops all SCMs of the ring. On the mini cluster all three
failed {{rotationCommit}} within 20 seconds of the leader loss (with a 5 second
check interval). The processes did not exit: {{scm.shutDown}} is called on the
Ratis apply thread (HDDS-16911), the last thing each of them logged was the
close of the Raft server, and 10 seconds later all three apply threads were
still inside {{rotationCommit}}.
* A restart does not repair it. The SCM loads the new root from disk but
{{scm/sub-ca-previous}} stays, so the same {{rotationCommit}} fails again (run
on one SCM's objects rebuilt from disk; that the entry is applied again after a
restart is argued from {{SCMStateMachine.applyTransaction}}, which records the
applied index only after the handler returns, not run).
* The same leftover directory, on one SCM only, results when that SCM is
stopped after it applied {{rotationCommit}} and before it applied
{{rotationCommitted}}: after the restart both entries are skipped by
{{shouldSkipRootCert}} (argued, not run).
h3. Reproduction
PASS, unmodified source, two runs. [^TestBugMC3LeaderLossOnRing.java]
({{hadoop-ozone/integration-test}}, about 150 seconds) starts a secure mini
cluster with three SCMs and root certificates that live eight minutes, so the
leader rotates by itself. The only intervention is the Ratis test hook at
{{RaftServerImpl.appendEntries}}: from the first AppendEntries that carries the
{{rotationCommitted}} entry on, the followers refuse every AppendEntries of
that leader. The test then shows the old leader's "Execution error" with
{{NotLeaderException}}, on all three SCMs the new root on disk, the old root in
memory and a non-empty {{scm/sub-ca-previous}}, no {{rotationCommitted}}
applied anywhere, the second rotation started by the new leader, "Directory not
empty" on all three and all three stopped.
[^TestBugMC3CommitWithoutCommitted.java] ({{hadoop-hdds/server-scm}}, 2
seconds, PASS) covers what the first test does not: it runs one SCM's handler,
manager and certificate client on real certificate material, starts from a
commit request without its committed request, and shows that a certificate
issued in that state chains to the unpublished root, that the commit request of
the next rotation fails, and that it fails again after the SCM objects are
rebuilt from disk.
A passing test means the defect is present.
h3. Patch
[^MC-3-complete-root-ca-rotation-at-commit.patch], against
b9b33851b20fa11baf4b07982de9afb8ea44f85a. It also applies to master at
5108acff480.
Three changes in {{RootCARotationHandlerImpl}}, none to the requests, the
interface or what is persisted:
* {{rotationCommit}} finishes the rotation itself: after the directories are
switched and the certificate ID is persisted it reloads the key and
certificates and deletes {{scm/sub-ca-previous}}, which is what
{{rotationCommitted}} did. A {{rotationCommitted}} request that arrives
afterwards finds the new root in memory and is skipped by the existing
{{shouldSkipRootCert}} check. The leader only learns that its own SCM applied
{{rotationCommit}} before it sends the second request, so the split did not
make the switch any more atomic across SCMs.
* {{rotationCommit}} deletes a leftover {{scm/sub-ca-previous}} before the
first move, so a directory left behind by an earlier rotation cannot fail it.
* {{rotationCommit}} returns after a failed move or a failed write of the
certificate ID. Until now it went on to the next step whenever {{scm.shutDown}}
returned, and with the first change that would reach the reload and the removal
of {{scm/sub-ca-previous}}.
Covered by {{testCommitCompletesRotationWithoutCommittedRequest}},
{{testCommitReplacesBackupOfEarlierRotation}} and
{{testFailedCommitKeepsBackup}} in the existing {{TestRootCARotationManager}}.
Without the change the first two fail (no reload, and {{scm.shutDown}} on the
leftover directory). The third fails without the added returns, because a
failed second move is then followed by the removal of {{scm/sub-ca-previous}}.
With it {{TestRootCARotationManager}}, {{TestScmInvokerCodeGenerator}} and
{{TestScmCodecFactoryReplicateCoverage}} (18 tests) pass and checkstyle is
clean. On the mini cluster with the patch and the same leader cut, all three
SCMs had the new root in memory and no {{scm/sub-ca-previous}} right after the
leader loss, without any {{rotationCommitted}} applied, and the new leader
published the new root. The following rotation, started by the new leader two
minutes later when the new root entered its own rotation window, completed on
all three SCMs. With the patch both reproductions fail at their first assertion
on the root in memory, which is then the new root.
Not changed by the patch: when the leader is lost after {{rotationCommit}}, the
steps its task runs after the two requests are still skipped (storing the new
root in the certificate store, the post-processing window, saving the new root
in the stateful configuration). So after such an interrupted rotation the new
root gets no certificate store entry from that rotation, and certificate
signing is not held back for one
{{hdds.x509.rootca.certificate.polling.interval}} as it is after a completed
rotation (both seen in the log of the patched mini cluster run). The effect on
datanodes and OMs was not run.
The patch is against the commit above alone. Other fixes proposed for
{{rotationCommit}} will need a manual merge with it.
Found by TLA+ model checking and code review of the SCM root CA rotation paths
under HDDS-15926, on commit b9b33851b20fa11baf4b07982de9afb8ea44f85a. Checked
against HDDS issues and apache/ozone pull requests for duplicates before
filing. The attached patch is a proposal for review. Generated with Specula
(Claude Opus 5.5).
--
This message was sent by Atlassian Jira
(v8.20.10#820010)
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]