[ 
https://issues.apache.org/jira/browse/HDDS-16955?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Siyao Meng updated HDDS-16955:
------------------------------
    Attachment: TestBugMC1SubmitTimeoutDropsKey.java

> SCM secret key rotation proposed after a Ratis submit timeout drops the key 
> of the timed out rotation on all SCMs
> -----------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16955
>                 URL: https://issues.apache.org/jira/browse/HDDS-16955
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Minor
>         Attachments: 
> MC-1-propose-pending-secret-key-again-after-failed-update.patch, 
> TestBugMC1SubmitTimeoutDropsKey.java
>
>
> h3. Mechanism
> {{SecretKeyManager.checkAndRotate}} proposes the whole key list: the keys of 
> the applied state plus one newly generated key. {{checkAndInitialize}} 
> proposes the keys of the local key file, or one newly generated key if the 
> file has no valid key. {{SecretKeyStateImpl.updateKeys}} replaces the list 
> when the request is applied. {{SCMRatisServerImpl.submitRequest}} waits 
> {{ozone.scm.ha.ratis.request.timeout}} (default 30s) for the reply and then 
> gives up without cancelling the request, so the request stays in the Raft log 
> and is applied later if the leader keeps its term. The callers treat the 
> resulting {{SCMException(TIMEOUT)}} as a failure: 
> {{SecretKeyManagerService.run}} logs it and tries again at the next check, 
> {{SCMSecurityProtocolServer.checkAndRotate}} returns it to the caller. The 
> next attempt still reads the old applied state, generates another key and 
> proposes a list without the first new key. Both requests are then applied in 
> log order: the first new key is the current key for a moment and is then 
> removed from every SCM and from its key file, although it has not expired.
> HDDS-16142 fixed the same assumption (a submit that timed out did not happen) 
> for the deleted block transaction summary.
> h3. Trigger
> # The SCM leader keeps its term but cannot commit or apply a request within 
> the request timeout.
> # A rotation is proposed and its submit times out. The request stays in the 
> Raft log.
> # A second rotation is proposed before the first one is applied. Either 
> {{ozone admin scm rotate --force}} is repeated after the timeout error, or 
> the rotation checker reaches its next check. The check interval 
> {{hdds.secret.key.rotate.check.duration}} is 10m by default, so on the 
> checker path the stall has to last that long.
> # The stall ends and both requests are applied.
> A stall in which the leader loses its term does not trigger this. With the 
> default 5s leader election timeout a plain follower outage ends in 
> {{NotLeaderException}} first, and a new leader accepts requests only after it 
> has applied every earlier entry.
> Run with default timeouts: every follower the quorum depends on (both 
> followers of a three SCM group, or the only live one) keeps answering 
> heartbeats but does not acknowledge log entries (see Reproduction). The 
> leader kept its term and the forced rotation timed out after 30s. With only 
> one of two followers affected nothing stalls. Argued, not run: this is how a 
> follower whose Raft log write hangs looks to the leader (in Ratis 3.3.1 the 
> reply to a heartbeat does not wait for the log write), and a leader whose 
> {{StateMachineUpdater}} thread is blocked for 30s gives the same timeout.
> The first key of a cluster has the same problem ({{checkAndInitialize}} with 
> no saved key). That case is covered by a unit test only.
> h3. Impact
> * A token signed with the removed key cannot be verified. SCM answers 
> {{getSecretKey}} with null and the verifier throws "Can't find the signing 
> secret key". Nothing restores the key. No invalid token is accepted.
> * The key is the current key only between the two applies, 28 ms in the 
> reproduction run (longer if other requests sit between the two in the log). 
> Signers in that window: SCM itself signs container tokens with the applied 
> current key ({{ContainerTokenSecretManager}}, with 
> {{hdds.container.token.enabled}}), for example for close container commands, 
> which are sent again later. OM and datanodes fetch the current key at start 
> and then once per check interval after their key has passed the rotation 
> period ({{DefaultSecretKeySignerClient}}). If such a fetch falls into the 
> window, that process signs with the removed key until its key passes the 
> rotation period (default 1d), a restart or {{ozone admin om fetch-key}}.
> * No real signer was seen to pick up the removed key. In the reproduction the 
> rejected token is signed by the test with the key taken from the Raft log.
> h3. Reproduction
> PASS on unmodified source with default Ratis timeouts. 
> [^TestBugMC1SubmitTimeoutDropsKey.java] ({{hadoop-ozone/integration-test}}) 
> starts a secure mini cluster with three SCMs. The Ratis test hook on 
> {{RaftServerImpl.appendEntries}} makes both followers hold every call that 
> carries entries while heartbeats pass. A forced rotation over the SCM secret 
> key protocol fails with {{TimeoutException}} after 30s, with the leader still 
> leader in the same term and the key list unchanged. The call is repeated and 
> the followers are released once the second request is in the leader's log. 
> The test then reads both {{updateKeys}} requests from the Raft log and shows 
> that the new key of the first one is missing on all three SCMs and from 
> {{getSecretKey}} while unexpired, and that the datanode block token verifier 
> rejects a token signed with it and accepts one signed with the surviving key. 
> About 70s including cluster start. A passing test means the defect is present.
> The rotation checker calls the same method without the force flag. That path 
> is covered by the unit test in the patch, not run on a cluster.
> h3. Suggested fix
> The patch changes only the proposing side. The alternative is to change what 
> {{updateKeys}} does on apply (merge into the current list, or carry the key 
> the proposal was based on and reject a stale one). That changes how a 
> replicated request is applied, so SCMs of two versions applying the same log 
> would end with different key lists unless it is gated by a layout feature. It 
> is not needed for this defect.
> h3. Patch
> [^MC-1-propose-pending-secret-key-again-after-failed-update.patch], against 
> 025266b52a6dcaf39508607cad736799c40907e8. It also applies to master at 
> a65a32e7c4b.
> {{SecretKeyManager}} remembers the new key of an update that failed. While 
> that key is not in the applied key list, the next rotation proposes it again, 
> on top of the then applied state, instead of generating another key. If the 
> first request is applied after the second one was proposed, the second one 
> carries the same key and removes nothing. If the first request was lost, the 
> second one does the rotation. Once the first request has been applied, its 
> key is in the applied list and the next rotation generates a new key, as it 
> does today. This is decided by key id, not by creation time: an applied key 
> has been through the Raft log, which stores its times in milliseconds, while 
> the proposer's copy has the precision of the JVM clock. A rotation proposal 
> never contains a key id twice, also when the first request is applied while 
> the next proposal is being built ({{SecretKeyStateImpl.updateKeys}} cannot 
> apply such a list). At initialization ({{checkAndInitialize}} with no saved 
> key) the remembered key is proposed again unless it has expired: the applied 
> list is empty there, so there is nothing to compare it with, and proposing 
> the same single key again is harmless if the first request is applied in the 
> meantime. The remembered key is forgotten when an update succeeds. It is 
> replaced by a new key when it has expired, or when it would no longer become 
> the current key because a newer key has been applied. This also holds across 
> a leader change: only this SCM can have an unapplied proposal of its own in 
> the log, because Ratis accepts requests only from a leader that has applied 
> all earlier entries.
> Nothing changes as long as no update fails. After a failed update that has 
> not been applied, the next rotation on that SCM uses the key that was 
> generated at the failed attempt, so a forced rotation that timed out and is 
> repeated before the first request is applied rotates once, not twice. The log 
> then says that the key of the last failed update is proposed again. After a 
> failed update that has been applied, the next rotation behaves as on 
> unmodified code.
> Tests are in the existing {{TestSecretKeyManager}}. Proposals are applied as 
> a state machine gets them, after {{toProtobuf}} and {{fromProtobuf}}. 
> {{testRotateAgainAfterTimeout}} (scheduled and forced) and 
> {{testInitializeAgainAfterTimeout}} fail without the change because the key 
> of the first update is missing from the final list. 
> {{testRotateAfterTimedOutUpdateIsApplied}} (timed out, applied after all, 
> rotated again), {{testRotateWhileTimedOutUpdateIsApplied}} (applied between 
> the two reads of the applied state), 
> {{testRotateAfterTimeoutAndNewerKeyApplied}} and 
> {{testExpiredKeyIsNotProposedAgain}} pass with and without the change. They 
> guard the cases in which the key must not be proposed again.
> With the change the secret key unit suites of {{hdds-server-framework}} 
> ({{TestSecretKeyManager}}, {{TestLocalKeyStore}}, {{TestManagedSecretKey}}, 
> 24 tests) and the mini cluster suites {{TestSecretKeysApi}}, 
> {{TestSecretKeySnapshot}} and {{TestBlockTokensCLI}} (8 tests) pass, 
> checkstyle is clean. Also run with the change on a three SCM mini cluster: a 
> forced rotation times out, the stall ends and the request is applied, then 
> the rotation is forced again. That rotation succeeded with a new key, and all 
> three SCMs stayed up with the same three keys. With the change the 
> reproduction fails at the assertion that the key is gone: the forced rotation 
> still times out, the repeated call proposes the same key, and the key list 
> ends with that key and the old one.
> Found by TLA+ model checking and code review of the SCM secret key rotation 
> and distribution paths under HDDS-15926, on commit 
> 025266b52a6dcaf39508607cad736799c40907e8. 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]

Reply via email to