From 4765ccc241e4b61ca09b61a5eb9321da348c7798 Mon Sep 17 00:00:00 2001 From: Michal Budzyn Date: Sat, 28 Oct 2017 19:53:07 +0200 Subject: [PATCH] Add authentication for Kubernetes Service Account Token. We now support Vault authentication using Kubernetes Service Account Token files. spring.cloud.vault: authentication: KUBERNETES kubernetes: role: dev-role Original pull request: gh-176. Related ticket: gh-173. Closes gh-173. --- .../config/ClientAuthenticationFactory.java | 23 ++- .../config/KubernetesAuthentication.java | 118 ++++++++++++++ .../KubernetesAuthenticationOptions.java | 154 ++++++++++++++++++ .../vault/config/KubernetesJwtSupplier.java | 33 ++++ .../KubernetesServiceAccountTokenFile.java | 121 ++++++++++++++ .../cloud/vault/config/VaultProperties.java | 26 ++- .../KubernetesAuthenticationUnitTests.java | 98 +++++++++++ .../config/VaultConfigKubernetesTests.java | 118 ++++++++++++++ src/test/bash/local_run_k8s.sh | 73 +++++++++ 9 files changed, 762 insertions(+), 2 deletions(-) create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthentication.java create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthenticationOptions.java create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesJwtSupplier.java create mode 100644 spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesServiceAccountTokenFile.java create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/KubernetesAuthenticationUnitTests.java create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigKubernetesTests.java create mode 100755 src/test/bash/local_run_k8s.sh diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java index 4d40c01a..3a2eee2b 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/ClientAuthenticationFactory.java @@ -55,6 +55,7 @@ import org.springframework.web.client.RestOperations; * * @author Mark Paluch * @author Kevin Holditch + * @author Michal Budzyn * @since 1.1 */ @RequiredArgsConstructor @@ -93,7 +94,10 @@ class ClientAuthenticationFactory { case CUBBYHOLE: return cubbyholeAuthentication(); - } + + case KUBERNETES: + return kubernetesAuthentication(vaultProperties); + } throw new UnsupportedOperationException(String.format( "Client authentication %s not supported", @@ -221,6 +225,23 @@ class ClientAuthenticationFactory { return new CubbyholeAuthentication(options, restOperations); } + private ClientAuthentication kubernetesAuthentication(VaultProperties vaultProperties) { + VaultProperties.KubernetesProperties kubernetes = vaultProperties.getKubernetes(); + + Assert.hasText(kubernetes.getRole(), + "Role (spring.cloud.vault.kubernetes.role) must not be empty"); + Assert.hasText(kubernetes.getServiceAccountTokenFile(), + "Role (spring.cloud.vault.kubernetes.service-account-token-file) must not be empty"); + + KubernetesAuthenticationOptions options = KubernetesAuthenticationOptions.builder() + .path(kubernetes.getKubernetesPath()).role(kubernetes.getRole()) + .jwtSupplier(new KubernetesServiceAccountTokenFile( + kubernetes.getServiceAccountTokenFile())) + .build(); + + return new KubernetesAuthentication(options, restOperations); + } + private static class AwsCredentialProvider { private static AWSCredentialsProvider getAwsCredentialsProvider() { diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthentication.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthentication.java new file mode 100644 index 00000000..6472d597 --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthentication.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 java.util.HashMap; +import java.util.Map; + +import org.springframework.util.Assert; +import org.springframework.vault.VaultException; +import org.springframework.vault.authentication.ClientAuthentication; +import org.springframework.vault.authentication.LoginToken; +import org.springframework.vault.client.VaultResponses; +import org.springframework.vault.support.VaultResponse; +import org.springframework.vault.support.VaultToken; +import org.springframework.web.client.HttpStatusCodeException; +import org.springframework.web.client.RestOperations; + +/** + * Kubernetes implementation of {@link ClientAuthentication}. {@link KubernetesAuthentication} + * uses a Kubernetes Service Account JSON Web Token to login into Vault. JWT and Role are + * sent in the login request to Vault to obtain a {@link VaultToken}. + * + * @author Michal Budzyn + * @see KubernetesAuthenticationOptions + * @see RestOperations + * @see Auth Backend: + * Kubernetes + */ +class KubernetesAuthentication implements ClientAuthentication { + + private final KubernetesAuthenticationOptions options; + + private final RestOperations restOperations; + + /** + * Create a {@link KubernetesAuthentication} using {@link KubernetesAuthenticationOptions} and + * {@link RestOperations}. + * + * @param options must not be {@literal null}. + * @param restOperations must not be {@literal null}. + */ + KubernetesAuthentication(KubernetesAuthenticationOptions options, + RestOperations restOperations) { + + Assert.notNull(options, "KubeAuthenticationOptions must not be null"); + Assert.notNull(restOperations, "RestOperations must not be null"); + + this.options = options; + this.restOperations = restOperations; + } + + private static Map getKubernetesLogin(String role, String jwt) { + + Assert.hasText(role, "role must not be empty"); + Assert.hasText(role, "jwt must not be empty"); + + Map login = new HashMap<>(); + + login.put("jwt", jwt); + login.put("role", role); + + return login; + } + + @Override + public VaultToken login() throws VaultException { + + Map login = getKubernetesLogin(options.getRole(), + options.getJwtSupplier().get()); + + try { + VaultResponse response = restOperations.postForObject("auth/{mount}/login", + login, VaultResponse.class, options.getPath()); + + Assert.state(response != null && response.getAuth() != null, + "Auth field must not be null"); + + return from(response.getAuth()); + } + catch (HttpStatusCodeException e) { + throw new VaultException(String.format("Cannot login using kubernetes: %s", + VaultResponses.getError(e.getResponseBodyAsString()))); + } + } + + /* + * @see org.springframework.vault.authentication.LoginTokenUtil#from + */ + private LoginToken from(Map auth) { + + String token = (String) auth.get("client_token"); + Boolean renewable = (Boolean) auth.get("renewable"); + Number leaseDuration = (Number) auth.get("lease_duration"); + + if (renewable != null && renewable) { + return LoginToken.renewable(token, leaseDuration.longValue()); + } + + if (leaseDuration != null) { + return LoginToken.of(token, leaseDuration.longValue()); + } + + return LoginToken.of(token); + } +} diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthenticationOptions.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthenticationOptions.java new file mode 100644 index 00000000..01aa4b5e --- /dev/null +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/KubernetesAuthenticationOptions.java @@ -0,0 +1,154 @@ +/* + * 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 org.springframework.util.Assert; + +/** + * Authentication options for {@link KubernetesAuthentication}. + *

+ * 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 kubeConfig = new HashMap<>(); + kubeConfig.put("kubernetes_ca_cert", certificate); + kubeConfig.put("kubernetes_host", host); + vaultOperations.write("auth/kubernetes/config", kubeConfig); + + Map roleData = new HashMap<>(); + roleData.put("bound_service_account_names", "default"); + roleData.put("bound_service_account_namespaces", "default"); + roleData.put("policies", "testpolicy"); + roleData.put("ttl", "1h"); + vaultOperations.write("auth/kubernetes/role/my-role", roleData); + + } + + @Test + public void contextLoads() { + assertThat(configValue).isEqualTo("foo"); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +} diff --git a/src/test/bash/local_run_k8s.sh b/src/test/bash/local_run_k8s.sh new file mode 100755 index 00000000..3c06ae10 --- /dev/null +++ b/src/test/bash/local_run_k8s.sh @@ -0,0 +1,73 @@ +#!/bin/bash + +CMD_MINIKUBE=${1:-minikube} +CMD_KUBECTL=${2:-kubectl} +MINIKUBE_OPTS=${3:-} + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" + +if [ ! -d "work" ]; then + echo "work directory could not be found." + exit 1 +fi + +mkdir -p work/minikube +SERVICE_ACCOUNT_TOKEN_FILE=work/minikube/hello-minikube-token +SERVICE_ACCOUNT_CA_CRT=work/minikube/ca.crt + +function is_cluster_running() { + local _running=$(${CMD_MINIKUBE} status | grep "cluster: Running" || true) + echo "$_running" +} + +if [[ -z "$(is_cluster_running)" ]]; then + ${CMD_MINIKUBE} start "${MINIKUBE_OPTS}" + while [[ -z "$(is_cluster_running)" ]]; do + echo "Wait for minikube cluster to be up" + sleep 1 + done +fi + +export MINIKUBE_IP=$(${CMD_MINIKUBE} ip) +echo "MINIKUBE_IP ${MINIKUBE_IP}" + +# ensure kubectl context is not stale +${CMD_MINIKUBE} update-context + +# https://kubernetes.io/docs/getting-started-guides/minikube/ +${CMD_KUBECTL} run hello-minikube --image=gcr.io/google_containers/echoserver:1.4 --port=8080 +${CMD_KUBECTL} expose deployment hello-minikube --type=NodePort + +# Wait for service to be ready +echo "Wait for hello-minikube service to be ready" +HELLO_MINIKUBE_URL=$(${CMD_MINIKUBE} service hello-minikube --url --interval 5 --wait 120) +if [ $? != 0 ] ; then + echo "Error during service startup" + echo "In case of DNS problems try 'VBoxManage modifyvm minikube --natdnshostresolver1 on'" + # kubectl get pod -> STATUS: ContainerCreating + exit 1 +fi +echo "HELLO_MINIKUBE_URL ${HELLO_MINIKUBE_URL}" + +POD_NAME=$(${CMD_KUBECTL} get pod --selector=run=hello-minikube -o jsonpath='{.items..metadata.name}') +# Copy service account token +${CMD_KUBECTL} exec ${POD_NAME} -- cat /var/run/secrets/kubernetes.io/serviceaccount/token > ${SERVICE_ACCOUNT_TOKEN_FILE} +if [ $? != 0 ] ; then + echo "Error while retrieving service account token file" + exit 1 +fi +# Copy ca cert +${CMD_KUBECTL} exec ${POD_NAME} -- cat /var/run/secrets/kubernetes.io/serviceaccount/ca.crt > ${SERVICE_ACCOUNT_CA_CRT} +if [ $? != 0 ] ; then + echo "Error while retrieving service account ca.crt" + exit 1 +fi + +#BASEDIR=`dirname $0`/../../.. +#sh <( +#cat <<-EOF +#cd ${BASEDIR} && ${BASEDIR}/src/test/bash/env.sh +#vault auth-enable kubernetes +#vault write auth/kubernetes/config kubernetes_host=https://$(minikube ip):8443 kubernetes_ca_cert=@$HOME/.minikube/ca.crt +#EOF +#)