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

LuChengxiang updated FLINK-41435:
---------------------------------
    Description: 
*Background*

Flink periodically obtains HDFS delegation tokens and distributes them through 
HadoopDelegationTokenReceiver. The receiver currently adds the new credentials 
to UserGroupInformation.getCurrentUser() on the refresh thread. Long-lived 
Hadoop clients, such as checkpoint storage and the job-result store, capture a 
UGI when their DFSClient is created and use tokens from that UGI for later RPC 
connections.

If the refresh thread's current UGI is not the UGI captured by the client, the 
client continues using the startup token even after a new token was created 
successfully. The job later fails with SecretManager$InvalidToken at the 24h 
deadline.

*Observed failure*

This was observed in a Kubernetes application-mode job using ZooKeeper-based 
high availability and HDFS-backed checkpoint storage. The primary failure was 
in the checkpoint storage path:

CheckpointCoordinator.initializeCheckpointLocation
  -> FsCheckpointStorageAccess.initializeLocationForCheckpoint
  -> HadoopFileSystem.mkdirs
  -> DFSClient.mkdirs

The checkpoint storage client reported InvalidToken for the startup token after 
the refresh had created a new HDFS token and the receiver reported success. The 
same JVM also showed a FileSystemJobResultStore.hasCleanJobResultEntryInternal 
stack using the expired token during cleanup. The job-result store is therefore 
a secondary long-lived client with the same failure mode, not the primary 
trigger.

Not every Kubernetes application-mode job is affected. The failure is 
timing-dependent: it occurs when a long-lived Hadoop client has captured one 
UGI and the cluster-io worker that later performs the refresh has inherited a 
different Subject. If the client and the refresh worker resolve to the same 
UGI, the refreshed credentials reach that client and the job can continue 
normally. The same is true if the affected client is recreated before the old 
token expires.

An earlier test build that skipped the second security installation (`Security 
configuration is already installed`) still hit the 24h deadline; install 
idempotency alone is therefore not sufficient. This is consistent with the 
condition above: removing the second installation removes one way to replace 
the login UGI, but it does not guarantee that every already-created client and 
the refresh worker share the same Subject.

FLINK-27204 (1.19+) moved FileSystemJobResultStore I/O onto the shared 
cluster-io executor. DefaultDelegationTokenManager uses the same executor for 
periodic token refresh. The provider and receiver are invoked sequentially on 
the same refresh thread. The provider's freshUGI.doAs() scope ends before the 
receiver runs, so the receiver's getCurrentUser() reflects the Subject 
inherited by the cluster-io worker, not the provider's short-lived UGI.

HadoopSecurityContext is created by HadoopSecurityContextFactory from 
UserGroupInformation.getLoginUser() during security installation. It stores 
that particular UGI/Subject, and clients created inside 
SecurityContext.runSecured() capture it. The token refresh is different: 
DefaultDelegationTokenManager dispatches the refresh with 
scheduledExecutor.schedule(() -> ioExecutor.execute(this::startTokensUpdate), 
...). A worker in that fixed thread pool captures its AccessControlContext when 
the worker is created. If the first cluster-io task is submitted from a 
different Subject lifecycle than the UGI captured by the long-lived client, 
later refresh executions on that worker see that inherited Subject through 
UserGroupInformation.getCurrentUser(). The receiver therefore uses the worker's 
inherited Subject, not the context/login UGI. The shared cluster-io executor is 
what makes that worker/Subject mismatch reachable in this path; checkpoint 
storage and the job-result store are both long-lived clients whose DFSClient 
can remain pinned to a UGI that does not receive the refreshed credentials.

The 1.18 path had synchronous FileSystemJobResultStore I/O, so this 
worker/Subject interaction was not triggered in the deployments observed. 1.15 
did not have the unified delegation-token manager path. These version 
differences explain the observed frequency; they do not prove that those 
versions are immune.

*Why this is normally not seen on YARN*

YARN with ZooKeeper HA uses the same ZooKeeperLeaderElectionHaServices, the 
same FileSystemJobResultStore on the shared ioExecutor, and the same 
DefaultDelegationTokenManager. The executor-sharing code path is therefore 
present on YARN as well; YARN is not using a different token-refresh 
implementation.

The difference is startup order, not the token code:

* Kubernetes application mode calls FileSystem.initialize(...) and 
installSecurityContext(...) before getPackagedProgram() as part of its 
artifact-fetch path; the job jar and artifacts can be remote. 
ClusterEntrypoint.startCluster() then calls installSecurityContext(...) again.
* YARN application mode does not perform that early 
filesystem-artifact/security setup. YarnApplicationClusterEntryPoint.main 
obtains the packaged program without an earlier security installation; the only 
security installation is the one in ClusterEntrypoint.startCluster().
* On YARN, runCluster() and all cluster services, including the cluster-io 
pool, are then started inside that single securityContext.runSecured(...). The 
pool is created, and normal startup work is submitted, inside that single 
secured context. There is no earlier security installation whose Subject can be 
inherited by the worker, and no second installSecurityContext that can replace 
the login UGI after the first one.
* Consequently, the receiver's getCurrentUser() on the refresh worker normally 
resolves to the same UGI used by the security context and by long-lived 
clients. This is why the 24h failure has not been reproduced in the normal YARN 
application-mode path.

This is a startup-order explanation, not a platform-level exemption. If a 
custom security module changes the login user, or if a first cluster-io task is 
submitted from a different Subject lifecycle, the same receiver-target issue 
could occur. YARN is therefore not listed as a confirmed affected deployment 
type, but it is not claimed to be immune.

*Root cause*

HadoopDelegationTokenReceiver targets getCurrentUser() of the refresh thread. 
That target is not guaranteed to be the UGI held by HadoopSecurityContext or 
the UGI captured by a long-lived checkpoint storage or job-result-store 
DFSClient.

*Proposed change*

This issue addresses the case where a token is obtained successfully but the 
refreshed credentials do not reach long-lived Hadoop clients, primarily 
checkpoint storage.

* Add an accessor for the UGI held by HadoopSecurityContext.
* In HadoopDelegationTokenReceiver, add refreshed credentials to the 
security-context UGI, the current UGI, and the login UGI, deduplicated by 
object identity. The security-context UGI is the primary target; the other two 
preserve non-secured deployments and previous behavior.
* Add tests covering (a) a refresh thread whose current user differs from the 
login user, and (b) a security-context UGI distinct from the current/login 
user; verify that all applicable targets receive the credentials.

Out of scope for this issue:

* The separate case where HadoopFSDelegationTokenProvider evaluates 
delegation-token requirements against the wrong current UGI and does not obtain 
a token at all.
* Closing FileSystem instances created under the short-lived fresh UGI; this is 
separate lifecycle cleanup.
* SecurityUtils.install idempotency.

*Testing*

* A JVM probe confirmed that a fixed thread pool created under Subject A but 
first submitted under Subject B runs subsequent tasks under Subject B.
* A runtime probe on a Kubernetes application-mode JVM confirmed that 
cluster-io threads retain the Subject present when the worker is created; the 
refresh receiver resolves getCurrentUser() from that inherited Subject, not 
from the installed HadoopSecurityContext.
* Source comparison: YARN application mode has one security installation in 
ClusterEntrypoint.startCluster() and no Kubernetes-style early installation 
before getPackagedProgram(), so the two-Subject startup condition is absent in 
the normal YARN path.
* Receiver test as described above.
* A deployed application-mode job should survive the 24h token deadline and 
continue checkpointing without SecretManager$InvalidToken. Verify that the 
checkpoint storage DFSClient receives the new token, and verify the 
job-result-store client separately because it can capture a different UGI.

*Acceptance criteria*

* A token refresh updates the UGI used by checkpoint storage and other 
long-lived Hadoop clients.
* Refresh does not depend on the Subject inherited by the shared cluster-io 
worker.
* A long-running application-mode job survives the 24h HDFS token deadline.

  was:
*Background*

Flink periodically obtains HDFS delegation tokens and distributes them through 
HadoopDelegationTokenReceiver. The receiver currently adds the new credentials 
to UserGroupInformation.getCurrentUser() on the refresh thread. Long-lived 
Hadoop clients, such as checkpoint storage and the job-result store, capture a 
UGI when their DFSClient is created and use tokens from that UGI for later RPC 
connections.

