mirror of
https://github.com/jlengrand/quarkus.git
synced 2026-03-10 08:41:22 +00:00
Add Support to Multiple Vault KV Paths - Fixes Issue 5638 #6174
This commit is contained in:
@@ -14,14 +14,18 @@ import static io.quarkus.vault.runtime.config.VaultRuntimeConfig.DEFAULT_TLS_USE
|
||||
import static io.quarkus.vault.runtime.config.VaultRuntimeConfig.KV_SECRET_ENGINE_VERSION_V1;
|
||||
import static java.lang.Boolean.parseBoolean;
|
||||
import static java.lang.Integer.parseInt;
|
||||
import static java.util.Collections.emptyMap;
|
||||
import static java.util.Optional.empty;
|
||||
import static java.util.stream.Collectors.toList;
|
||||
import static java.util.stream.Collectors.toMap;
|
||||
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.AbstractMap.SimpleEntry;
|
||||
import java.util.Collections;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
@@ -48,6 +52,8 @@ public class VaultConfigSource implements ConfigSource {
|
||||
|
||||
private static final String PROPERTY_PREFIX = "quarkus.vault.";
|
||||
public static final Pattern CREDENTIAL_PATTERN = Pattern.compile("^quarkus\\.vault\\.credentials-provider\\.([^.]+)\\.");
|
||||
public static final Pattern SECRET_CONFIG_KV_PATH_PATTERN = Pattern
|
||||
.compile("^quarkus\\.vault\\.secret-config-kv-path\\.([^.]+)$");
|
||||
|
||||
private AtomicReference<VaultCacheEntry<Map<String, String>>> cache = new AtomicReference<>(null);
|
||||
private AtomicReference<VaultRuntimeConfig> serverConfig = new AtomicReference<>(null);
|
||||
@@ -76,7 +82,7 @@ public class VaultConfigSource implements ConfigSource {
|
||||
*/
|
||||
@Override
|
||||
public Map<String, String> getProperties() {
|
||||
return Collections.emptyMap();
|
||||
return emptyMap();
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -102,13 +108,19 @@ public class VaultConfigSource implements ConfigSource {
|
||||
|
||||
Map<String, String> properties = new HashMap<>();
|
||||
|
||||
if (serverConfig.secretConfigKvPath.isPresent()) {
|
||||
try {
|
||||
properties.putAll(fetchSecrets(serverConfig));
|
||||
log.debug("loaded " + properties.size() + " properties from vault");
|
||||
} catch (RuntimeException e) {
|
||||
return tryReturnLastKnownValue(e, cacheEntry);
|
||||
try {
|
||||
// default kv paths
|
||||
if (serverConfig.secretConfigKvPath.isPresent()) {
|
||||
fetchSecrets(serverConfig.secretConfigKvPath.get(), null, properties);
|
||||
}
|
||||
|
||||
// prefixed kv paths
|
||||
serverConfig.secretConfigKvPrefixPath.entrySet()
|
||||
.forEach(entry -> fetchSecrets(entry.getValue(), entry.getKey(), properties));
|
||||
|
||||
log.debug("loaded " + properties.size() + " properties from vault");
|
||||
} catch (RuntimeException e) {
|
||||
return tryReturnLastKnownValue(e, cacheEntry);
|
||||
}
|
||||
|
||||
cache.set(new VaultCacheEntry(properties));
|
||||
@@ -116,11 +128,19 @@ public class VaultConfigSource implements ConfigSource {
|
||||
|
||||
}
|
||||
|
||||
private Map<String, String> fetchSecrets(VaultRuntimeConfig serverConfig) {
|
||||
private void fetchSecrets(List<String> paths, String prefix, Map<String, String> properties) {
|
||||
paths.forEach(path -> properties.putAll(fetchSecrets(path, prefix)));
|
||||
}
|
||||
|
||||
private Map<String, String> fetchSecrets(String path, String prefix) {
|
||||
VaultManager instance = getVaultManager();
|
||||
return instance == null
|
||||
? Collections.emptyMap()
|
||||
: instance.getVaultKvManager().readSecret(serverConfig.secretConfigKvPath.get());
|
||||
return instance == null ? emptyMap() : prefixMap(instance.getVaultKvManager().readSecret(path), prefix);
|
||||
}
|
||||
|
||||
private Map<String, String> prefixMap(Map<String, String> map, String prefix) {
|
||||
return prefix == null
|
||||
? map
|
||||
: map.entrySet().stream().collect(toMap(entry -> prefix + "." + entry.getKey(), Map.Entry::getValue));
|
||||
}
|
||||
|
||||
// ---
|
||||
@@ -176,7 +196,7 @@ public class VaultConfigSource implements ConfigSource {
|
||||
getVaultProperty("kv-secret-engine-version", KV_SECRET_ENGINE_VERSION_V1));
|
||||
serverConfig.kvSecretEngineMountPath = getVaultProperty("kv-secret-engine-mount-path",
|
||||
DEFAULT_KV_SECRET_ENGINE_MOUNT_PATH);
|
||||
serverConfig.secretConfigKvPath = getOptionalVaultProperty("secret-config-kv-path");
|
||||
serverConfig.secretConfigKvPath = getOptionalListProperty("secret-config-kv-path");
|
||||
serverConfig.tls.skipVerify = parseBoolean(getVaultProperty("tls.skip-verify", DEFAULT_TLS_SKIP_VERIFY));
|
||||
serverConfig.tls.useKubernetesCaCert = parseBoolean(
|
||||
getVaultProperty("tls.use-kubernetes-ca-cert", DEFAULT_TLS_USE_KUBERNETES_CACERT));
|
||||
@@ -185,10 +205,25 @@ public class VaultConfigSource implements ConfigSource {
|
||||
serverConfig.readTimeout = getVaultDuration("read-timeout", DEFAULT_READ_TIMEOUT);
|
||||
|
||||
serverConfig.credentialsProvider = getCredentialsProviders();
|
||||
serverConfig.secretConfigKvPrefixPath = getSecretConfigKvPrefixPaths();
|
||||
|
||||
return serverConfig;
|
||||
}
|
||||
|
||||
private Optional<List<String>> getOptionalListProperty(String name) {
|
||||
|
||||
Optional<String> optionalVaultProperty = getOptionalVaultProperty(name);
|
||||
if (!optionalVaultProperty.isPresent()) {
|
||||
return empty();
|
||||
}
|
||||
|
||||
String[] split = optionalVaultProperty.get().split(",");
|
||||
return Optional.of(Arrays.stream(split)
|
||||
.map(String::trim)
|
||||
.filter(s -> !s.isEmpty())
|
||||
.collect(toList()));
|
||||
}
|
||||
|
||||
private Optional<URL> newURL(Optional<String> url) {
|
||||
try {
|
||||
return Optional.ofNullable(url.isPresent() ? new URL(url.get()) : null);
|
||||
@@ -232,6 +267,17 @@ public class VaultConfigSource implements ConfigSource {
|
||||
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
|
||||
}
|
||||
|
||||
private Map<String, List<String>> getSecretConfigKvPrefixPaths() {
|
||||
|
||||
return getConfigSourceStream()
|
||||
.flatMap(configSource -> configSource.getPropertyNames().stream())
|
||||
.map(this::getSecretConfigKvPrefixPathName)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.map(this::createNameSecretConfigKvPrefixPathPair)
|
||||
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
|
||||
}
|
||||
|
||||
private Stream<ConfigSource> getConfigSourceStream() {
|
||||
Config config = ConfigProviderResolver.instance().getConfig();
|
||||
return StreamSupport.stream(config.getConfigSources().spliterator(), false).filter(this::retain);
|
||||
@@ -252,11 +298,20 @@ public class VaultConfigSource implements ConfigSource {
|
||||
return new SimpleEntry<>(name, getCredentialsProviderConfig(name));
|
||||
}
|
||||
|
||||
private SimpleEntry<String, List<String>> createNameSecretConfigKvPrefixPathPair(String name) {
|
||||
return new SimpleEntry<>(name, getSecretConfigKvPrefixPath(name));
|
||||
}
|
||||
|
||||
private String getCredentialsProviderName(String propertyName) {
|
||||
Matcher matcher = CREDENTIAL_PATTERN.matcher(propertyName);
|
||||
return matcher.find() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
private String getSecretConfigKvPrefixPathName(String propertyName) {
|
||||
Matcher matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher(propertyName);
|
||||
return matcher.find() ? matcher.group(1) : null;
|
||||
}
|
||||
|
||||
private CredentialsProviderConfig getCredentialsProviderConfig(String name) {
|
||||
String prefix = "credentials-provider." + name;
|
||||
CredentialsProviderConfig config = new CredentialsProviderConfig();
|
||||
@@ -266,4 +321,8 @@ public class VaultConfigSource implements ConfigSource {
|
||||
return config;
|
||||
}
|
||||
|
||||
private List<String> getSecretConfigKvPrefixPath(String prefixName) {
|
||||
return getOptionalListProperty("secret-config-kv-path." + prefixName).get();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import static io.quarkus.vault.runtime.config.VaultAuthenticationType.USERPASS;
|
||||
|
||||
import java.net.URL;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@@ -82,10 +83,35 @@ public class VaultRuntimeConfig {
|
||||
public Duration secretConfigCachePeriod;
|
||||
|
||||
/**
|
||||
* Vault path in kv store, where all properties will be available as MP config.
|
||||
* List of comma separated vault paths in kv store,
|
||||
* where all properties will be available as MP config properties as-is, with no prefix.
|
||||
* For instance, if vault contains property {@code foo}, it will be made available to the
|
||||
* quarkus application as
|
||||
* {@code @ConfigProperty(name = "foo") String foo;}
|
||||
* <p>
|
||||
* If 2 paths contain the same property, the last path will win. For instance if
|
||||
* {@code secret/base-config} contains {@code foo='bar'} and
|
||||
* {@code secret/myapp/config} contains {@code foo='myappbar'}, then
|
||||
* {@code @ConfigProperty(name = "foo") String foo;} will have for value {@code "bar"}
|
||||
* with application properties
|
||||
* {@code quarkus.vault.secret-config-kv-path=base-config,myapp/config}
|
||||
*/
|
||||
@ConfigItem
|
||||
public Optional<String> secretConfigKvPath;
|
||||
public Optional<List<String>> secretConfigKvPath;
|
||||
|
||||
/**
|
||||
* List of comma separated vault paths in kv store,
|
||||
* where all properties will be available as prefixed MP config properties.
|
||||
* For instance if the application properties contains
|
||||
* {@code quarkus.vault.secret-config-kv-path-prefix.myprefix=config}, all properties located
|
||||
* in vault path {@code secret/config} contains {@code foo='bar'}, then {@code myprefix.foo}
|
||||
* will be available in the MP config.
|
||||
* <p>
|
||||
* If the same property is available in 2 different paths for the same prefix, the last one
|
||||
* will win.
|
||||
*/
|
||||
@ConfigItem(name = "secret-config-kv-path")
|
||||
public Map<String, List<String>> secretConfigKvPrefixPath;
|
||||
|
||||
/**
|
||||
* Used to hide confidential infos, for logging in particular.
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
package io.quarkus.vault;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import org.eclipse.microprofile.config.Config;
|
||||
import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
|
||||
import org.jboss.shrinkwrap.api.ShrinkWrap;
|
||||
import org.jboss.shrinkwrap.api.spec.JavaArchive;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledOnOs;
|
||||
import org.junit.jupiter.api.condition.OS;
|
||||
import org.junit.jupiter.api.extension.RegisterExtension;
|
||||
|
||||
import io.quarkus.test.QuarkusUnitTest;
|
||||
import io.quarkus.test.common.QuarkusTestResource;
|
||||
import io.quarkus.vault.test.VaultTestLifecycleManager;
|
||||
|
||||
@DisabledOnOs(OS.WINDOWS)
|
||||
@QuarkusTestResource(VaultTestLifecycleManager.class)
|
||||
public class VaultMultiPathConfigITCase {
|
||||
@RegisterExtension
|
||||
static final QuarkusUnitTest config = new QuarkusUnitTest()
|
||||
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
|
||||
.addAsResource("application-vault-multi-path.properties", "application.properties"));
|
||||
|
||||
@Test
|
||||
public void defaultPath() {
|
||||
Config config = ConfigProviderResolver.instance().getConfig();
|
||||
assertEquals("red", config.getValue("color", String.class));
|
||||
assertEquals("XL", config.getValue("size", String.class));
|
||||
assertEquals("3", config.getValue("weight", String.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void prefixPath() {
|
||||
Config config = ConfigProviderResolver.instance().getConfig();
|
||||
assertEquals("green", config.getValue("singer.color", String.class));
|
||||
assertEquals("paul", config.getValue("singer.firstname", String.class));
|
||||
assertEquals("simon", config.getValue("singer.lastname", String.class));
|
||||
assertEquals("78", config.getValue("singer.age", String.class));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
quarkus.vault.url=https://localhost:8200
|
||||
quarkus.vault.authentication.userpass.username=bob
|
||||
quarkus.vault.authentication.userpass.password=sinclair
|
||||
quarkus.vault.secret-config-kv-path=multi/default1,multi/default2
|
||||
quarkus.vault.secret-config-kv-path.singer=multi/singer1,multi/singer2
|
||||
|
||||
quarkus.vault.tls.skip-verify=true
|
||||
@@ -204,6 +204,12 @@ public class VaultTestExtension {
|
||||
execVault(format("vault kv put %s/%s %s=%s", SECRET_PATH_V1, APP_SECRET_PATH, SECRET_KEY, SECRET_VALUE));
|
||||
execVault(format("vault kv put %s/%s %s=%s", SECRET_PATH_V1, APP_CONFIG_PATH, PASSWORD_PROPERTY_NAME, DB_PASSWORD));
|
||||
|
||||
// multi config
|
||||
execVault(format("vault kv put %s/multi/default1 color=blue size=XL", SECRET_PATH_V1));
|
||||
execVault(format("vault kv put %s/multi/default2 color=red weight=3", SECRET_PATH_V1));
|
||||
execVault(format("vault kv put %s/multi/singer1 firstname=paul lastname=shaffer", SECRET_PATH_V1));
|
||||
execVault(format("vault kv put %s/multi/singer2 lastname=simon age=78 color=green", SECRET_PATH_V1));
|
||||
|
||||
// static secrets kv v2
|
||||
execVault(format("vault secrets enable -path=%s -version=2 kv", SECRET_PATH_V2));
|
||||
execVault(format("vault kv put %s/%s %s=%s", SECRET_PATH_V2, APP_SECRET_PATH, SECRET_KEY, SECRET_VALUE));
|
||||
|
||||
@@ -8,6 +8,11 @@ path "secret/config" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# vault config source kv engine v1 with multi paths
|
||||
path "secret/multi/*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
|
||||
# kv engine v2
|
||||
path "secret-v2/data/foo" {
|
||||
capabilities = ["read"]
|
||||
|
||||
Reference in New Issue
Block a user