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.
This commit is contained in:
@@ -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.
|
||||
* <p>
|
||||
* 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<String> jwtSupplier) {
|
||||
@@ -156,7 +162,7 @@ public class KubernetesAuthenticationOptions {
|
||||
|
||||
return new KubernetesAuthenticationOptions(path, role,
|
||||
jwtSupplier == null ? new KubernetesServiceAccountTokenFile()
|
||||
: jwtSupplier);
|
||||
.cached() : jwtSupplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,4 +36,21 @@ public interface KubernetesJwtSupplier extends Supplier<String> {
|
||||
*/
|
||||
@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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -48,26 +48,30 @@ class AppRoleAuthenticationIntegrationTestBase extends IntegrationTestSupport {
|
||||
prepare().mountAuth("approle");
|
||||
}
|
||||
|
||||
getVaultOperations().doWithSession(restOperations -> {
|
||||
getVaultOperations().doWithSession(
|
||||
restOperations -> {
|
||||
|
||||
Map<String, String> withSecretId = new HashMap<String, String>();
|
||||
withSecretId.put("policies", "dummy"); // policy
|
||||
withSecretId.put("bound_cidr_list", "0.0.0.0/0");
|
||||
withSecretId.put("bind_secret_id", "true");
|
||||
Map<String, String> 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<String, String> noSecretIdRole = new HashMap<String, String>();
|
||||
noSecretIdRole.put("policies", "dummy"); // policy
|
||||
noSecretIdRole.put("bound_cidr_list", "0.0.0.0/0");
|
||||
noSecretIdRole.put("bind_secret_id", "false");
|
||||
Map<String, String> 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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<String> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user