If the refresh thread's current UGI is not the UGI captured by the client, the 
client continues using the startup token even after a new token was created 
successfully. The job later fails with SecretManager$InvalidToken at the 24h 
deadline.

*Observed failure*

This was observed in a Kubernetes application-mode job using ZooKeeper-based 
high availability and HDFS-backed checkpoint storage. The primary failure was 
in the checkpoint storage path:

CheckpointCoordinator.initializeCheckpointLocation
  -> FsCheckpointStorageAccess.initializeLocationForCheckpoint
  -> HadoopFileSystem.mkdirs
  -> DFSClient.mkdirs

The checkpoint storage client reported InvalidToken for the startup token after 
the refresh had created a new HDFS token and the receiver reported success. The 
same JVM also showed a FileSystemJobResultStore.hasCleanJobResultEntryInternal 
stack using the expired token during cleanup. The job-result store is therefore 
a secondary long-lived client with the same failure mode, not the primary 
trigger.

Not every Kubernetes application-mode job is affected. The failure is 
timing-dependent: it occurs when a long-lived Hadoop client has captured one 
UGI and the cluster-io worker that later performs the refresh has inherited a 
different Subject. If the client and the refresh worker resolve to the same 
UGI, the refreshed credentials reach that client and the job can continue 
normally. The same is true if the affected client is recreated before the old 
token expires.

An earlier test build that skipped the second security installation (`Security 
configuration is already installed`) still hit the 24h deadline; install 
idempotency alone is therefore not sufficient. This is consistent with the 
condition above: removing the second installation removes one way to replace 
the login UGI, but it does not guarantee that every already-created client and 
the refresh worker share the same Subject.

FLINK-27204 (1.19+) moved FileSystemJobResultStore I/O onto the shared 
cluster-io executor. DefaultDelegationTokenManager uses the same executor for 
periodic token refresh. The provider and receiver are invoked sequentially on 
the same refresh thread. The provider's freshUGI.doAs() scope ends before the 
receiver runs, so the receiver's getCurrentUser() reflects the Subject 
inherited by the cluster-io worker, not the provider's short-lived UGI.

HadoopSecurityContext is created by HadoopSecurityContextFactory from 
UserGroupInformation.getLoginUser() during security installation. It stores 
that particular UGI/Subject, and clients created inside 
SecurityContext.runSecured() capture it. The token refresh is different: 
DefaultDelegationTokenManager dispatches the refresh with 
scheduledExecutor.schedule(() -> ioExecutor.execute(this::startTokensUpdate), 
...). A worker in that fixed thread pool captures its AccessControlContext when 
the worker is created. If the first cluster-io task is submitted from a 
different Subject lifecycle than the UGI captured by the long-lived client, 
later refresh executions on that worker see that inherited Subject through 
UserGroupInformation.getCurrentUser(). The provider's freshUGI.doAs() only 
wraps the provider call and ends before the receiver runs; the receiver 
therefore uses the worker's inherited Subject, not the context/login UGI. The 
shared cluster-io executor is what makes that worker/Subject mismatch reachable 
in this path; checkpoint storage and the job-result store are both long-lived 
clients whose DFSClient can remain pinned to a UGI that does not receive the 
refreshed credentials.

The 1.18 path had synchronous FileSystemJobResultStore I/O, so this 
worker/Subject interaction was not triggered in the deployments observed. 1.15 
did not have the unified delegation-token manager path. These version 
differences explain the observed frequency; they do not prove that those 
versions are immune.

*Why this is normally not seen on YARN*

YARN with ZooKeeper HA uses the same ZooKeeperLeaderElectionHaServices, the 
same FileSystemJobResultStore on the shared ioExecutor, and the same 
DefaultDelegationTokenManager. The executor-sharing code path is therefore 
present on YARN as well; YARN is not using a different token-refresh 
implementation.

The difference is startup order, not the token code:

