From 063ae47fb96e660b2fc5c5a0a84401e5bf80c38e 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 | 26 +++- .../cloud/vault/config/VaultProperties.java | 26 +++- .../config/VaultConfigKubernetesTests.java | 118 ++++++++++++++++++ spring-cloud-vault-dependencies/pom.xml | 2 +- src/test/bash/local_run_k8s.sh | 73 +++++++++++ 5 files changed, 242 insertions(+), 3 deletions(-) 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 3301507f..dca89cd6 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 @@ -42,6 +42,9 @@ import org.springframework.vault.authentication.ClientCertificateAuthentication; import org.springframework.vault.authentication.CubbyholeAuthentication; import org.springframework.vault.authentication.CubbyholeAuthenticationOptions; import org.springframework.vault.authentication.IpAddressUserId; +import org.springframework.vault.authentication.KubernetesAuthentication; +import org.springframework.vault.authentication.KubernetesAuthenticationOptions; +import org.springframework.vault.authentication.KubernetesServiceAccountTokenFile; import org.springframework.vault.authentication.MacAddressUserId; import org.springframework.vault.authentication.StaticUserId; import org.springframework.vault.authentication.TokenAuthentication; @@ -55,6 +58,7 @@ import org.springframework.web.client.RestOperations; * * @author Mark Paluch * @author Kevin Holditch + * @author Michal Budzyn * @since 1.1 */ @RequiredArgsConstructor @@ -93,7 +97,10 @@ class ClientAuthenticationFactory { case CUBBYHOLE: return cubbyholeAuthentication(); - } + + case KUBERNETES: + return kubernetesAuthentication(vaultProperties); + } throw new UnsupportedOperationException(String.format( "Client authentication %s not supported", @@ -221,6 +228,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/VaultProperties.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultProperties.java index d83777d3..05becb92 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 @@ -31,6 +31,7 @@ import org.springframework.validation.annotation.Validated; * @author Spencer Gibb * @author Mark Paluch * @author Kevin Holditch + * @author Michal Budzyn */ @ConfigurationProperties("spring.cloud.vault") @Data @@ -97,6 +98,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(); @@ -240,6 +243,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 @Validated public static class Ssl { @@ -299,6 +323,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/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/spring-cloud-vault-dependencies/pom.xml b/spring-cloud-vault-dependencies/pom.xml index 0d44d0a4..008b3001 100644 --- a/spring-cloud-vault-dependencies/pom.xml +++ b/spring-cloud-vault-dependencies/pom.xml @@ -18,7 +18,7 @@ Spring Cloud Vault Dependencies - 2.0.0.M3 + 2.0.0.BUILD-SNAPSHOT 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 +#)