Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/modules/ROOT/pages/authentication.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -399,6 +399,7 @@ spring.cloud.vault:
key-store-password: changeit
key-store-type: JKS
cert-auth-path: cert
role: my-dev-role
----

See also: https://www.vaultproject.io/docs/auth/cert.html[Vault Documentation: Using the Cert auth backend]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -378,11 +378,14 @@ private ClientAuthentication pcfAuthentication(VaultProperties vaultProperties)

private ClientAuthentication certificateAuthentication(VaultProperties vaultProperties) {

ClientCertificateAuthenticationOptions options = ClientCertificateAuthenticationOptions.builder()
.path(vaultProperties.getSsl().getCertAuthPath())
.build();
ClientCertificateAuthenticationOptions.ClientCertificateAuthenticationOptionsBuilder optionsBuilder = ClientCertificateAuthenticationOptions
.builder();
optionsBuilder.path(vaultProperties.getSsl().getCertAuthPath());
if (StringUtils.hasText(vaultProperties.getSsl().getRole())) {
optionsBuilder.role(vaultProperties.getSsl().getRole());
}

return new ClientCertificateAuthentication(options, this.restOperations);
return new ClientCertificateAuthentication(optionsBuilder.build(), this.restOperations);
}

private ClientAuthentication tokenAuthentication(VaultProperties vaultProperties) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
* @author Michal Budzyn
* @author Grenville Wilson
* @author Mårten Svantesson
* @author Issam El-atif
*/
@ConfigurationProperties(VaultProperties.PREFIX)
public class VaultProperties implements EnvironmentAware {
Expand Down Expand Up @@ -1152,6 +1153,13 @@ public static class Ssl {
*/
private String certAuthPath = "cert";

/**
* Name of the role against which the login is being attempted.
*
* @since 5.0
*/
private String role = "";

/**
* List of enabled SSL/TLS protocol.
* @since 3.0.2
Expand Down Expand Up @@ -1226,6 +1234,14 @@ public void setCertAuthPath(String certAuthPath) {
this.certAuthPath = certAuthPath;
}

public String getRole() {
return role;
}

public void setRole(String role) {
this.role = role;
}

public List<String> getEnabledProtocols() {
return this.enabledProtocols;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,22 @@
import java.util.Map;

import org.assertj.core.util.Files;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;

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.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.test.system.CapturedOutput;
import org.springframework.boot.test.system.OutputCaptureExtension;
import org.springframework.cloud.vault.util.Settings;
import org.springframework.cloud.vault.util.VaultRule;
import org.springframework.cloud.vault.util.VaultTestContextRunner;
import org.springframework.vault.core.VaultOperations;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.springframework.cloud.vault.config.VaultProperties.AuthenticationMethod.CERT;
import static org.springframework.cloud.vault.util.Settings.findWorkDir;

/**
Expand All @@ -44,18 +48,21 @@
* referenced with {@code ../work/keystore.jks}.
*
* @author Mark Paluch
* @author Issam El-atif
*/

@SpringBootTest(classes = VaultConfigTlsCertAuthenticationTests.TestApplication.class,
properties = { "spring.cloud.vault.authentication=cert",
"spring.cloud.vault.ssl.key-store=file:../work/client-cert.jks",
"spring.cloud.vault.ssl.key-store-password=changeit",
"spring.cloud.vault.application-name=VaultConfigTlsCertAuthenticationTests",
"spring.cloud.vault.reactive.enabled=false", "spring.cloud.bootstrap.enabled=true" })
@ExtendWith(OutputCaptureExtension.class)
public class VaultConfigTlsCertAuthenticationTests {

@Value("${vault.value}")
String configValue;
VaultTestContextRunner contextRunner = VaultTestContextRunner.of(CERT)
.testClass(getClass())
.bootstrap(true)
.reactive(false)
.configurations(TestConfig.class);

private static VaultOperations vaultOperations;

private static final String certificate = Files.contentOf(new File(findWorkDir(), "ca/certs/client.cert.pem"),
StandardCharsets.US_ASCII);

@BeforeAll
public static void beforeClass() {
Expand All @@ -69,7 +76,7 @@ public static void beforeClass() {
vaultRule.prepare().mountAuth(vaultProperties.getSsl().getCertAuthPath());
}

VaultOperations vaultOperations = vaultRule.prepare().getVaultOperations();
vaultOperations = vaultRule.prepare().getVaultOperations();

String rules = "{ \"name\": \"testpolicy\",\n" //
+ " \"path\": {\n" //
Expand All @@ -82,28 +89,49 @@ public static void beforeClass() {
vaultOperations.write("secret/" + VaultConfigTlsCertAuthenticationTests.class.getSimpleName(),
Collections.singletonMap("vault.value", "foo"));

File workDir = findWorkDir();

String certificate = Files.contentOf(new File(workDir, "ca/certs/client.cert.pem"), StandardCharsets.US_ASCII);

Map<String, String> role = new HashMap<>();
role.put("certificate", certificate);
role.put("policies", "testpolicy");

vaultOperations.write("auth/cert/certs/my-role", role);
}

@AfterEach
void cleanup() {
vaultOperations.delete("auth/cert/certs/another-role");
}

@Test
public void contextLoads() {
assertThat(this.configValue).isEqualTo("foo");
void authenticateUsingTlsCertificate() {
this.contextRunner
.run(context -> assertThat(context.getEnvironment().getProperty("vault.value")).isEqualTo("foo"));
}

@SpringBootApplication
public static class TestApplication {
@Test
void authenticateUsingNamedCertificateRole() {
Map<String, String> anotherRole = new HashMap<>();
anotherRole.put("certificate", certificate);
vaultOperations.write("auth/cert/certs/another-role", anotherRole);

public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
this.contextRunner.property("spring.cloud.vault.ssl.role", "my-role")
.run(context -> assertThat(context.getEnvironment().getProperty("vault.value")).isEqualTo("foo"));
}

@Test
void authenticationFailsWhenMultipleCertsAndNoNamedRole(CapturedOutput capturedOutput) {
Map<String, String> anotherRole = new HashMap<>();
anotherRole.put("certificate", certificate);
vaultOperations.write("auth/cert/certs/another-role", anotherRole);

this.contextRunner.run(context -> {
assertThat(capturedOutput.getOut())
.contains("org.springframework.vault.VaultException: Status 403 Forbidden");
assertNull(context.getEnvironment().getProperty("vault.value"));
});
}

@TestConfiguration
public static class TestConfig {

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright 2025-present 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.cloud.vault.util;

import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.cloud.vault.config.VaultProperties.AuthenticationMethod;
import org.springframework.context.ConfigurableApplicationContext;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Consumer;

/**
* A utility class built on top of {@link SpringApplicationBuilder} that follows Spring
* Boot’s testing patterns but designed for Spring Cloud Vault integration tests.
*
* <p>
* Its goal is to eliminate repetitive boilerplate particularly when repeatedly
* configuring:
* </p>
*
* <ul>
* <li>Bootstrap mode</li>
* <li>Reactive vs. non-reactive modes</li>
* <li>Environment and test classes</li>
* <li>Vault-specific properties</li>
* </ul>
*
* @author Issam EL-ATIF
* @since 5.0.1
* @see SpringApplicationBuilder
* @see ApplicationContextRunner
*/
public class VaultTestContextRunner {

private final List<Class<?>> configurationClasses = new ArrayList<>();

private final Map<String, Object> properties = new LinkedHashMap<>();

private VaultTestContextRunner(Map<String, Object> properties) {
this.properties.putAll(properties);
}

public static VaultTestContextRunner of(AuthenticationMethod authenticationMethod) {
Map<String, Object> authProperties = new LinkedHashMap<>();
switch (authenticationMethod) {
case CERT -> {
authProperties.put("spring.cloud.vault.authentication", "cert");
authProperties.put("spring.cloud.vault.ssl.key-store", "file:../work/client-cert.jks");
authProperties.put("spring.cloud.vault.ssl.key-store-password", "changeit");
}
case KUBERNETES -> {
authProperties.put("spring.cloud.vault.authentication", "kubernetes");
authProperties.put("spring.cloud.vault.kubernetes.service-account-token-file",
"../work/minikube/hello-minikube-token");
}
}

return new VaultTestContextRunner(authProperties);
}

public VaultTestContextRunner configurations(Class<?>... configClasses) {
this.configurationClasses.addAll(Arrays.asList(configClasses));
return this;
}

public VaultTestContextRunner property(String key, Object value) {
this.properties.put(key, value);
return this;
}

public VaultTestContextRunner testClass(Class<?> testClass) {
this.properties.put("spring.cloud.vault.application-name", testClass.getSimpleName());
return this;
}

public VaultTestContextRunner bootstrap(boolean bootstrap) {
this.properties.put("spring.cloud.bootstrap.enabled", bootstrap);
return this;
}

public VaultTestContextRunner reactive(boolean reactive) {
this.properties.put("spring.cloud.vault.reactive.enabled", reactive);
return this;
}

public void run(Consumer<ConfigurableApplicationContext> consumer) {
SpringApplicationBuilder builder = new SpringApplicationBuilder();
builder.sources(configurationClasses.toArray(new Class<?>[0]))
.web(WebApplicationType.NONE)
.properties(this.properties);

try (ConfigurableApplicationContext context = builder.run()) {
consumer.accept(context);
}
}

}
Loading