From 7c7aa05ae8ccb2ef2d0da6066651565aa9ca3c01 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Wed, 21 Sep 2016 22:21:31 +0200 Subject: [PATCH] Add support for cubbyhole authentication. We now support cubbyhole authentication to securely use tokens. Cubbyhole authentication uses Vault primitives to provide a secured authentication workflow. Cubbyhole authentication uses tokens as primary login method. An ephemeral token is used to obtain a second, login VaultToken from Vault's Cubbyhole secret backend. The login token is usually longer-lived and used to interact with Vault. The login token will be retrieved from a wrapped response stored at `/cubbyhole/response`. A wrapped token can be created with: vault token-create -wrap-ttl="10m" boostrap.yml: spring.cloud.vault: authentication: CUBBYHOLE token: (the wrapping token) Fixes gh-15. --- .../asciidoc/spring-cloud-vault-config.adoc | 42 +++++++ .../config/VaultBootstrapConfiguration.java | 27 ++++- .../cloud/vault/config/VaultProperties.java | 2 +- ...ultConfigCubbyholeAuthenticationTests.java | 105 ++++++++++++++++++ 4 files changed, 171 insertions(+), 5 deletions(-) create mode 100644 spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigCubbyholeAuthenticationTests.java diff --git a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc index c44a6705..defdd7be 100644 --- a/docs/src/main/asciidoc/spring-cloud-vault-config.adoc +++ b/docs/src/main/asciidoc/spring-cloud-vault-config.adoc @@ -250,6 +250,48 @@ trust-store. See also: https://www.vaultproject.io/docs/auth/cert.html[Vault Documentation: Using the cert auth backend] + +== Cubbyhole authentication + +Cubbyhole authentication uses Vault primitives to provide a secured authentication +workflow. Cubbyhole authentication uses tokens as primary login method. +An ephemeral token is used to obtain a second, login VaultToken from Vault's +Cubbyhole secret backend. The login token is usually longer-lived and used to +interact with Vault. The login token will be retrieved from a wrapped +response stored at `/cubbyhole/response`. + +*Creating a wrapped token* + +NOTE: Response Wrapping for token creation requires Vault 0.6.0 or higher. + +.Crating and storing tokens +==== +[source,shell] +---- +$ vault token-create -wrap-ttl="10m" +Key Value +--- ----- +wrapping_token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +wrapping_token_ttl: 0h10m0s +wrapping_token_creation_time: 2016-09-18 20:29:48.652957077 +0200 CEST +wrapped_accessor: 46b6aebb-187f-932a-26d7-4f3d86a68319 +---- +==== + +[source,yaml] +.bootstrap.yml +---- +spring.cloud.vault: + authentication: CUBBYHOLE + token: 397ccb93-ff6c-b17b-9389-380b01ca2645 +---- + +See also: + +* https://www.vaultproject.io/docs/concepts/tokens.html[Vault Documentation: Tokens] +* https://www.vaultproject.io/docs/secrets/cubbyhole/index.html[Vault Documentation:Cubbyhole Secret Backend] +* https://www.vaultproject.io/docs/concepts/response-wrapping.html[Vault Documentation: Response Wrapping] + == Backends [[vault-client-generic]] diff --git a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java index 8e110cd2..31fac91c 100644 --- a/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java +++ b/spring-cloud-vault-config/src/main/java/org/springframework/cloud/vault/config/VaultBootstrapConfiguration.java @@ -38,6 +38,8 @@ import org.springframework.vault.authentication.AwsEc2Authentication; import org.springframework.vault.authentication.AwsEc2AuthenticationOptions; import org.springframework.vault.authentication.ClientAuthentication; import org.springframework.vault.authentication.ClientCertificateAuthentication; +import org.springframework.vault.authentication.CubbyholeAuthentication; +import org.springframework.vault.authentication.CubbyholeAuthenticationOptions; import org.springframework.vault.authentication.DefaultSessionManager; import org.springframework.vault.authentication.IpAddressUserId; import org.springframework.vault.authentication.MacAddressUserId; @@ -54,6 +56,7 @@ import org.springframework.vault.core.VaultOperations; import org.springframework.vault.core.VaultTemplate; import org.springframework.vault.support.ClientOptions; import org.springframework.vault.support.SslConfiguration; +import org.springframework.vault.support.VaultToken; import org.springframework.web.client.RestTemplate; /** @@ -73,7 +76,8 @@ public class VaultBootstrapConfiguration { private final Collection vaultSecretBackends; private final Collection> factories; - public VaultBootstrapConfiguration(ApplicationContext applicationContext, VaultProperties vaultProperties) { + public VaultBootstrapConfiguration(ApplicationContext applicationContext, + VaultProperties vaultProperties) { this.applicationContext = applicationContext; this.vaultProperties = vaultProperties; @@ -98,7 +102,6 @@ public class VaultBootstrapConfiguration { vaultGenericBackendProperties, backendAccessors); } - @Bean @ConditionalOnMissingBean public ClientAuthentication clientAuthentication() { @@ -108,8 +111,8 @@ public class VaultBootstrapConfiguration { switch (vaultProperties.getAuthentication()) { case TOKEN: - Assert.hasText("Token (spring.cloud.vault.token) must not be empty", - vaultProperties.getToken()); + Assert.hasText(vaultProperties.getToken(), + "Token (spring.cloud.vault.token) must not be empty"); return new TokenAuthentication(vaultProperties.getToken()); case APPID: @@ -121,6 +124,9 @@ public class VaultBootstrapConfiguration { case AWS_EC2: return awsEc2Authentication(vaultProperties, vaultClient); + case CUBBYHOLE: + return cubbyholeAuthentication(vaultClient); + } throw new UnsupportedOperationException( @@ -189,6 +195,19 @@ public class VaultBootstrapConfiguration { vaultClient.getRestTemplate()); } + private ClientAuthentication cubbyholeAuthentication(VaultClient vaultClient) { + + Assert.hasText(vaultProperties.getToken(), + "Initial Token (spring.cloud.vault.token) for Cubbyhole authentication must not be empty"); + + CubbyholeAuthenticationOptions options = CubbyholeAuthenticationOptions.builder() // + .wrapped() // + .initialToken(VaultToken.of(vaultProperties.getToken())) // + .build(); + + return new CubbyholeAuthentication(options, vaultClient); + } + /** * Creates a {@link ClientFactoryWrapper} containing a * {@link ClientHttpRequestFactory}. {@link ClientHttpRequestFactory} is not exposed 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 a8d8d650..3c3d91de 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 @@ -193,6 +193,6 @@ public class VaultProperties { } public enum AuthenticationMethod { - TOKEN, APPID, AWS_EC2, CERT + TOKEN, APPID, AWS_EC2, CERT, CUBBYHOLE; } } diff --git a/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigCubbyholeAuthenticationTests.java b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigCubbyholeAuthenticationTests.java new file mode 100644 index 00000000..20598ed4 --- /dev/null +++ b/spring-cloud-vault-config/src/test/java/org/springframework/cloud/vault/config/VaultConfigCubbyholeAuthenticationTests.java @@ -0,0 +1,105 @@ +/* + * Copyright 2016 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.*; + +import java.util.Collections; + +import org.junit.AfterClass; +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.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; +import org.springframework.vault.client.VaultResponseEntity; +import org.springframework.vault.core.VaultOperations; +import org.springframework.vault.support.VaultResponse; + +/** + * Integration test using config infrastructure with Cubbyhole authentication. In case this + * test should fail because of SSL make sure you run the test within the + * spring-cloud-vault-config/spring-cloud-vault-config directory as the keystore is + * referenced with {@code ../work/keystore.jks}. + * + * @author Mark Paluch + */ +@RunWith(SpringJUnit4ClassRunner.class) +@SpringBootTest(classes = VaultConfigCubbyholeAuthenticationTests.TestApplication.class, properties = { + "spring.cloud.vault.authentication=cubbyhole", + "spring.application.name=VaultConfigAppIdTests" }) +public class VaultConfigCubbyholeAuthenticationTests { + + @BeforeClass + public static void beforeClass() throws Exception { + + VaultRule vaultRule = new VaultRule(); + vaultRule.before(); + + VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations(); + + vaultOperations.write( + "secret/" + VaultConfigCubbyholeAuthenticationTests.class.getSimpleName(), + Collections.singletonMap("vault.value", "foo")); + + VaultResponseEntity entity = vaultOperations.doWithVault( + new VaultOperations.SessionCallback>() { + @Override + public VaultResponseEntity doWithVault( + VaultOperations.VaultSession session) { + + HttpHeaders headers = new HttpHeaders(); + headers.add("X-Vault-Wrap-TTL", "1h"); + + return session.exchange("auth/token/create", HttpMethod.POST, + new HttpEntity(headers), VaultResponse.class, + null); + } + }); + + String initialToken = entity.getBody().getWrapInfo().get("token"); + System.setProperty("spring.cloud.vault.token", initialToken); + } + + @AfterClass + public static void afterClass() { + System.clearProperty("spring.cloud.vault.token"); + } + + @Value("${vault.value}") + String configValue; + + @Test + public void contextLoads() { + + assertThat(configValue).isEqualTo("foo"); + } + + @SpringBootApplication + public static class TestApplication { + + public static void main(String[] args) { + SpringApplication.run(TestApplication.class, args); + } + } +}