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

Siyao Meng updated HDDS-16957:
------------------------------
    Attachment: TestBugMC3VerifierCachesAbsentKey.java

> Datanode caches a "no such secret key" answer from a stale SCM leader and 
> keeps rejecting tokens signed with that key
> ---------------------------------------------------------------------------------------------------------------------
>
>                 Key: HDDS-16957
>                 URL: https://issues.apache.org/jira/browse/HDDS-16957
>             Project: Apache Ozone
>          Issue Type: Bug
>            Reporter: Siyao Meng
>            Priority: Major
>         Attachments: 
> MC-3-do-not-keep-unknown-secret-key-id-in-verifier-cache.patch, 
> TestBugMC3VerifierCachesAbsentKey.java
>
>
> h3. Mechanism
> {{DefaultSecretKeyVerifierClient}} loads secret keys by ID from SCM into a 
> Guava cache with {{expireAfterWrite}} of twice 
> {{hdds.secret.key.expiry.duration}} (18d at defaults). When SCM answers that 
> it has no key with that ID, the loader stores {{Optional.empty()}} like a 
> key. From then on {{getSecretKey}} returns null for that ID without asking 
> SCM, until the entry expires or the process restarts, and 
> {{ShortLivedTokenVerifier.verifyTokenPassword}} rejects every token signed 
> with that key with "Can't find the signing secret key". The comment on the 
> cache size explains the factor of two with recently expired keys and says 
> nothing about unknown IDs.
> "No such key" is not a stable answer. SCM serves {{getSecretKey}} from its 
> own memory, and the only gate is {{StorageContainerManager.checkLeader}}, 
> which is the local Ratis {{isLeaderReady}} with no lease or majority check. 
> An SCM leader that has lost its followers keeps passing it until Ratis 
> notices, which happens only after the leader has seen no event for one 
> maximum election timeout (5.2s for SCM), and every append to its own log 
> restarts that timer. In the meantime a new leader can create a new key. A 
> verifier whose SCM proxy is still on the old leader asks it for that key, 
> gets the empty answer, which is a successful call and causes no failover, and 
> keeps it.
> OM uses the same client to verify delegation tokens and STS tokens 
> ({{OzoneDelegationTokenSecretManager.verifySignature}}, 
> {{STSSecurityUtil.getValidatedSecretKey}}), so an OM that cached the empty 
> answer rejects those in the same way. This is from reading the code, it was 
> not run.
> h3. Trigger
> # Secure cluster with SCM HA and {{hdds.block.token.enabled}} or 
> {{hdds.container.token.enabled}}.
> # The SCM leader loses the Raft links to both peers but is still reachable 
> for datanodes, and requests that need a Raft write keep arriving at it, at 
> least one every 5.2s for the whole time until step 5. They cannot commit, but 
> each append to its own log keeps it from stepping down. Every such request 
> blocks its caller for {{ozone.scm.ha.ratis.request.timeout}} (30s), so the 
> writes have to come from several callers. The datanodes have their secret key 
> proxy on this SCM because it was the leader.
> # The peers elect a new leader, and the new leader rotates the secret key 
> (the daily rotation falls into this time).
> # A token signed with the new key reaches a datanode. By code reading the new 
> SCM leader is itself such a signer: it puts a container token signed with its 
> current key into the close container commands it sends to datanodes.
> # The datanode asks the old leader for the key and caches the empty answer.
> This needs a partition of the SCM leader that overlaps a key rotation, so it 
> is rare. When the writes on the old leader stop, it steps down 5.2s later, 
> usually before a successor exists: in one run with a single write the old 
> leader stepped down 88 ms before the successor was elected, and the verifier 
> found the key.
> h3. Impact
> * The datanode rejects every block and container token signed with that key, 
> although all SCMs have the key, and it keeps doing so after the partition is 
> gone. Every datanode that looked the key up during the window is affected. A 
> datanode that asks later is not.
> * It lasts as long as tokens signed with that key are presented, which is 
> about one rotation period (1d) plus the token lifetime, or until the datanode 
> is restarted. The cache entry itself lives 18d.
> * Fail closed: no token is accepted that should not be.
> h3. Reproduction
> PASS, unmodified source, about 60 seconds. 
> [^TestBugMC3VerifierCachesAbsentKey.java] ({{hadoop-ozone/integration-test}}) 
> starts a secure mini cluster with three SCMs and default Ratis timeouts. It 
> holds back the Raft RPCs between the leader and its two peers with the Ratis 
> {{CodeInjectionForTesting}} hook and submits one request per second to the 
> old leader's Ratis server. When the peers have elected a new leader, the test 
> rotates the key on it, lets it sign a container token, and verifies the token 
> with a verifier that is built the way a datanode builds its own. In the run 
> the new key was created 8.78 s after the cut, {{checkLeader}} was still true 
> on the old leader, the verifier logged "Secret key fetched from SCM: null" 
> and rejected the token. After the links were restored all three SCMs had the 
> key and a second verifier accepted the same token, but the first verifier 
> still rejected it 14 s later. A passing test means the defect is present.
> Not exercised: a datanode process rejecting a request over the wire, OM as 
> the signer of a block token, a partition made by anything other than the 
> Ratis test hook, and real write traffic on a cut off leader. The writes in 
> the test are synthetic (the test hands one request per second to the old 
> leader's Ratis server), so that a production SCM stays leader ready in this 
> way is argued from the Ratis code, not shown.
> h3. Patch
> [^MC-3-do-not-keep-unknown-secret-key-id-in-verifier-cache.patch], against 
> 025266b52a6dcaf39508607cad736799c40907e8. It also applies to master at 
> a65a32e7c4b.
> The cache loader no longer produces an entry for an ID that SCM does not 
> know. Such an ID is remembered for 10 seconds in a second bounded cache, then 
> the next token with that ID makes the verifier ask SCM again. The ID is 
> remembered briefly so that a key that SCM really does not have is not looked 
> up again at once. 10 seconds is longer than the 5.2s after which a cut off 
> SCM leader without writes steps down, so the next lookup normally reaches the 
> new leader. No new configuration and no protocol change. The server side is 
> not changed, so an SCM leader that has lost its followers can still give the 
> stale answer; with the patch the datanode recovers from it by itself.
> Covered by the new {{TestDefaultSecretKeyVerifierClient}} (there was no unit 
> test for this class): {{keyUnknownToScmIsLookedUpAgainAfterExpiry}} and 
> {{failedLookupIsNotCached}}. Without the change the first one fails 
> ("expected: SecretKey(...) but was: null"), the second one passes and pins 
> the neighbouring behaviour. With it the secret key and token unit suites of 
> {{hadoop-hdds/framework}} (40 tests) and {{TestBlockTokens}} on a secure mini 
> cluster (5 tests) pass, and checkstyle is clean. With the patch the 
> reproduction fails at the assertion that the first verifier still rejects the 
> token, because that verifier fetches the key again (20 s after the empty 
> answer in the run) and accepts the token.
> 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