+ * 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. + * + * @author Michal Budzyn + * @see KubernetesAuthentication + * @see #builder() + */ +class KubernetesAuthenticationOptions { + + static final String DEFAULT_KUBERNETES_AUTHENTICATION_PATH = "kubernetes"; + + /** + * Path of the kubernetes authentication backend mount. + */ + private final String path; + + /** + * The Role. + */ + private final String role; + + /** + * {@link KubernetesJwtSupplier} instance to obtain a service account JSON Web Tokens. + */ + private final KubernetesJwtSupplier jwtSupplier; + + private KubernetesAuthenticationOptions(String path, String role, + KubernetesJwtSupplier jwtSupplier) { + + this.path = path; + this.role = role; + this.jwtSupplier = jwtSupplier; + } + + /** + * @return a new {@link KubernetesAuthenticationOptionsBuilder}. + */ + static KubernetesAuthenticationOptionsBuilder builder() { + return new KubernetesAuthenticationOptionsBuilder(); + } + + /** + * @return the path of the aws authentication backend mount. + */ + String getPath() { + return path; + } + + /** + * @return name of the role against which the login is being attempted. + */ + String getRole() { + return role; + } + + /** + * @return JSON Web Token supplier. + */ + KubernetesJwtSupplier getJwtSupplier() { + return jwtSupplier; + } + + /** + * Builder for {@link KubernetesAuthenticationOptions}. + */ + static class KubernetesAuthenticationOptionsBuilder { + private String path = DEFAULT_KUBERNETES_AUTHENTICATION_PATH; + + private String role; + + private KubernetesJwtSupplier jwtSupplier; + + /** + * Configure the mount path. + * + * @param path must not be {@literal null} or empty. + * @return {@code this} {@link KubernetesAuthenticationOptionsBuilder}. + */ + KubernetesAuthenticationOptionsBuilder path(String path) { + + Assert.hasText(path, "Path must not be empty"); + + this.path = path; + return this; + } + + /** + * Configure the role. + * + * @param role name of the role against which the login is being attempted, must + * not be {@literal null} or empty. + * @return {@code this} {@link KubernetesAuthenticationOptionsBuilder}. + */ + KubernetesAuthenticationOptionsBuilder role(String role) { + + Assert.hasText(role, "Role must not be empty"); + + this.role = role; + return this; + } + + /** + * Configure the {@link KubernetesJwtSupplier} to obtain a Kubernetes authentication token. + * + * @param jwtSupplier the supplier, must not be {@literal null}. + * @return {@code this} {@link KubernetesAuthenticationOptionsBuilder}. + */ + KubernetesAuthenticationOptionsBuilder jwtSupplier( + KubernetesJwtSupplier jwtSupplier) { + + Assert.notNull(jwtSupplier, "JwtSupplier must not be null"); + + this.jwtSupplier = jwtSupplier; + return this; + } + + /** + * Build a new {@link KubernetesAuthenticationOptions} instance. + * + * @return a new {@link KubernetesAuthenticationOptions}. + */ + KubernetesAuthenticationOptions build() { + + Assert.notNull(role, "Role must not be null"); + + return new KubernetesAuthenticationOptions(path, role, + jwtSupplier == null ? new KubernetesServiceAccountTokenFile() + : jwtSupplier); + } + } +} \ No newline at end of file diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesJwtSupplier.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesJwtSupplier.java new file mode 100644 index 00000000..168089f0 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesJwtSupplier.java @@ -0,0 +1,33 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.vault.config; + +/** + * Interface to obtain a Kubernetes Service Account Token for Kubernetes authentication. + * Implementations are used by {@link KubernetesAuthentication}. + * + * @author Michal Budzyn + * @see KubernetesAuthentication + */ +interface KubernetesJwtSupplier { + + /** + * Get a JWT for Kubernetes authentication. + * + * @return the Kubernetes Service Account JWT. + */ + String get(); +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesServiceAccountTokenFile.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesServiceAccountTokenFile.java new file mode 100644 index 00000000..3fa8dfdb --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesServiceAccountTokenFile.java @@ -0,0 +1,121 @@ +/* + * Copyright 2016-2017 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.cloud.vault.config; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +import org.springframework.core.io.FileSystemResource; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; +import org.springframework.util.StreamUtils; +import org.springframework.vault.VaultException; + +/** + * Mechanism to retrieve a Kubernetes service account token. + *
+ * A file containing a token for a pod’s service account is automatically mounted at
+ * /var/run/secrets/kubernetes.io/serviceaccount/token
+ *
+ * @author Michal Budzyn
+ * @see KubernetesJwtSupplier
+ */
+class KubernetesServiceAccountTokenFile implements KubernetesJwtSupplier {
+
+ /**
+ * Default path to the service account token file.
+ */
+ static final String DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE = "/var/run/secrets/kubernetes.io/serviceaccount/token";
+
+ private byte[] token;
+
+ /**
+ * Create a new {@link KubernetesServiceAccountTokenFile} pointing to the
+ * {@link #DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE}. Construction fails with an
+ * exception if the file does not exist.
+ *
+ * @throws IllegalArgumentException if the
+ * {@link #DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE} does not exist.
+ */
+ KubernetesServiceAccountTokenFile() {
+ this(DEFAULT_KUBERNETES_SERVICE_ACCOUNT_TOKEN_FILE);
+ }
+
+ /**
+ * Create a new {@link KubernetesServiceAccountTokenFile}
+ * {@link KubernetesServiceAccountTokenFile} from a {@code path}.
+ *
+ * @param path path to the service account token file.
+ * @throws IllegalArgumentException if the{@code path} does not exist.
+ */
+ KubernetesServiceAccountTokenFile(String path) {
+ this(new FileSystemResource(path));
+ }
+
+ /**
+ * Create a new {@link KubernetesServiceAccountTokenFile}
+ * {@link KubernetesServiceAccountTokenFile} from a {@link File} handle.
+ *
+ * @param file path to the service account token file.
+ * @throws IllegalArgumentException if the{@code path} does not exist.
+ */
+ KubernetesServiceAccountTokenFile(File file) {
+ this(new FileSystemResource(file));
+ }
+
+ /**
+ * Create a new {@link KubernetesServiceAccountTokenFile}
+ * {@link KubernetesServiceAccountTokenFile} from a {@link Resource} handle.
+ *
+ * @param resource resource pointing to the service account token file.
+ * @throws IllegalArgumentException if the{@code path} does not exist.
+ */
+ KubernetesServiceAccountTokenFile(Resource resource) {
+
+ 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);
+ }
+ }
+
+ @Override
+ public String get() {
+ return new String(token, StandardCharsets.US_ASCII);
+ }
+
+ /**
+ * Read the token from {@link Resource}.
+ *
+ * @param resource the resource to read from, must not be {@literal null}.
+ * @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 {
+
+ Assert.notNull(resource, "Resource must not be null");
+
+ try (InputStream is = resource.getInputStream()) {
+ return StreamUtils.copyToByteArray(is);
+ }
+ }
+}
diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java
index 0c9da60d..65a953ed 100644
--- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java
+++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java
@@ -33,6 +33,7 @@ import org.springframework.validation.annotation.Validated;
* @author Spencer Gibb
* @author Mark Paluch
* @author Kevin Holditch
+ * @author Michal Budzyn
*/
@ConfigurationProperties("spring.cloud.vault")
@Data
@@ -99,6 +100,8 @@ public class VaultProperties implements EnvironmentAware {
private AwsIamProperties awsIam = new AwsIamProperties();
+ private KubernetesProperties kubernetes = new KubernetesProperties();
+
private Ssl ssl = new Ssl();
private Config config = new Config();
@@ -250,6 +253,27 @@ public class VaultProperties implements EnvironmentAware {
private String serverName;
}
+ @Data
+ public static class KubernetesProperties {
+
+ /**
+ * Mount path of the Kubernetes authentication backend.
+ */
+ @NotEmpty
+ private String kubernetesPath = "kubernetes";
+
+ /**
+ * The Role.
+ */
+ private String role = null;
+
+ /**
+ * File with service account token.
+ */
+ @NotEmpty
+ private String serviceAccountTokenFile = "/var/run/secrets/kubernetes.io/serviceaccount/token";
+ }
+
@Data
public static class Ssl {
@@ -308,6 +332,6 @@ public class VaultProperties implements EnvironmentAware {
}
public enum AuthenticationMethod {
- TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, CERT, CUBBYHOLE;
+ TOKEN, APPID, APPROLE, AWS_EC2, AWS_IAM, CERT, CUBBYHOLE, KUBERNETES;
}
}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/KubernetesAuthenticationUnitTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/KubernetesAuthenticationUnitTests.java
new file mode 100644
index 00000000..966d4345
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/KubernetesAuthenticationUnitTests.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.cloud.vault.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.jsonPath;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.method;
+import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError;
+import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.http.HttpMethod;
+import org.springframework.http.MediaType;
+import org.springframework.test.web.client.MockRestServiceServer;
+import org.springframework.vault.VaultException;
+import org.springframework.vault.authentication.LoginToken;
+import org.springframework.vault.client.VaultClients;
+import org.springframework.vault.client.VaultClients.PrefixAwareUriTemplateHandler;
+import org.springframework.vault.support.VaultToken;
+import org.springframework.web.client.RestTemplate;
+
+/**
+ * Unit tests for {@link KubernetesAuthentication}.
+ *
+ * @author Michal Budzyn
+ */
+public class KubernetesAuthenticationUnitTests {
+
+ private RestTemplate restTemplate;
+ private MockRestServiceServer mockRest;
+
+ @Before
+ public void before() throws Exception {
+
+ RestTemplate restTemplate = VaultClients.createRestTemplate();
+ restTemplate.setUriTemplateHandler(new PrefixAwareUriTemplateHandler());
+ this.mockRest = MockRestServiceServer.createServer(restTemplate);
+ this.restTemplate = restTemplate;
+ }
+
+ @Test
+ public void loginShouldObtainTokenWithStaticJwtSupplier() throws Exception {
+
+ KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder()
+ .role("hello") //
+ .jwtSupplier((new KubernetesJwtSupplier() {
+ @Override
+ public String get() {
+ return "my-jwt-token";
+ }
+ })).build();
+
+ mockRest.expect(requestTo("/auth/kubernetes/login"))
+ .andExpect(method(HttpMethod.POST))
+ .andExpect(jsonPath("$.role").value("hello"))
+ .andExpect(jsonPath("$.jwt").value("my-jwt-token"))
+ .andRespond(withSuccess().contentType(MediaType.APPLICATION_JSON)
+ .body("{" + "\"auth\":{\"client_token\":\"my-token\"}" + "}"));
+
+ KubernetesAuthentication authentication = new KubernetesAuthentication(options, restTemplate);
+
+ VaultToken login = authentication.login();
+ assertThat(login).isInstanceOf(LoginToken.class);
+ assertThat(login.getToken()).isEqualTo("my-token");
+ }
+
+ @Test(expected = VaultException.class)
+ public void loginShouldFail() throws Exception {
+
+ KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder()
+ .role("hello").jwtSupplier(new KubernetesJwtSupplier() {
+ @Override
+ public String get() {
+ return "my-jwt-token";
+ }
+ }).build();
+
+ mockRest.expect(requestTo("/auth/kubernetes/login")) //
+ .andRespond(withServerError());
+
+ new KubernetesAuthentication(options, restTemplate).login();
+ }
+}
diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigKubernetesTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigKubernetesTests.java
new file mode 100644
index 00000000..02e6d235
--- /dev/null
+++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigKubernetesTests.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016-2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.cloud.vault.config;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.Assume.assumeTrue;
+import static org.springframework.cloud.vault.util.Settings.findWorkDir;
+
+import java.io.File;
+import java.nio.charset.StandardCharsets;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+import org.assertj.core.util.Files;
+import org.junit.BeforeClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.cloud.vault.util.VaultRule;
+import org.springframework.cloud.vault.util.Version;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.util.StringUtils;
+import org.springframework.vault.core.VaultOperations;
+
+/**
+ * Integration test using config infrastructure with Kubernetes authentication.
+ *
+ * @author Michal Budzyn
+ */
+@RunWith(SpringJUnit4ClassRunner.class)
+@SpringBootTest(classes = VaultConfigKubernetesTests.TestApplication.class, properties = {
+ "spring.cloud.vault.authentication=kubernetes",
+ "spring.cloud.vault.kubernetes.role=my-role",
+ "spring.cloud.vault.kubernetes.service-account-token-file=../work/minikube/hello-minikube-token",
+ "spring.cloud.vault.application-name=VaultConfigKubernetesTests" })
+public class VaultConfigKubernetesTests {
+
+ @Value("${vault.value}")
+ String configValue;
+
+ @BeforeClass
+ public static void beforeClass() throws Exception {
+
+ VaultRule vaultRule = new VaultRule();
+ vaultRule.before();
+
+ String minikubeIp = System.getProperty("MINIKUBE_IP");
+ assumeTrue(StringUtils.hasText(minikubeIp) && vaultRule.prepare().getVersion()
+ .isGreaterThanOrEqualTo(Version.parse("0.8.3")));
+
+ if (!vaultRule.prepare().hasAuth("kubernetes")) {
+ vaultRule.prepare().mountAuth("kubernetes");
+ }
+
+ VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
+
+ String rules = "{ \"name\": \"testpolicy\",\n" //
+ + " \"path\": {\n" //
+ + " \"*\": { \"policy\": \"read\" }\n" //
+ + " }\n" //
+ + "}";
+
+ vaultOperations.write("sys/policy/testpolicy",
+ Collections.singletonMap("rules", rules));
+
+ vaultOperations.write(
+ "secret/" + VaultConfigKubernetesTests.class.getSimpleName(),
+ Collections.singletonMap("vault.value", "foo"));
+
+ File workDir = findWorkDir();
+ String certificate = Files.contentOf(new File(workDir, "minikube/ca.crt"),
+ StandardCharsets.US_ASCII);
+
+ String host = String.format("https://%s:8443", minikubeIp);
+ Map