Siyao Meng created HDDS-16957:
---------------------------------

             Summary: 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
         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