From da2c4aded2a9f4a29613ffdd8e9ef65bcd06a950 Mon Sep 17 00:00:00 2001 From: Mark Paluch Date: Fri, 10 Jan 2020 13:10:48 +0100 Subject: [PATCH] Support for Vault Agent We now support Vault Agent usage by creating VaultTemplate and ReactiveVaultTemplate without a client authentication mechanism/session manager to inherit Vault Agent's authentication. Closes gh-515 --- .../vault/core/ReactiveVaultTemplate.java | 71 +++++++++++++++ .../vault/core/VaultTemplate.java | 89 ++++++++++++++++++- ...cateAuthenticationIntegrationTestBase.java | 17 +++- ...iveVaultTemplateAgentIntegrationTests.java | 83 +++++++++++++++++ .../VaultTemplateAgentIntegrationTests.java | 79 ++++++++++++++++ src/test/bash/vault-agent.conf | 25 ++++++ 6 files changed, 361 insertions(+), 3 deletions(-) create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateAgentIntegrationTests.java create mode 100644 spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateAgentIntegrationTests.java create mode 100644 src/test/bash/vault-agent.conf diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java index 19134622..7730d97d 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/ReactiveVaultTemplate.java @@ -40,6 +40,7 @@ import org.springframework.vault.client.VaultResponses; import org.springframework.vault.client.WebClientBuilder; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; +import org.springframework.vault.support.VaultToken; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.reactive.function.BodyExtractors; import org.springframework.web.reactive.function.client.ClientRequest; @@ -68,6 +69,22 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { private final VaultTokenSupplier vaultTokenSupplier; + /** + * Create a new {@link ReactiveVaultTemplate} with a {@link VaultEndpoint}, + * {@link ClientHttpConnector}. This constructor does not use a + * {@link VaultTokenSupplier}. It is intended for usage with Vault Agent to inherit + * Vault Agent's authentication without using the {@link VaultHttpHeaders#VAULT_TOKEN + * authentication token header}. + * + * @param vaultEndpoint must not be {@literal null}. + * @param connector must not be {@literal null}. + * @since 2.2.1 + */ + public ReactiveVaultTemplate(VaultEndpoint vaultEndpoint, + ClientHttpConnector connector) { + this(SimpleVaultEndpointProvider.of(vaultEndpoint), connector); + } + /** * Create a new {@link ReactiveVaultTemplate} with a {@link VaultEndpoint}, * {@link ClientHttpConnector} and {@link VaultTokenSupplier}. @@ -82,6 +99,30 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { vaultTokenSupplier); } + /** + * Create a new {@link ReactiveVaultTemplate} with a {@link VaultEndpointProvider} and + * {@link ClientHttpConnector}. This constructor does not use a + * {@link VaultTokenSupplier}. It is intended for usage with Vault Agent to inherit + * Vault Agent's authentication without using the {@link VaultHttpHeaders#VAULT_TOKEN + * authentication token header}. + * + * @param endpointProvider must not be {@literal null}. + * @param connector must not be {@literal null}. + * @since 2.2.1 + */ + public ReactiveVaultTemplate(VaultEndpointProvider endpointProvider, + ClientHttpConnector connector) { + + Assert.notNull(endpointProvider, "VaultEndpointProvider must not be null"); + Assert.notNull(connector, "ClientHttpConnector must not be null"); + + WebClient webClient = doCreateWebClient(endpointProvider, connector); + + this.vaultTokenSupplier = NoTokenSupplier.INSTANCE; + this.statelessClient = webClient; + this.sessionClient = webClient; + } + /** * Create a new {@link ReactiveVaultTemplate} with a {@link VaultEndpointProvider}, * {@link ClientHttpConnector} and {@link VaultTokenSupplier}. @@ -102,6 +143,26 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { this.sessionClient = doCreateSessionWebClient(endpointProvider, connector); } + /** + * Create a new {@link ReactiveVaultTemplate} through a {@link WebClientBuilder}. This + * constructor does not use a {@link VaultTokenSupplier}. It is intended for usage + * with Vault Agent to inherit Vault Agent's authentication without using the + * {@link VaultHttpHeaders#VAULT_TOKEN authentication token header}. + * + * @param webClientBuilder must not be {@literal null}. + * @since 2.2.1 + */ + public ReactiveVaultTemplate(WebClientBuilder webClientBuilder) { + + Assert.notNull(webClientBuilder, "WebClientBuilder must not be null"); + + WebClient webClient = webClientBuilder.build(); + + this.vaultTokenSupplier = NoTokenSupplier.INSTANCE; + this.statelessClient = webClient; + this.sessionClient = webClient; + } + /** * Create a new {@link ReactiveVaultTemplate} through a {@link WebClientBuilder}, and * {@link VaultTokenSupplier}. @@ -321,4 +382,14 @@ public class ReactiveVaultTemplate implements ReactiveVaultOperations { private static class VaultListResponse extends VaultResponseSupport> { } + + private enum NoTokenSupplier implements VaultTokenSupplier { + INSTANCE; + + @Override + public Mono getVaultToken() { + return Mono + .error(new UnsupportedOperationException("Token retrieval disabled")); + } + } } diff --git a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java index 898d4e90..88dcb034 100644 --- a/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java +++ b/spring-vault-core/src/main/java/org/springframework/vault/core/VaultTemplate.java @@ -41,6 +41,7 @@ import org.springframework.vault.client.VaultResponses; import org.springframework.vault.core.VaultKeyValueOperationsSupport.KeyValueBackend; import org.springframework.vault.support.VaultResponse; import org.springframework.vault.support.VaultResponseSupport; +import org.springframework.vault.support.VaultToken; import org.springframework.web.client.HttpStatusCodeException; import org.springframework.web.client.RestTemplate; @@ -62,6 +63,20 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa private final boolean dedicatedSessionManager; + /** + * Create a new {@link VaultTemplate} with a {@link VaultEndpoint}. This constructor + * does not use a {@link ClientAuthentication} mechanism. It is intended for usage + * with Vault Agent to inherit Vault Agent's authentication without using the + * {@link VaultHttpHeaders#VAULT_TOKEN authentication token header}. + * + * @param vaultEndpoint must not be {@literal null}. + * @since 2.2.1 + */ + public VaultTemplate(VaultEndpoint vaultEndpoint) { + this(SimpleVaultEndpointProvider.of(vaultEndpoint), + new SimpleClientHttpRequestFactory()); + } + /** * Create a new {@link VaultTemplate} with a {@link VaultEndpoint} and * {@link ClientAuthentication}. @@ -87,6 +102,22 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa this.sessionTemplate = doCreateSessionTemplate(endpointProvider, requestFactory); } + /** + * Create a new {@link VaultTemplate} with a {@link VaultEndpoint}, and + * {@link ClientHttpRequestFactory}. This constructor does not use a + * {@link ClientAuthentication} mechanism. It is intended for usage with Vault Agent + * to inherit Vault Agent's authentication without using the + * {@link VaultHttpHeaders#VAULT_TOKEN authentication token header}. + * + * @param vaultEndpoint must not be {@literal null}. + * @param clientHttpRequestFactory must not be {@literal null}. + * @since 2.2.1 + */ + public VaultTemplate(VaultEndpoint vaultEndpoint, + ClientHttpRequestFactory clientHttpRequestFactory) { + this(SimpleVaultEndpointProvider.of(vaultEndpoint), clientHttpRequestFactory); + } + /** * Create a new {@link VaultTemplate} with a {@link VaultEndpoint}, * {@link ClientHttpRequestFactory} and {@link SessionManager}. @@ -102,6 +133,32 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa sessionManager); } + /** + * Create a new {@link VaultTemplate} with a {@link VaultEndpointProvider}, + * {@link ClientHttpRequestFactory} and {@link SessionManager}. This constructor does + * not use a {@link ClientAuthentication} mechanism. It is intended for usage with + * Vault Agent to inherit Vault Agent's authentication without using the + * {@link VaultHttpHeaders#VAULT_TOKEN authentication token header}. + * + * @param endpointProvider must not be {@literal null}. + * @param requestFactory must not be {@literal null}. + * @since 2.2.1 + */ + public VaultTemplate(VaultEndpointProvider endpointProvider, + ClientHttpRequestFactory requestFactory) { + + Assert.notNull(endpointProvider, "VaultEndpointProvider must not be null"); + Assert.notNull(requestFactory, "ClientHttpRequestFactory must not be null"); + + RestTemplate restTemplate = doCreateRestTemplate(endpointProvider, + requestFactory); + + this.sessionManager = NoSessionManager.INSTANCE; + this.dedicatedSessionManager = false; + this.statelessTemplate = restTemplate; + this.sessionTemplate = restTemplate; + } + /** * Create a new {@link VaultTemplate} with a {@link VaultEndpointProvider}, * {@link ClientHttpRequestFactory} and {@link SessionManager}. @@ -120,11 +177,32 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa this.sessionManager = sessionManager; this.dedicatedSessionManager = false; - this.statelessTemplate = doCreateRestTemplate(endpointProvider, requestFactory); this.sessionTemplate = doCreateSessionTemplate(endpointProvider, requestFactory); } + /** + * Create a new {@link VaultTemplate} through a {@link RestTemplateBuilder} and + * {@link SessionManager}. This constructor does not use a + * {@link ClientAuthentication} mechanism. It is intended for usage with Vault Agent + * to inherit Vault Agent's authentication without using the + * {@link VaultHttpHeaders#VAULT_TOKEN authentication token header}. + * + * @param restTemplateBuilder must not be {@literal null}. + * @since 2.2.1 + */ + public VaultTemplate(RestTemplateBuilder restTemplateBuilder) { + + Assert.notNull(restTemplateBuilder, "RestTemplateBuilder must not be null"); + + RestTemplate restTemplate = restTemplateBuilder.build(); + + this.sessionManager = NoSessionManager.INSTANCE; + this.dedicatedSessionManager = false; + this.statelessTemplate = restTemplate; + this.sessionTemplate = restTemplate; + } + /** * Create a new {@link VaultTemplate} through a {@link RestTemplateBuilder} and * {@link SessionManager}. @@ -410,4 +488,13 @@ public class VaultTemplate implements InitializingBean, VaultOperations, Disposa } }); } + + private enum NoSessionManager implements SessionManager { + INSTANCE; + + @Override + public VaultToken getSessionToken() { + throw new UnsupportedOperationException(); + } + } } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java index f7f02a31..b4caa13c 100644 --- a/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java +++ b/spring-vault-core/src/test/java/org/springframework/vault/authentication/ClientCertificateAuthenticationIntegrationTestBase.java @@ -17,7 +17,7 @@ package org.springframework.vault.authentication; import java.io.File; import java.nio.charset.StandardCharsets; -import java.util.Collections; +import java.util.LinkedHashMap; import java.util.Map; import org.assertj.core.util.Files; @@ -25,6 +25,7 @@ import org.junit.jupiter.api.BeforeEach; import org.springframework.core.io.FileSystemResource; import org.springframework.vault.core.RestOperationsCallback; +import org.springframework.vault.support.Policy; import org.springframework.vault.support.SslConfiguration; import org.springframework.vault.support.SslConfiguration.KeyStoreConfiguration; import org.springframework.vault.util.IntegrationTestSupport; @@ -40,6 +41,11 @@ import static org.springframework.vault.util.Settings.findWorkDir; public abstract class ClientCertificateAuthenticationIntegrationTestBase extends IntegrationTestSupport { + static final Policy POLICY = Policy.of(Policy.Rule.builder().path("/*") + .capabilities(Policy.BuiltinCapabilities.READ, + Policy.BuiltinCapabilities.CREATE, Policy.BuiltinCapabilities.UPDATE) + .build()); + @BeforeEach public void before() { @@ -47,6 +53,9 @@ public abstract class ClientCertificateAuthenticationIntegrationTestBase prepare().mountAuth("cert"); } + prepare().getVaultOperations().opsForSys().createOrUpdatePolicy("cert-auth", + POLICY); + prepare().getVaultOperations() .doWithSession((RestOperationsCallback) restOperations -> { File workDir = findWorkDir(); @@ -55,8 +64,12 @@ public abstract class ClientCertificateAuthenticationIntegrationTestBase new File(workDir, "ca/certs/client.cert.pem"), StandardCharsets.US_ASCII); + Map role = new LinkedHashMap<>(); + role.put("token_policies", "cert-auth"); + role.put("certificate", certificate); + return restOperations.postForEntity("auth/cert/certs/my-role", - Collections.singletonMap("certificate", certificate), + role, Map.class); }); } diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateAgentIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateAgentIntegrationTests.java new file mode 100644 index 00000000..8501cb8a --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/ReactiveVaultTemplateAgentIntegrationTests.java @@ -0,0 +1,83 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.vault.core; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; +import reactor.test.StepVerifier; + +import org.springframework.http.client.reactive.ClientHttpConnector; +import org.springframework.vault.client.ClientHttpConnectorFactory; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.client.WebClientBuilder; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.Settings; + +/** + * Integration tests for {@link ReactiveVaultTemplate} through Vault Agent. + * + * @author Mark Paluch + */ +class ReactiveVaultTemplateAgentIntegrationTests extends IntegrationTestSupport { + + ClientHttpConnector connector = ClientHttpConnectorFactory.create(new ClientOptions(), + Settings.createSslConfiguration()); + + VaultEndpoint endpoint = VaultEndpoint.create("localhost", 8202); + + @BeforeEach + void setUp() { + + try (Socket socket = new Socket()) { + + socket.connect(new InetSocketAddress(endpoint.getHost(), endpoint.getPort()), + (int) new ClientOptions().getConnectionTimeout().toMillis()); + } + catch (IOException e) { + throw new TestAbortedException( + "Vault Agent not available: " + e.getMessage()); + } + } + + @Test + void shouldUseAgentAuthentication() { + + ReactiveVaultTemplate vaultTemplate = new ReactiveVaultTemplate(endpoint, + connector); + + vaultTemplate.write("secret/foo", Collections.singletonMap("key", "value")) + .as(StepVerifier::create).verifyComplete(); + } + + @Test + void shouldUseAgentAuthenticationWithBuilder() { + + WebClientBuilder builder = WebClientBuilder.builder().endpoint(endpoint) + .httpConnector(connector); + + ReactiveVaultTemplate vaultTemplate = new ReactiveVaultTemplate(builder); + + vaultTemplate.write("secret/foo", Collections.singletonMap("key", "value")) + .as(StepVerifier::create).verifyComplete(); + } +} diff --git a/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateAgentIntegrationTests.java b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateAgentIntegrationTests.java new file mode 100644 index 00000000..33e0ba59 --- /dev/null +++ b/spring-vault-core/src/test/java/org/springframework/vault/core/VaultTemplateAgentIntegrationTests.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016-2019 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 + * + * https://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.vault.core; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.Socket; +import java.util.Collections; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.opentest4j.TestAbortedException; + +import org.springframework.http.client.ClientHttpRequestFactory; +import org.springframework.vault.client.ClientHttpRequestFactoryFactory; +import org.springframework.vault.client.RestTemplateBuilder; +import org.springframework.vault.client.VaultEndpoint; +import org.springframework.vault.support.ClientOptions; +import org.springframework.vault.util.IntegrationTestSupport; +import org.springframework.vault.util.Settings; + +/** + * Integration tests for {@link VaultTemplate} through Vault Agent. + * + * @author Mark Paluch + */ +class VaultTemplateAgentIntegrationTests extends IntegrationTestSupport { + + ClientHttpRequestFactory requestFactory = ClientHttpRequestFactoryFactory + .create(new ClientOptions(), Settings.createSslConfiguration()); + + VaultEndpoint endpoint = VaultEndpoint.create("localhost", 8202); + + @BeforeEach + void setUp() { + + try (Socket socket = new Socket()) { + + socket.connect(new InetSocketAddress(endpoint.getHost(), endpoint.getPort()), + (int) new ClientOptions().getConnectionTimeout().toMillis()); + } + catch (IOException e) { + throw new TestAbortedException( + "Vault Agent not available: " + e.getMessage()); + } + } + + @Test + void shouldUseAgentAuthentication() { + + VaultTemplate vaultTemplate = new VaultTemplate(endpoint, requestFactory); + + vaultTemplate.write("secret/foo", Collections.singletonMap("key", "value")); + } + + @Test + void shouldUseAgentAuthenticationWithBuilder() { + + RestTemplateBuilder builder = RestTemplateBuilder.builder().endpoint(endpoint) + .requestFactory(requestFactory); + + VaultTemplate vaultTemplate = new VaultTemplate(builder); + + vaultTemplate.write("secret/foo", Collections.singletonMap("key", "value")); + } +} diff --git a/src/test/bash/vault-agent.conf b/src/test/bash/vault-agent.conf new file mode 100644 index 00000000..94647b16 --- /dev/null +++ b/src/test/bash/vault-agent.conf @@ -0,0 +1,25 @@ +exit_after_auth = false +pid_file = "./pidfile" + +vault { + address = "https://127.0.0.1:8200" + ca_cert = "work/ca/certs/ca.cert.pem" + client_cert = "work/ca/certs/client.cert.pem" + client_key = "work/ca/private/client.decrypted.key.pem" +} + +auto_auth { + method "cert" { + mount_path = "auth/cert" + } +} + +cache { + use_auto_auth_token = true +} + +listener "tcp" { + address = "0.0.0.0:8202" + tls_cert_file = "work/ca/certs/localhost.cert.pem" + tls_key_file = "work/ca/private/localhost.decrypted.key.pem" +}