Siyao Meng created HDDS-16960:
---------------------------------

             Summary: One failed poll for the current secret key permanently 
stops the secret key refresh in OM and datanodes
                 Key: HDDS-16960
                 URL: https://issues.apache.org/jira/browse/HDDS-16960
             Project: Apache Ozone
          Issue Type: Bug
            Reporter: Siyao Meng
         Attachments: 
MC-4-keep-secret-key-poller-running-after-failed-poll.patch, 
TestBugMC4FailedPollStopsKeyRefresh.java

h3. Mechanism
{{DefaultSecretKeySignerClient.scheduleSecretKeyPoller}} runs 
{{checkAndRefresh}} with {{ScheduledExecutorService.scheduleAtFixedRate}}. When 
{{SecretKeyProtocol.getCurrentSecretKey}} fails, {{checkAndRefresh}} rethrows 
the {{IOException}} as {{UncheckedIOException}}. A periodic task that throws is 
never run again by the executor, and the exception stays in a 
{{ScheduledFuture}} that nobody reads, so nothing is logged either. From then 
on {{getCurrentSecretKey}} returns the last fetched key for the life of the 
process, and {{ShortLivedTokenSecretManager.createPassword}} keeps signing with 
it without looking at its expiry.

The class Javadoc says the client "periodically check and get the latest 
current secret key from SCM", and the only comment at the throw site is a TODO 
about failure metrics, so the cancellation does not look intended. HDDS-9020 
added a retry for {{SECRET_KEY_NOT_INITIALIZED}} to the initial fetch only.

h3. Trigger
# Secure cluster. OM always runs this client. Datanodes run it when 
{{hdds.block.token.enabled}} or {{hdds.container.token.enabled}} is set.
# The poller calls SCM once its key is older than 
{{hdds.secret.key.rotate.duration}} (1d), and then at every 
{{hdds.secret.key.rotate.check.duration}} (10m) until SCM returns a newer key. 
Processes that hold the same key become due within one check interval of each 
other.
# One of these calls fails. The poller of that process is gone.

Ways the call can fail at default settings:
* SCM cannot be reached, or has no leader, until the SCM failover proxy gives 
up. With {{hdds.scmclient.max.retry.timeout}} 10m and 
{{hdds.scmclient.failover.retry.interval}} 2s that is 300 failovers, about 10 
minutes when the connections are refused. An SCM outage that covers the daily 
rotation time by that much is enough, and it hits all OMs and datanodes 
together. This is the case that was run, with the failure injected or with a 
shortened retry budget (see Reproduction). The default 10 minute budget was not 
run.
* The leader SCM is ready but has not initialized its secret keys yet (a 
restarted SCM loads the keys from its key file only after it has become 
leader). It answers {{SECRET_KEY_NOT_INITIALIZED}} as a response status, which 
{{SecretKeyProtocolClientSideTranslatorPB.handleError}} turns into an exception 
outside of the retry proxy, so it is not retried at all. Argued from the code, 
not run. The window is normally one Ratis write.
* Any {{RuntimeException}} or {{Error}} from the call has the same effect, by 
the contract of the executor. Not run as the first failure.

h3. Impact
* The process keeps issuing tokens signed with the stale key. Datanodes reject 
them from the moment the key expires ({{hdds.secret.key.expiry.duration}}, 9d 
after its creation, so up to 8 days after the failed poll), with "Can't find 
the signing secret key" once SCM has dropped the key at the next rotation (this 
is the message seen in the reproduction) and, by code reading, with "Token 
can't be verified due to expired secret key" on a datanode that still has the 
key cached. For the OM that serves clients this means every block token it 
hands out is rejected. A datanode signs the block and container tokens it uses 
toward other datanodes ({{TokenHelper}}, used by EC reconstruction and 
container reconciliation, by code reading), so those requests are rejected in 
the same way.
* Nothing in the process starts the poller again. The only recovery is a 
restart of each affected OM and datanode. (By code reading there is one 
exception: an OM that installs a checkpoint from the leader stops and starts 
its secret key client as a side effect. Not run.)
* {{ozone admin om fetch-key}} does not help either, because it does not reach 
the signer at the moment.
* The same signer key is used by OM for delegation tokens and STS tokens (code 
reading, not run).

Fail closed: no token is accepted that should not be. The damage is 
availability, it is silent, and it shows up days after the cause.

h3. Reproduction
PASS, deterministic, unmodified source, about 4 seconds. 
[^TestBugMC4FailedPollStopsKeyRefresh.java] ({{hadoop-hdds/framework}}) uses 
the real {{DefaultSecretKeyClient}} for the signer and for the verifier, 
{{OzoneBlockTokenSecretManager}} and {{TokenVerifier}}, and a real 
{{SecretKeyManager}} as SCM that is called in process instead of over RPC. 
While SCM is "unreachable" its {{getCurrentSecretKey}} throws 
{{ConnectException}}, the exception the failover proxy ends with. Rotation is 
500 ms, the check interval 50 ms, key expiry 3 s. The test shows the signer 
following a rotation, then one failed poll, then no poll at all in the next 20 
check intervals while SCM rotates, and finally a freshly issued block token 
rejected with "Can't find the signing secret key" once SCM has dropped the 
stale key. A restarted signer is accepted. A passing test means the defect is 
present. A real SCM, the RPC retry path and a datanode over the wire are not 
exercised by it.

A run with a real SCM exists, made by the verification pipeline that found this 
and not repeated for this report: a signer client with the real SCM RPC proxy 
on a secure mini cluster, whose only SCM was stopped and started again, with 
the client retry budget shortened to about one second. The poll that fell into 
the outage failed with a {{ConnectException}}, no poll followed while SCM 
rotated four more times, and after SCM had dropped the signer's key a fresh 
token of that signer was rejected with "Can't find the signing secret key".

h3. Patch
[^MC-4-keep-secret-key-poller-running-after-failed-poll.patch], against 
025266b52a6dcaf39508607cad736799c40907e8. It also applies to master at 
4389803780f.

The scheduled task now catches whatever the poll throws (errors included, since 
anything thrown ends the schedule), logs it at WARN with the cause and leaves 
the next run to the executor, so the fetch is retried at the next check. 
{{checkAndRefresh}} itself still throws, so {{refetchSecretKey}} keeps 
reporting a failure to its caller. No new retry loop and no new configuration. 
One visible consequence: during a long SCM outage the poller now keeps trying 
for as long as its key is due, and the SCM failover proxy prints one line to 
stderr per attempt (every 2s). Before, that stopped with the poller.

Covered by the new {{TestDefaultSecretKeySignerClient}} (there was no unit test 
for this class): {{pollerKeepsRefreshingAfterFailedPolls}} fails three polls in 
a row, with an {{IOException}}, a {{RuntimeException}} and an {{Error}}, and 
expects the key of the following poll, 
{{refetchSecretKeyReportsFailureAndKeepsKey}} pins the behaviour of the manual 
refetch. Without the change the first one fails ("Wanted 5 times ... But was 2 
times"). 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, checkstyle is clean. With the patch the reproduction 
fails at its assertion that no poll follows the failed 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