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:
Mark Paluch
2019-07-07 16:02:54 +02:00
parent b390aab875
commit 28976b1fd5
7 changed files with 103 additions and 37 deletions

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -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");

View File

@@ -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() {

View File

@@ -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

View File

@@ -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();
}
}

View File

@@ -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());
}