[
https://issues.apache.org/jira/browse/HDDS-16912?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
]
Siyao Meng updated HDDS-16912:
------------------------------
Attachment: MC-3-complete-root-ca-rotation-at-commit.patch
> 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
> Priority: Major
> 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]