Skip to content

fix(helper): fail KeyStoreHelper.run when the X.509 context watch fails - #489

Merged
maxlambrecht merged 1 commit into
spiffe:mainfrom
NopeCharon:fix/keystore-helper-async-watch-error
Oct 29, 2026
Merged

maxlambrecht merged 1 commit into
spiffe:mainfrom
NopeCharon:fix/keystore-helper-async-watch-error

Conversation

@NopeCharon

Copy link
Copy Markdown
Contributor

What

KeyStoreHelper's X.509 context watcher no longer throws from onError. It records the first error reported while run is waiting and releases the latch, and run throws that error, wrapped in KeyStoreHelperException as before, once the latch is released. Errors that arrive after run has returned are only logged.

Fixes #488

Why

DefaultWorkloadApiClient calls watchers on gRPC executor threads, so the WatcherException thrown from onError never reached the thread blocked in run. That thread only saw the latch released, and run returned normally:

  • run(false) reported success when the first X.509 context could not be fetched or stored.
  • The CLI uses run(true). After a failed watch (for example PERMISSION_DENIED from the Workload API, or an update that could not be written to the KeyStore), it closed the helper and exited with status 0.

The existing testCreateKeyStoreHelper_throwsExceptionWhenNoUpdateCanBeFetched didn't catch this because WorkloadApiClientErrorStub calls onError synchronously, on the thread that called run.

When the helper stops is unchanged: any watch error still ends run, and now run reports it.

How tested

  • KeyStoreHelperTest.testRun_errorReportedAsynchronously_throwsException and testRun_keepRunning_errorReportedAsynchronously_throwsException use a client that reports the watch error from another thread. Both fail without this change, because run returns normally.
  • Temporary end-to-end test, not committed: a Netty gRPC server on tcp://127.0.0.1:<port> returns PERMISSION_DENIED from FetchX509SVID, and KeyStoreHelper uses the default client. Before: run(true) returned normally. After: it threw KeyStoreHelperException caused by Cancelling X.509 Context watch.
  • ./gradlew build on JDK 17.

KeyStoreHelper's X.509 context watcher released the run latch and then
threw a WatcherException from onError. DefaultWorkloadApiClient invokes
watchers on gRPC executor threads, so the exception never reached the
thread blocked in run: run returned normally, as if the KeyStore had
been written. run(false) therefore reported success when the first
X.509 context could not be fetched or stored, and the CLI, which uses
run(true), exited with status 0 after a failed watch (for example
PERMISSION_DENIED from the Workload API, or an update that could not be
stored). The existing error test passed only because its stub calls
onError synchronously on the caller's thread.

Record the first error reported while run is waiting and throw it from
run after the latch is released, so run fails with
KeyStoreHelperException as documented and the CLI exits with status 1.
Errors arriving after run has returned are only logged; previously the
exception thrown from onError could also cancel the gRPC stream.

Fixes spiffe#488

Signed-off-by: NopeCharon <115794254+NopeCharon@users.noreply.github.com>
@maxlambrecht
maxlambrecht force-pushed the fix/keystore-helper-async-watch-error branch from 7c95492 to e8a7280 Compare October 29, 2026 19:41
@maxlambrecht
maxlambrecht merged commit 49d736d into spiffe:main Oct 29, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

KeyStoreHelper.run returns normally when the X.509 context watch fails

2 participants