* Kubernetes application mode calls FileSystem.initialize(...) and 
installSecurityContext(...) before getPackagedProgram() as part of its 
artifact-fetch path; the job jar and artifacts can be remote. 
ClusterEntrypoint.startCluster() then calls installSecurityContext(...) again.
* YARN application mode does not perform that early 
filesystem-artifact/security setup. YarnApplicationClusterEntryPoint.main only 
loads the configuration and obtains the packaged program, and the only security 
installation is the one in ClusterEntrypoint.startCluster().
* On YARN, runCluster() and all cluster services, including the cluster-io 
pool, are then started inside that single securityContext.runSecured(...). The 
pool is created, and normal startup work is submitted, inside that single 
secured context. There is no earlier security installation whose Subject can be 
inherited by the worker, and no second installSecurityContext after the first 
one that can replace the process login user after clients have captured it.
* Consequently, the receiver's getCurrentUser() on the refresh worker normally 
resolves to the same UGI used by the security context and by long-lived 
clients. This is why the 24h failure has not been reproduced on YARN.

This is a startup-order explanation, not a platform-level exemption. If a YARN 
extension or plugin changes the login user, or if a first cluster-io task is 
submitted from a different Subject lifecycle, the same receiver-target issue 
could occur. YARN is therefore not listed as a confirmed affected deployment 
type, but it is not claimed to be immune.

*Root cause*

HadoopDelegationTokenReceiver targets getCurrentUser() of the refresh thread. 
That target is not guaranteed to be the UGI held by HadoopSecurityContext or 
the UGI captured by a long-lived checkpoint storage or job-result-store 
DFSClient.

*Proposed change*

This issue addresses the case where a token is obtained successfully but the 
refreshed credentials do not reach long-lived Hadoop clients, primarily 
checkpoint storage.

* Add an accessor for the UGI held by HadoopSecurityContext.
* In HadoopDelegationTokenReceiver, add refreshed credentials to the 
security-context UGI, the current UGI, and the login UGI, deduplicated by 
object identity. The security-context UGI is the primary target; the other two 
preserve non-secured deployments and previous behavior.
* Add tests covering (a) a refresh thread whose current user differs from the 
login user, and (b) a security-context UGI distinct from the current/login 
user; verify that all applicable targets receive the credentials.

Out of scope for this issue:

* The separate case where HadoopFSDelegationTokenProvider evaluates 
delegation-token requirements against the wrong current UGI and does not obtain 
a token at all.
* Closing FileSystem instances created under the short-lived fresh UGI; this is 
separate lifecycle cleanup.
* SecurityUtils.install idempotency.

*Testing*

* A JVM probe confirmed that a fixed thread pool created under Subject A but 
first submitted under Subject B runs subsequent tasks under Subject B.
* A runtime probe on a Kubernetes application-mode JVM confirmed that 
cluster-io threads retain the Subject present when the worker is created; the 
refresh receiver resolves getCurrentUser() from that inherited Subject, not 
from the installed HadoopSecurityContext.
* Source comparison: YARN application mode has one security installation in 
ClusterEntrypoint.startCluster() and no Kubernetes-style early installation 
before getPackagedProgram(), so the two-Subject startup condition is absent in 
the normal YARN path.
* Receiver test as described above.
* A deployed application-mode job should survive the 24h token deadline and 
continue checkpointing without SecretManager$InvalidToken. Verify that the 
checkpoint storage DFSClient receives the new token, and verify the 
job-result-store client separately because it can capture a different UGI.

*Acceptance criteria*

* A token refresh updates the UGI used by checkpoint storage and other 
long-lived Hadoop clients.
* Refresh does not depend on the Subject inherited by the shared cluster-io 
worker.
* A long-running application-mode job survives the 24h HDFS token deadline.


> Hadoop delegation token refresh can target a different UGI than Hadoop clients
> ------------------------------------------------------------------------------
>
>                 Key: FLINK-41435
>                 URL: https://issues.apache.org/jira/browse/FLINK-41435
>             Project: Flink
>          Issue Type: Bug
>          Components: Runtime / Configuration
>    Affects Versions: 2.0.0, 1.19.0, 1.20.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0
>         Environment: Flink 1.19.0 and later (1.19.x, 1.20.x, 2.0 through 
> 2.4), plus master (2.5-SNAPSHOT); Kubernetes and standalone application mode 
> with Kerberos and HDFS delegation tokens. The mismatch is more likely when 
> FileSystemJobResultStore and the token manager share the cluster-io executor 
> (FLINK-27204).
>            Reporter: LuChengxiang
>            Priority: Major
>
> *Background*
> Flink periodically obtains HDFS delegation tokens and distributes them 
> through HadoopDelegationTokenReceiver. The receiver currently adds the new 
> credentials to UserGroupInformation.getCurrentUser() on the refresh thread. 
> Long-lived Hadoop clients, such as checkpoint storage and the job-result 
> store, capture a UGI when their DFSClient is created and use tokens from that 
> UGI for later RPC connections.
> If the refresh thread's current UGI is not the UGI captured by the client, 
> the client continues using the startup token even after a new token was 
> created successfully. The job later fails with SecretManager$InvalidToken at 
> the 24h deadline.
> *Observed failure*
> This was observed in a Kubernetes application-mode job using ZooKeeper-based 
> high availability and HDFS-backed checkpoint storage. The primary failure was 
> in the checkpoint storage path:
> CheckpointCoordinator.initializeCheckpointLocation
>   -> FsCheckpointStorageAccess.initializeLocationForCheckpoint
>   -> HadoopFileSystem.mkdirs
>   -> DFSClient.mkdirs
> The checkpoint storage client reported InvalidToken for the startup token 
> after the refresh had created a new HDFS token and the receiver reported 
> success. The same JVM also showed a 
> FileSystemJobResultStore.hasCleanJobResultEntryInternal stack using the 
> expired token during cleanup. The job-result store is therefore a secondary 
> long-lived client with the same failure mode, not the primary trigger.
> Not every Kubernetes application-mode job is affected. The failure is 
> timing-dependent: it occurs when a long-lived Hadoop client has captured one 
> UGI and the cluster-io worker that later performs the refresh has inherited a 
> different Subject. If the client and the refresh worker resolve to the same 
> UGI, the refreshed credentials reach that client and the job can continue 
> normally. The same is true if the affected client is recreated before the old 
> token expires.
> An earlier test build that skipped the second security installation 
> (`Security configuration is already installed`) still hit the 24h deadline; 
> install idempotency alone is therefore not sufficient. This is consistent 
> with the condition above: removing the second installation removes one way to 
> replace the login UGI, but it does not guarantee that every already-created 
> client and the refresh worker share the same Subject.
> FLINK-27204 (1.19+) moved FileSystemJobResultStore I/O onto the shared 
> cluster-io executor. DefaultDelegationTokenManager uses the same executor for 
> periodic token refresh. The provider and receiver are invoked sequentially on 
> the same refresh thread. The provider's freshUGI.doAs() scope ends before the 
> receiver runs, so the receiver's getCurrentUser() reflects the Subject 
> inherited by the cluster-io worker, not the provider's short-lived UGI.
> HadoopSecurityContext is created by HadoopSecurityContextFactory from 
> UserGroupInformation.getLoginUser() during security installation. It stores 
> that particular UGI/Subject, and clients created inside 
> SecurityContext.runSecured() capture it. The token refresh is different: 
> DefaultDelegationTokenManager dispatches the refresh with 
> scheduledExecutor.schedule(() -> ioExecutor.execute(this::startTokensUpdate), 
> ...). A worker in that fixed thread pool captures its AccessControlContext 
> when the worker is created. If the first cluster-io task is submitted from a 
> different Subject lifecycle than the UGI captured by the long-lived client, 
> later refresh executions on that worker see that inherited Subject through 
> UserGroupInformation.getCurrentUser(). The receiver therefore uses the 
> worker's inherited Subject, not the context/login UGI. The shared cluster-io 
> executor is what makes that worker/Subject mismatch reachable in this path; 
> checkpoint storage and the job-result store are both long-lived clients whose 
> DFSClient can remain pinned to a UGI that does not receive the refreshed 
> credentials.
> The 1.18 path had synchronous FileSystemJobResultStore I/O, so this 
> worker/Subject interaction was not triggered in the deployments observed. 
> 1.15 did not have the unified delegation-token manager path. These version 
> differences explain the observed frequency; they do not prove that those 
> versions are immune.
> *Why this is normally not seen on YARN*
> YARN with ZooKeeper HA uses the same ZooKeeperLeaderElectionHaServices, the 
> same FileSystemJobResultStore on the shared ioExecutor, and the same 
> DefaultDelegationTokenManager. The executor-sharing code path is therefore 
> present on YARN as well; YARN is not using a different token-refresh 
> implementation.
> The difference is startup order, not the token code:
> * Kubernetes application mode calls FileSystem.initialize(...) and 
> installSecurityContext(...) before getPackagedProgram() as part of its 
> artifact-fetch path; the job jar and artifacts can be remote. 
> ClusterEntrypoint.startCluster() then calls installSecurityContext(...) again.
> * YARN application mode does not perform that early 
> filesystem-artifact/security setup. YarnApplicationClusterEntryPoint.main 
> obtains the packaged program without an earlier security installation; the 
> only security installation is the one in ClusterEntrypoint.startCluster().
> * On YARN, runCluster() and all cluster services, including the cluster-io 
> pool, are then started inside that single securityContext.runSecured(...). 
> The pool is created, and normal startup work is submitted, inside that single 
> secured context. There is no earlier security installation whose Subject can 
> be inherited by the worker, and no second installSecurityContext that can 
> replace the login UGI after the first one.
> * Consequently, the receiver's getCurrentUser() on the refresh worker 
> normally resolves to the same UGI used by the security context and by 
> long-lived clients. This is why the 24h failure has not been reproduced in 
> the normal YARN application-mode path.
> This is a startup-order explanation, not a platform-level exemption. If a 
> custom security module changes the login user, or if a first cluster-io task 
> is submitted from a different Subject lifecycle, the same receiver-target 
> issue could occur. YARN is therefore not listed as a confirmed affected 
> deployment type, but it is not claimed to be immune.
> *Root cause*
> HadoopDelegationTokenReceiver targets getCurrentUser() of the refresh thread. 
> That target is not guaranteed to be the UGI held by HadoopSecurityContext or 
> the UGI captured by a long-lived checkpoint storage or job-result-store 
> DFSClient.
> *Proposed change*
> This issue addresses the case where a token is obtained successfully but the 
> refreshed credentials do not reach long-lived Hadoop clients, primarily 
> checkpoint storage.
> * Add an accessor for the UGI held by HadoopSecurityContext.
> * In HadoopDelegationTokenReceiver, add refreshed credentials to the 
> security-context UGI, the current UGI, and the login UGI, deduplicated by 
> object identity. The security-context UGI is the primary target; the other 
> two preserve non-secured deployments and previous behavior.
> * Add tests covering (a) a refresh thread whose current user differs from the 
> login user, and (b) a security-context UGI distinct from the current/login 
> user; verify that all applicable targets receive the credentials.
> Out of scope for this issue:
> * The separate case where HadoopFSDelegationTokenProvider evaluates 
> delegation-token requirements against the wrong current UGI and does not 
> obtain a token at all.
> * Closing FileSystem instances created under the short-lived fresh UGI; this 
> is separate lifecycle cleanup.
> * SecurityUtils.install idempotency.
> *Testing*
> * A JVM probe confirmed that a fixed thread pool created under Subject A but 
> first submitted under Subject B runs subsequent tasks under Subject B.
> * A runtime probe on a Kubernetes application-mode JVM confirmed that 
> cluster-io threads retain the Subject present when the worker is created; the 
> refresh receiver resolves getCurrentUser() from that inherited Subject, not 
> from the installed HadoopSecurityContext.
> * Source comparison: YARN application mode has one security installation in 
> ClusterEntrypoint.startCluster() and no Kubernetes-style early installation 
> before getPackagedProgram(), so the two-Subject startup condition is absent 
> in the normal YARN path.
> * Receiver test as described above.
> * A deployed application-mode job should survive the 24h token deadline and 
> continue checkpointing without SecretManager$InvalidToken. Verify that the 
> checkpoint storage DFSClient receives the new token, and verify the 
> job-result-store client separately because it can capture a different UGI.
> *Acceptance criteria*
> * A token refresh updates the UGI used by checkpoint storage and other 
> long-lived Hadoop clients.
> * Refresh does not depend on the Subject inherited by the shared cluster-io 
> worker.
> * A long-running application-mode job survives the 24h HDFS token deadline.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to