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
This commit is contained in:
Mark Paluch
2020-01-10 13:10:48 +01:00
parent ae43232b31
commit da2c4aded2
6 changed files with 361 additions and 3 deletions

View File

@@ -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<Map<String, Object>> {
}
private enum NoTokenSupplier implements VaultTokenSupplier {
INSTANCE;
@Override
public Mono<VaultToken> getVaultToken() {
return Mono
.error(new UnsupportedOperationException("Token retrieval disabled"));
}
}
}

View File

@@ -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();
}
}
}

View File

@@ -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<Object>) restOperations -> {
File workDir = findWorkDir();
@@ -55,8 +64,12 @@ public abstract class ClientCertificateAuthenticationIntegrationTestBase
new File(workDir, "ca/certs/client.cert.pem"),
StandardCharsets.US_ASCII);
Map<String, Object> 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);
});
}

View File

@@ -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();
}
}

View File

@@ -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"));
}
}

View File

@@ -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"
}