From 28976b1fd5bd9ff25d7a651cc627ada202d5fbf0 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Sun, 7 Jul 2019 16:02:54 +0200 Subject: [PATCH] Allow re-reading Kubernetes service token for each authentication attempt. KubernetesJwtSupplier now reads the token resource on each token retrieval. KubernetesAuthenticationOptions defaults to a cached variant for e.g. reactive use to retain previous behavior. Closes gh-449. --- .../KubernetesAuthenticationOptions.java | 8 +++- .../authentication/KubernetesJwtSupplier.java | 17 +++++++++ .../KubernetesServiceAccountTokenFile.java | 21 ++++++----- ...RoleAuthenticationIntegrationTestBase.java | 37 ++++++++++--------- ...leAuthenticationStepsIntegrationTests.java | 21 +++++++++-- .../KubernetesAuthenticationUnitTests.java | 26 +++++++++++++ .../asciidoc/reference/authentication.adoc | 10 ++--- 7 files changed, 103 insertions(+), 37 deletions(-) diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java index c9db71c3..620777d4 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesAuthenticationOptions.java @@ -26,11 +26,16 @@ import org.springframework.util.Assert; * Authentication options provide the path, role and jwt supplier. * {@link KubernetesAuthentication} can be constructed using {@link #builder()}. Instances * of this class are immutable once constructed. + *

+ * Default to obtain a cached token from + * {@code /var/run/secrets/kubernetes.io/serviceaccount/token}. * * @author Michal Budzyn * @author Mark Paluch * @since 2.0 * @see KubernetesAuthentication + * @see KubernetesJwtSupplier + * @see KubernetesServiceAccountTokenFile * @see #builder() */ public class KubernetesAuthenticationOptions { @@ -135,6 +140,7 @@ public class KubernetesAuthenticationOptions { * * @param jwtSupplier the supplier, must not be {@literal null}. * @return {@code this} {@link KubernetesAuthenticationOptionsBuilder}. + * @see KubernetesJwtSupplier */ public KubernetesAuthenticationOptionsBuilder jwtSupplier( Supplier jwtSupplier) { @@ -156,7 +162,7 @@ public class KubernetesAuthenticationOptions { return new KubernetesAuthenticationOptions(path, role, jwtSupplier == null ? new KubernetesServiceAccountTokenFile() - : jwtSupplier); + .cached() : jwtSupplier); } } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java index 1f6c01a8..63a8f091 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesJwtSupplier.java @@ -36,4 +36,21 @@ public interface KubernetesJwtSupplier extends Supplier { */ @Override String get(); + + /** + * Retrieve a cached {@link KubernetesJwtSupplier} that obtains the JWT early and + * reuses the token for each {@link #get()} call. This is useful to prevent I/O + * operations in e.g. reactive usage. + *

+ * Reusing a cached token can lead to authentication failures if the token expires. + * + * @return a caching {@link KubernetesJwtSupplier}. + * @since 2.2 + */ + default KubernetesJwtSupplier cached() { + + String jwt = get(); + + return () -> jwt; + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java index a65c8101..8b087eb3 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/authentication/KubernetesServiceAccountTokenFile.java @@ -44,7 +44,7 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier */ public static final String DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token"; - private byte[] token; + private final Resource resource; /** * Create a new {@link KubernetesServiceAccountTokenFile} pointing to the @@ -92,18 +92,19 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier Assert.isTrue(resource.exists(), () -> String.format("Resource %s does not exist", resource)); - try { - this.token = readToken(resource); - } - catch (IOException e) { - throw new VaultException(String.format( - "Kube JWT token retrieval from %s failed", resource), e); - } + this.resource = resource; } @Override public String get() { - return new String(token, StandardCharsets.US_ASCII); + + try { + return new String(readToken(this.resource), StandardCharsets.US_ASCII); + } + catch (IOException e) { + throw new VaultException(String.format( + "Kube JWT token retrieval from %s failed", this.resource), e); + } } /** @@ -113,7 +114,7 @@ public class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier * @return the new byte array that has been copied to (possibly empty). * @throws IOException in case of I/O errors. */ - protected static byte[] readToken(Resource resource) throws IOException { + private static byte[] readToken(Resource resource) throws IOException { Assert.notNull(resource, "Resource must not be null"); diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java index e667c89a..87ce8545 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationIntegrationTestBase.java @@ -48,26 +48,30 @@ class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport { prepare().mountAuth("approle"); } - getVaultOperations().doWithSession(restOperations -> { + getVaultOperations().doWithSession( + restOperations -> { - Map withSecretId = new HashMap(); - withSecretId.put("policies", "dummy"); // policy - withSecretId.put("bound_cidr_list", "0.0.0.0/0"); - withSecretId.put("bind_secret_id", "true"); + Map withSecretId = new HashMap<>(); + withSecretId.put("policies", "dummy"); + withSecretId.put("bound_cidr_list", "0.0.0.0/0"); + withSecretId.put("bind_secret_id", "true"); + withSecretId.put("token_ttl", "60s"); + withSecretId.put("token_max_ttl", "60s"); - restOperations.postForEntity("auth/approle/role/with-secret-id", - withSecretId, Map.class); + restOperations.postForEntity("auth/approle/role/with-secret-id", + withSecretId, Map.class); - Map noSecretIdRole = new HashMap(); - noSecretIdRole.put("policies", "dummy"); // policy - noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); - noSecretIdRole.put("bind_secret_id", "false"); + Map noSecretIdRole = new HashMap<>(); + noSecretIdRole.put("policies", "dummy"); // policy + noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0"); + noSecretIdRole.put("bind_secret_id", "false"); + noSecretIdRole.put("max_ttl", "60s"); - restOperations.postForEntity("auth/approle/role/no-secret-id", - noSecretIdRole, Map.class); + restOperations.postForEntity("auth/approle/role/no-secret-id", + noSecretIdRole, Map.class); - return null; - }); + return null; + }); } VaultOperations getVaultOperations() { @@ -77,8 +81,7 @@ class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport { String getRoleId(String roleName) { return (String) getVaultOperations() .read(String.format("auth/approle/role/%s/role-id", roleName)) - .getRequiredData() - .get("role_id"); + .getRequiredData().get("role_id"); } VaultToken generateWrappedSecretIdResponse() { diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java index 16549f8f..5864627a 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/AppRoleAuthenticationStepsIntegrationTests.java @@ -19,6 +19,7 @@ import java.util.Collections; import org.junit.jupiter.api.Test; +import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.vault.VaultException; import org.springframework.vault.authentication.AppRoleAuthenticationOptions.RoleId; import org.springframework.vault.authentication.AppRoleAuthenticationOptions.SecretId; @@ -40,7 +41,8 @@ class AppRoleAuthenticationStepsIntegrationTests extends AppRoleAuthenticationIntegrationTestBase { @Test - void authenticationStepsShouldAuthenticateWithWrappedSecretId() { + void authenticationStepsShouldAuthenticateWithWrappedSecretId() + throws InterruptedException { String roleId = getRoleId("with-secret-id"); VaultToken unwrappingToken = generateWrappedSecretIdResponse(); @@ -53,7 +55,7 @@ class AppRoleAuthenticationStepsIntegrationTests extends AppRoleAuthentication.createAuthenticationSteps(options), prepare() .getRestTemplate()); - assertThat(executor.login()).isNotNull(); + } @Test @@ -77,7 +79,7 @@ class AppRoleAuthenticationStepsIntegrationTests extends } @Test - void shouldAuthenticateWithFullPullMode() { + void shouldAuthenticateWithFullPullMode() throws InterruptedException { AppRoleAuthenticationOptions options = AppRoleAuthenticationOptions.builder() .appRole("with-secret-id").roleId(RoleId.pull(Settings.token())) @@ -87,7 +89,18 @@ class AppRoleAuthenticationStepsIntegrationTests extends AppRoleAuthentication.createAuthenticationSteps(options), prepare() .getRestTemplate()); - assertThat(executor.login()).isNotNull(); + ThreadPoolTaskScheduler taskScheduler = new ThreadPoolTaskScheduler(); + taskScheduler.afterPropertiesSet(); + LifecycleAwareSessionManager sessionManager = new LifecycleAwareSessionManager( + executor, taskScheduler, prepare().getRestTemplate()); + + VaultToken sessionToken = sessionManager.getSessionToken(); + System.out.println(sessionToken); + + Thread.sleep(90000); + VaultToken sessionToken2 = sessionManager.getSessionToken(); + System.out.println(sessionToken2); + } @Test diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java index 3016f4eb..272bb121 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/KubernetesAuthenticationUnitTests.java @@ -15,6 +15,8 @@ */ package org.springframework.vault.authentication; +import java.util.concurrent.atomic.AtomicReference; + import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -91,4 +93,28 @@ class KubernetesAuthenticationUnitTests { assertThatExceptionOfType(VaultException.class).isThrownBy( () -> new KubernetesAuthentication(options, restTemplate).login()); } + + @Test + void shouldReuseCachedToken() { + + AtomicReference token = new AtomicReference<>("foo"); + KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions + .builder().role("hello") // + .jwtSupplier(((KubernetesJwtSupplier) token::get).cached()).build(); + + token.set("bar"); + + mockRest.expect(requestTo("/auth/kubernetes/login")) + .andExpect(method(HttpMethod.POST)) + .andExpect(jsonPath("$.role").value("hello")) + .andExpect(jsonPath("$.jwt").value("foo")) + .andRespond( + withSuccess().contentType(MediaType.APPLICATION_JSON).body( + "{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}")); + + KubernetesAuthentication authentication = new KubernetesAuthentication(options, + restTemplate); + + authentication.login(); + } } diff --git a/src/main/asciidoc/reference/authentication.adoc b/src/main/asciidoc/reference/authentication.adoc index c4121b58..e9fb3c9f 100644 --- a/src/main/asciidoc/reference/authentication.adoc +++ b/src/main/asciidoc/reference/authentication.adoc @@ -184,7 +184,7 @@ public class MyUserIdMechanism implements AppIdUserIdMechanism { @Override public String createUserId() { - String userId = ... + String userId = … return userId; } } @@ -694,9 +694,9 @@ See also: Vault supports since 0.8.3 https://www.vaultproject.io/docs/auth/kubernetes.html[kubernetes]-based authentication using Kubernetes tokens. Using Kubernetes authentication requires a Kubernetes Service Account Token, -usually mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`. The file contains -the token which is read and sent to Vault. Vault verifies its validity using Kubernets' API -during login. +usually mounted at `/var/run/secrets/kubernetes.io/serviceaccount/token`. +The file contains the token which is read and sent to Vault. +Vault verifies its validity using Kubernetes' API during login. Configuring Kubernetes authentication requires at least the role name to be provided: @@ -712,7 +712,7 @@ class AppConfig extends AbstractVaultConfiguration { public ClientAuthentication clientAuthentication() { KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder() - .role(…).build(); + .role(…).jwtSupplier(…).build(); return new KubernetesAuthentication(options, restOperations()); }