Skip to content

Issue #8302 : Add Kubernetes authentication to the HashiCorp Vault variable resolver - #8305

Open
mattcasters wants to merge 2 commits into
apache:mainfrom
mattcasters:issue-8302
Open

Issue #8302 : Add Kubernetes authentication to the HashiCorp Vault variable resolver#8305
mattcasters wants to merge 2 commits into
apache:mainfrom
mattcasters:issue-8302

Conversation

@mattcasters

Copy link
Copy Markdown
Contributor

The HashiCorp Vault (and OpenBAO) variable resolver can now authenticate with a Kubernetes ServiceAccount JWT instead of a long-lived Vault token.

Addresses #8302

What changed

  • New TOKEN / KUBERNETES authentication type on BaseVaultVariableResolver. Existing metadata without the field still uses TOKEN.
  • Kubernetes options: role, JWT file path (default /var/run/secrets/kubernetes.io/serviceaccount/token, read with HopVfs), optional inline JWT, optional auth mount (default kubernetes).
  • After Kubernetes login the short-lived Vault token is cached in memory, renewed when Vault says it is renewable, and replaced by a fresh login when it expires. It is never stored in metadata.
  • The editor groups Connection / Authentication / Secrets and only shows the fields that apply to the selected auth type.

Tests

  • Unit tests for auth-type parsing, JWT loading, path prefix, and client cache.
  • UI tests for widget visibility per auth type.
  • Testcontainers IT against Vault 1.19 with a TokenReview mock (inline JWT, JWT file, custom mount, cache, token regression, failure cases).
  • Hop docker integration tests: main-0004-kubernetes-auth plus the existing vault suite.

Thank you for your contribution! Follow this checklist to help us incorporate your contribution quickly and easily:

  • Run mvn clean install apache-rat:check to make sure basic checks pass. A more thorough check will be performed on your pull request automatically.
  • If you have a group of commits related to the same change, please squash your commits into one and force push your branch using git rebase -i.
  • Mention the appropriate issue in your description (for example: addresses #123), if applicable.

To make clear that you license your contribution under the Apache License Version 2.0, January 2004
you have to acknowledge this by using the following check-box.

@bamaer bamaer left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work — the design is sound, backward compatibility is handled (empty authenticationTypeTOKEN), and the JWT is read through HopVfs as it should be. I decompiled vault-java-driver to double-check the login path: loginByKubernetes(role, jwt, path) builds <address>/v1/<path>/login, so kubernetesLoginPath() returning auth/<mount> is correct.

Three things I'd like to see addressed:

1. An expired or revoked token is never retried, and the failure is silent. In tokenNeedsRefresh(), the 30s margin only applies when tokenRenewable is true. A non-renewable token (a role with token_type=batch, common for short-lived k8s workloads) only refreshes once remaining <= 0 — after it has already died. A resolve() in that window uses the dead token, gets a 403, and resolve() logs and returns null, so the variable silently resolves to nothing. Same gap if Vault revokes the token early, and if getAuthLeaseDuration() returns ≤ 0 then tokenExpiryMillis stays 0 and nothing ever refreshes.

Suggest applying the margin regardless of tokenRenewable, and re-authenticating once on an auth failure rather than returning null.

2. The old Vault token stays in the metadata file after switching to Kubernetes auth. setWidgetsHidden is visual only — GuiCompositeWidgets.getWidgetsContents reads every element regardless of visibility. So a user who had a token, switches to KUBERNETES and saves still has that token written into the variable-resolver JSON, which usually lives in a version-controlled project directory. The field is hidden, so they have no reason to think it is still there. Worth clearing the credentials that don't apply to the selected auth type on save.

3. toUpperCase() without a locale in parseAuthType() and readAuthType(). In a Turkish locale "kubernetes".toUpperCase() maps iİ and valueOf throws. toUpperCase(Locale.ROOT) on both. testAuthTypeIsCaseInsensitive won't catch it since it runs in the default locale.

Testing. Ran both layers locally and they pass: VaultKubernetesAuthIT 7/7 against a real Vault container, plus the unit and UI tests, and the docker suite PROJECT_NAME=vault 4/4 including the new main-0004-kubernetes-auth. The test setup is genuinely good — neither layer needs a real cluster. One caveat for CI: @Testcontainers(disabledWithoutDocker = true) means the 7 IT tests skip silently and the build still reports success, so it's worth confirming the CI agent has a Docker daemon.

…ntication

Apply the 30s refresh margin for non-renewable tokens, retry Kubernetes
auth once on 401/403, clear unused credentials on save, and parse auth
types with Locale.ROOT.
@mattcasters

Copy link
Copy Markdown
Contributor Author

Thanks for the review — addressed in 0ed000d:

  1. Token refresh / retry. The 30s margin now applies whether or not the token is renewable. A missing lease (getAuthLeaseDuration() <= 0) is treated as expiring after that same margin instead of being cached forever. Kubernetes auth failures (401/403) invalidate the cached client and retry the lookup once; TOKEN auth is left as a single attempt so a bad static token does not loop.

  2. Leftover credentials. Hidden widgets are still read by getWidgetsContents, so VariableResolverEditor now calls persistContents on save. That strips vaultToken when the type is Kubernetes and kubernetesJwt when it is TOKEN. A variable in authenticationType (e.g. ${VAULT_AUTH_TYPE}) keeps both sets of credentials, and the editor shows every credential field in that case so values are not wiped.

  3. toUpperCase(Locale.ROOT) in parseAuthType() and readAuthType(), with a unit test that switches the default locale to tr-TR.

The CI Docker-daemon caveat is noted; @Testcontainers(disabledWithoutDocker = true) is unchanged.

@mattcasters

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review, @bamaer! All points have been addressed in commit 0ed000d716:

  1. Token Refresh & Re-Authentication on 401/403:

    • The 30s refresh margin (REFRESH_MARGIN_MILLIS) now applies unconditionally in tokenNeedsRefresh() so non-renewable/batch tokens are refreshed before they die.
    • If Vault reports a non-positive lease duration (leaseSeconds <= 0), the token expiry is set to 30s so it doesn't get cached forever.
    • On a 401/403 auth error during resolve(), invalidateCachedClient() is invoked and re-authentication is attempted once before failing.
    • Covered by unit tests testNonRenewableTokenRefreshesBeforeExpiry, testKubernetesAuthFailureRetriesOnce, and testTokenAuthFailureDoesNotRetry.
  2. Clearing Stale Credentials on Auth Type Switch:

    • VariableResolverEditor now triggers persistContents() after reading widget contents.
    • clearUnusedCredentials() clears vaultToken when KUBERNETES is chosen, and clears kubernetesJwt when TOKEN is chosen (both in the model and the UI controls).
    • If authenticationType is a variable expression (${...}), credentials and fields are preserved.
    • Covered by unit tests testClearUnusedCredentialsDropsVaultTokenForKubernetes, testClearUnusedCredentialsDropsJwtForToken, and SWT UI tests in VaultWidgetVisibilityTest.
  3. Locale-Insensitive Enum Parsing:

    • Updated parseAuthType() and readAuthType() to use toUpperCase(Locale.ROOT).
    • Covered by unit test testAuthTypeParseUsesRootLocale under Turkish (tr-TR) locale.

All unit and UI tests pass, and Spotless is clean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants