Siyao Meng created HDDS-16913:
---------------------------------
Summary: SCM that applies a root CA rotation commit after a
restart or without a staged sub CA certificate breaks its sub CA directory and
cannot start
Key: HDDS-16913
URL: https://issues.apache.org/jira/browse/HDDS-16913
Project: Apache Ozone
Issue Type: Bug
Reporter: Siyao Meng
Attachments: MC-2-commit-reads-staged-sub-ca-certificate.patch,
TestBugMC2CommitAfterRestartNullCertId.java,
TestBugMC5PrepareAndCommitBackToBack.java
h3. Mechanism
{{RootCARotationHandlerImpl.rotationCommit}} moves {{scm/sub-ca}} to
{{scm/sub-ca-previous}}, moves {{scm/sub-ca-next}} to {{scm/sub-ca}} and then
writes the new certificate ID to the VERSION file. It takes that ID from the in
memory field {{newSubCACertId}}, which only {{SubCARotationPrepareTask}} sets
in the same process, and it does not check that {{scm/sub-ca-next}} exists
before the first move. Two cases follow.
# The SCM was restarted after it staged its certificate and sent the prepare
ack. The field is null, both moves succeed and
{{SCMStorageConfig.setScmCertSerialId(null)}} throws a
{{NullPointerException}}, which {{SCMStateMachine.applyTransaction}} turns into
a terminate. {{scm/sub-ca}} now holds the new certificate while the VERSION
file names the old one, which is only left in {{scm/sub-ca-previous}}.
# Nothing is staged when the commit is applied. {{rotationPrepare}} only
schedules the prepare task, and the task deletes {{scm/sub-ca-next}} before it
asks the leader for a new certificate. When the SCM applies prepare and commit
back to back from the RAFT log, the commit runs first: the first move succeeds,
the second fails with {{NoSuchFileException}} and the handler calls
{{scm.shutDown}}. {{scm/sub-ca}} is gone.
HDDS-8983 (fixed in 1.4.0) is not this issue. It fixed the same null ID for the
order of the ack and the commit inside one running process. The field is still
only in memory, so a restart loses it.
The code is the same 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. Trigger
Secure SCM HA with {{hdds.x509.ca.rotation.enabled=true}} (default false) while
a root CA rotation runs. With the default {{hdds.x509.max.duration}} of P1865D
that is once in about five years.
* Case 1: an SCM is restarted after its prepare ack and before it applies the
commit, with its applied index already past the prepare. A graceful stop always
gives that, because it takes a snapshot. The leader polls the acks every
second, so the window is one to two seconds when all SCMs answer, and up to
{{hdds.x509.ca.rotation.ack.timeout}} (PT15M) while the leader waits for
another SCM.
* Case 2, replay: an SCM is killed in the same window before its applied index
is persisted (the SCM transaction buffer is flushed every 60s by default, or by
a snapshot). On the next start Ratis applies prepare again and then commit.
* Case 2, never prepared: an SCM applies a commit for a rotation it has not
prepared for, for example one that was stopped during the whole rotation while
the leader counted two acks of a restarted SCM. A new SCM whose own certificate
chain still ends in the older root certificate, for example one bootstrapped
before the rotation and started after it, takes the same path when it replays
these entries (argued, not run). An SCM whose chain already ends in the new
root certificate skips them.
h3. Impact
The SCM is out of service and fails on every later start until an operator
repairs the directories by hand. The other SCMs finish the rotation, so the
ring loses one member, and quorum if two SCMs are hit.
* Case 1: the SCM terminates while it applies the commit. On later starts the
certificate client finds no certificate for the ID in the VERSION file. With
{{hdds.grpc.tls.enabled=true}} the start fails with a {{NullPointerException}}
when the Ratis TLS configuration is built. Without it (the default) a primary
SCM fails with a {{NullPointerException}} in
{{StorageContainerManager.persistPrimarySCMCerts}} (run with a real
{{StorageContainerManager}} constructor on the same on disk state, not part of
the attached reproductions). Any other SCM is expected to fail when the commit
is applied again, in {{RootCARotationManager.shouldSkipRootCert}} on the
missing trust chain (argued, not run).
* Case 2: the handler calls {{scm.shutDown}} on the thread that applies the log
entry, which leaves the process half stopped instead of exiting (HDDS-16911).
On later starts {{DefaultCAServer.init}} throws {{IllegalStateException:
INTERMEDIARY_CA Should not be in Initialize State during startup.}}
Keys and certificates are not lost: the old pair is in {{scm/sub-ca-previous}},
and in case 1 the new pair is in {{scm/sub-ca}}. {{ozone repair scm cert
recover}} does not handle these directories.
h3. Reproduction
PASS, deterministic, unmodified source.
[^TestBugMC2CommitAfterRestartNullCertId.java] and
[^TestBugMC5PrepareAndCommitBackToBack.java] ({{hadoop-hdds/server-scm}}) run
the real {{RootCARotationManager}} with its prepare task,
{{RootCARotationHandlerImpl}}, {{SCMCertificateClient}}, {{SCMStorageConfig}}
and CA servers on a temporary directory. The {{StorageContainerManager}}
object, Ratis and the security RPC are mocks: the tests call the handler where
the state machine applies a log entry and rebuild the objects from disk where
the process restarts. The second class holds the certificate request of the
prepare task with a latch, as an RPC that has not returned yet. No state is
written by hand. A passing test means the defect is present.
Both cases were also seen on a three SCM compose cluster built from unmodified
source, driven only by docker pause, stop, kill and start (logs read for this
report, not run again). Case 1, with {{hdds.grpc.tls.enabled=true}}: one SCM
sends its ack, is stopped gracefully and takes a snapshot, is started again,
logs {{Persistent new scm certificate null}} and {{Terminating with exit status
1}} when it applies the commit, and two further starts fail with the
{{NullPointerException}} from the Ratis TLS configuration. Case 2, never
prepared: an SCM that was stopped during the whole rotation applies prepare and
commit 7 ms apart and logs {{Failed to move /data/metadata/scm/sub-ca-next to
/data/metadata/scm/sub-ca}}. Its process keeps running, and its prepare task
starts only afterwards.
h3. Patch
[^MC-2-commit-reads-staged-sub-ca-certificate.patch], against
b9b33851b20fa11baf4b07982de9afb8ea44f85a. It also applies to master at
5108acff480. The patch is made against that commit alone, other fixes proposed
for the same methods (HDDS-16911 among them) will need a manual merge.
* {{rotationCommit}} reads the new certificate ID from the certificate staged
in {{scm/sub-ca-next}}, before the first move. It accepts the staged
certificate only when its chain ends in the root certificate being committed
and that root certificate issued it. A certificate signed by the sub CA of a
leader that no longer has the new root CA server is not accepted. The in memory
field is removed.
* {{rotationPrepare}} keeps a certificate that is already staged and acceptable
for the same root certificate and sends the ack again with the same ID, so a
replayed prepare no longer deletes it. Anything else in {{scm/sub-ca-next}} is
replaced by the prepare task as before.
* Without an acceptable staged certificate the commit logs an error and moves
nothing, the committed step does nothing, and the pending prepare task of that
rotation is cancelled: when the leader answers its request, with a certificate
or with a rejection, the task discards the result, stages nothing and does not
stop the SCM. The SCM keeps running on its current certificate.
With the patch an SCM that takes the last path is not rotated and needs
operator attention. Bringing it to the new root certificate is not part of this
patch. What follows from that state (all read from the code, not run):
* The only sign is one ERROR line from {{rotationCommit}} when the commit is
applied. There is no metric, the leader still counts the rotation as
successful, and nothing is logged on a later start.
* The old root certificate, and with it the chain of this SCM, expires at most
2 x {{hdds.x509.renew.grace.duration}} (56 days by default) after the rotation
started.
* If this SCM is elected leader while the post rotation record is still stored
({{hdds.x509.rootca.certificate.polling.interval}}, PT2h by default),
{{RootCARotationManager.checkAndHandlePostProcessing}} finds a stored root
certificate newer than its own and throws a {{RuntimeException}} out of
{{notifyStatusChanged}}.
* If it is elected leader later, its monitor task (run every
{{hdds.x509.ca.rotation.check.interval}}, P1D) finds its own root certificate
inside the grace period and schedules another rotation, which rotates the whole
ring again, this SCM included.
* It loads the new root certificate only on its next start, when
{{StorageContainerManager.start}} refreshes the CA certificates from the
leader. Until then, with {{hdds.grpc.tls.enabled=true}}, it cannot verify the
certificates of the rotated SCMs.
{{RootCARotationHandler.setSubCACertId}} now only logs. It is kept because
removing it changes the handler interface and the generated invoker.
Covered by six cases in the existing {{TestRootCARotationManager}}: commit
after a restart, prepare applied again before the commit, commit without a
staged certificate while the request of the prepare task is still open
(answered with a certificate and with a rejection), commit while a certificate
for another root certificate is staged, and commit while a certificate signed
by a sub CA is staged. Without the change all six fail with the
{{NullPointerException}} from {{rotationCommit}}. With it
{{TestRootCARotationManager}} (10 tests) and the SCM HA and certificate unit
suites next to it pass (29 tests in total) and checkstyle is clean. With the
patch the three tests of the two reproduction classes fail, because the commit
no longer fails.
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]