Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix secret-config-kv-prefix-path property #13300

Merged
merged 1 commit into from
Nov 26, 2020
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import static java.lang.Integer.parseInt;
import static java.util.Collections.emptyMap;
import static java.util.Optional.empty;
import static java.util.regex.Pattern.compile;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toMap;

Expand Down Expand Up @@ -52,12 +53,13 @@ public class VaultConfigSource implements ConfigSource {

private static final Logger log = Logger.getLogger(VaultConfigSource.class);

private static final String PROPERTY_PREFIX = "quarkus.vault.";
public static final Pattern CREDENTIALS_PATTERN = Pattern.compile("^quarkus\\.vault\\.credentials-provider\\.([^.]+)\\.");
public static final Pattern TRANSIT_KEY_PATTERN = Pattern.compile("^quarkus\\.vault\\.transit.key\\.([^.]+)\\.");
public static final Pattern SECRET_CONFIG_KV_PATH_PATTERN = Pattern
.compile("^quarkus\\.vault\\.secret-config-kv-path\\.(?:([^.]+)|(?:\"([^\"]+)\"))$");
public static final Pattern EXPANSION_PATTERN = Pattern.compile("\\$\\{([^}]+)\\}");
static final String PROPERTY_PREFIX = "quarkus.vault.";
public static final Pattern CREDENTIALS_PATTERN = compile("^quarkus\\.vault\\.credentials-provider\\.([^.]+)\\.");
public static final Pattern TRANSIT_KEY_PATTERN = compile("^quarkus\\.vault\\.transit.key\\.([^.]+)\\.");
public static final String SECRET_CONFIG_KV_PREFIX_PATHS = "secret-config-kv-path";
public static final Pattern SECRET_CONFIG_KV_PREFIX_PATH_PATTERN = compile(
"^quarkus\\.vault\\." + SECRET_CONFIG_KV_PREFIX_PATHS + "\\.(?:([^.]+)|(?:\"([^\"]+)\"))$");
public static final Pattern EXPANSION_PATTERN = compile("\\$\\{([^}]+)\\}");

private AtomicReference<VaultCacheEntry<Map<String, String>>> cache = new AtomicReference<>(null);
private AtomicReference<VaultRuntimeConfig> serverConfig = new AtomicReference<>(null);
Expand Down Expand Up @@ -117,13 +119,10 @@ private Map<String, String> getSecretConfig() {

try {
// default kv paths
if (serverConfig.secretConfigKvPath.isPresent()) {
fetchSecrets(serverConfig.secretConfigKvPath.get(), null, properties);
}
serverConfig.secretConfigKvPath.ifPresent(strings -> fetchSecrets(strings, null, properties));

// prefixed kv paths
serverConfig.secretConfigKvPrefixPath.entrySet()
.forEach(entry -> fetchSecrets(entry.getValue(), entry.getKey(), properties));
serverConfig.secretConfigKvPathPrefix.forEach((key, value) -> fetchSecrets(value.paths, key, properties));

log.debug("loaded " + properties.size() + " properties from vault");
} catch (RuntimeException e) {
Expand Down Expand Up @@ -252,7 +251,7 @@ private VaultRuntimeConfig loadRuntimeConfig() {

serverConfig.credentialsProvider = createCredentialProviderConfigParser().getConfig();
serverConfig.transit.key = createTransitKeyConfigParser().getConfig();
serverConfig.secretConfigKvPrefixPath = getSecretConfigKvPrefixPaths();
serverConfig.secretConfigKvPathPrefix = getSecretConfigKvPrefixPaths();

return serverConfig;
}
Expand Down Expand Up @@ -362,15 +361,16 @@ protected String getBaseProperty(String propertyName, String defaultValue) {
.orElse(defaultValue);
}

private Map<String, List<String>> getSecretConfigKvPrefixPaths() {
private Map<String, VaultRuntimeConfig.KvPathConfig> getSecretConfigKvPrefixPaths() {

return getConfigSourceStream()
.flatMap(configSource -> configSource.getPropertyNames().stream())
.map(this::getSecretConfigKvPrefixPathName)
.filter(Objects::nonNull)
.distinct()
.map(this::createNameSecretConfigKvPrefixPathPair)
.collect(toMap(SimpleEntry::getKey, SimpleEntry::getValue));
.collect(toMap(SimpleEntry::getKey,
kvStore -> new VaultRuntimeConfig.KvPathConfig(kvStore.getValue())));
}

private Stream<ConfigSource> getConfigSourceStream() {
Expand All @@ -394,12 +394,12 @@ private SimpleEntry<String, List<String>> createNameSecretConfigKvPrefixPathPair
}

private String getSecretConfigKvPrefixPathName(String propertyName) {
Matcher matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher(propertyName);
Matcher matcher = SECRET_CONFIG_KV_PREFIX_PATH_PATTERN.matcher(propertyName);
return matcher.matches() ? (matcher.group(1) != null ? matcher.group(1) : matcher.group(2)) : null;
}

private List<String> getSecretConfigKvPrefixPath(String prefixName) {
return getOptionalListProperty("secret-config-kv-path." + prefixName).get();
return getOptionalListProperty(SECRET_CONFIG_KV_PREFIX_PATHS + "." + prefixName).get();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,14 @@

import java.net.URL;
import java.time.Duration;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Optional;

import io.quarkus.runtime.annotations.ConfigDocMapKey;
import io.quarkus.runtime.annotations.ConfigDocSection;
import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigPhase;
import io.quarkus.runtime.annotations.ConfigRoot;
Expand All @@ -23,7 +26,6 @@ public class VaultRuntimeConfig {

public static final String DEFAULT_KUBERNETES_JWT_TOKEN_PATH = "/var/run/secrets/kubernetes.io/serviceaccount/token";
public static final String DEFAULT_KV_SECRET_ENGINE_MOUNT_PATH = "secret";
public static final String KV_SECRET_ENGINE_VERSION_V1 = "1";
public static final String KV_SECRET_ENGINE_VERSION_V2 = "2";
public static final String DEFAULT_RENEW_GRACE_PERIOD = "1H";
public static final String DEFAULT_SECRET_CONFIG_CACHE_PERIOD = "10M";
Expand All @@ -35,9 +37,9 @@ public class VaultRuntimeConfig {

/**
* Vault server url.
* <p>
*
* Example: https://localhost:8200
* <p>
*
* See also the documentation for the `kv-secret-engine-mount-path` property for some insights on how
* the full Vault url gets built.
*
Expand All @@ -55,7 +57,7 @@ public class VaultRuntimeConfig {

/**
* Renew grace period duration.
* <p>
*
* This value if used to extend a lease before it expires its ttl, or recreate a new lease before the current
* lease reaches its max_ttl.
* By default Vault leaseDuration is equal to 7 days (ie: 168h or 604800s).
Expand All @@ -75,7 +77,7 @@ public class VaultRuntimeConfig {

/**
* Vault config source cache period.
* <p>
*
* Properties fetched from vault as MP config will be kept in a cache, and will not be fetched from vault
* again until the expiration of that period.
* This property is ignored if `secret-config-kv-path` is not set.
Expand All @@ -89,20 +91,20 @@ public class VaultRuntimeConfig {
/**
* List of comma separated vault paths in kv store,
* where all properties will be available as MP config properties **as-is**, with no prefix.
* <p>
*
* For instance, if vault contains property `foo`, it will be made available to the
* quarkus application as `@ConfigProperty(name = "foo") String foo;`
* <p>
*
* If 2 paths contain the same property, the last path will win.
* <p>
*
* For instance if
* <p>
*
* * `secret/base-config` contains `foo=bar` and
* * `secret/myapp/config` contains `foo=myappbar`, then
* <p>
*
* `@ConfigProperty(name = "foo") String foo` will have value `myappbar`
* with application properties `quarkus.vault.secret-config-kv-path=base-config,myapp/config`
* <p>
*
* See also the documentation for the `kv-secret-engine-mount-path` property for some insights on how
* the full Vault url gets built.
*
Expand All @@ -112,32 +114,17 @@ public class VaultRuntimeConfig {
@ConfigItem
public Optional<List<String>> secretConfigKvPath;

// @formatter:off
/**
* List of comma separated vault paths in kv store,
* where all properties will be available as **prefixed** MP config properties.
* <p>
* For instance if the application properties contains
* `quarkus.vault.secret-config-kv-path.myprefix=config`, and
* vault path `secret/config` contains `foo=bar`, then `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.
* <p>
* See also the documentation for the `kv-secret-engine-mount-path` property for some insights on how
* the full Vault url gets built.
*
* @asciidoclet
* KV store paths configuration.
*/
// @formatter:on
@ConfigItem(name = "secret-config-kv-path.\"prefix\"")
public Map<String, List<String>> secretConfigKvPrefixPath;
@ConfigItem(name = "secret-config-kv-path")
@ConfigDocMapKey("prefix")
public Map<String, KvPathConfig> secretConfigKvPathPrefix;

/**
* Used to hide confidential infos, for logging in particular.
* Possible values are:
* <p>
*
* * low: display all secrets.
* * medium: display only usernames and lease ids (ie: passwords and tokens are masked).
* * high: hide lease ids and dynamic credentials username.
Expand All @@ -149,7 +136,7 @@ public class VaultRuntimeConfig {

/**
* Kv secret engine version.
* <p>
*
* see https://www.vaultproject.io/docs/secrets/kv/index.html
*
* @asciidoclet
Expand All @@ -159,24 +146,24 @@ public class VaultRuntimeConfig {

/**
* KV secret engine path.
* <p>
*
* This value is used when building the url path in the KV secret engine programmatic access
* (i.e. `VaultKVSecretEngine`) and the vault config source (i.e. fetching configuration properties from Vault).
* <p>
*
* For a v2 KV secret engine (default - see `kv-secret-engine-version property`)
* the full url is built from the expression `<url>/v1/</kv-secret-engine-mount-path>/data/...`.
* <p>
*
* With property `quarkus.vault.url=https://localhost:8200`, the following call
* `vaultKVSecretEngine.readSecret("foo/bar")` would lead eventually to a `GET` on Vault with the following
* url: `https://localhost:8200/v1/secret/data/foo/bar`.
* <p>
*
* With a KV secret engine v1, the url changes to: `<url>/v1/</kv-secret-engine-mount-path>/...`.
* <p>
*
* The same logic is applied to the Vault config source. With `quarkus.vault.secret-config-kv-path=config/myapp`
* The secret properties would be fetched from Vault using a `GET` on
* `https://localhost:8200/v1/secret/data/config/myapp` for a KV secret engine v2 (or
* `https://localhost:8200/v1/secret/config/myapp` for a KV secret engine v1).
* <p>
*
* see https://www.vaultproject.io/docs/secrets/kv/index.html
*
* @asciidoclet
Expand Down Expand Up @@ -205,10 +192,10 @@ public class VaultRuntimeConfig {

/**
* List of named credentials providers, such as: `quarkus.vault.credentials-provider.foo.kv-path=mypath`
* <p>
*
* This defines a credentials provider `foo` returning key `password` from vault path `mypath`.
* Once defined, this provider can be used in credentials consumers, such as the Agroal connection pool.
* <p>
*
* Example: `quarkus.datasource.credentials-provider=foo`
*
* @asciidoclet
Expand Down Expand Up @@ -271,4 +258,43 @@ public String toString() {
'}';
}

@ConfigGroup
public static class KvPathConfig {
// @formatter:off
/**
* 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
* `quarkus.vault.secret-config-kv-path.myprefix=config`, and
* vault path `secret/config` contains `foo=bar`, then `myprefix.foo`
* will be available in the MP config.
*
* If the same property is available in 2 different paths for the same prefix, the last one
* will win.
*
* See also the documentation for the `quarkus.vault.kv-secret-engine-mount-path` property for some insights on how
* the full Vault url gets built.
*
* @asciidoclet
*/
// @formatter:on
@ConfigItem(name = ConfigItem.PARENT)
List<String> paths;

public KvPathConfig(List<String> paths) {
this.paths = paths;
}

public KvPathConfig() {
paths = Collections.emptyList();
}

@Override
public String toString() {
return "SecretConfigKvPathConfig{" +
"paths=" + paths +
'}';
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package io.quarkus.vault.runtime.config;

import static io.quarkus.vault.runtime.config.VaultConfigSource.SECRET_CONFIG_KV_PATH_PATTERN;
import static io.quarkus.vault.runtime.config.VaultConfigSource.PROPERTY_PREFIX;
import static io.quarkus.vault.runtime.config.VaultConfigSource.SECRET_CONFIG_KV_PREFIX_PATHS;
import static io.quarkus.vault.runtime.config.VaultConfigSource.SECRET_CONFIG_KV_PREFIX_PATH_PATTERN;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

Expand All @@ -10,16 +12,18 @@

public class VaultConfigSourceTest {

private static final String PROPERTY = PROPERTY_PREFIX + SECRET_CONFIG_KV_PREFIX_PATHS;

@Test
void secretConfigKvPathPattern() {

Matcher matcher;

matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher("quarkus.vault.secret-config-kv-path.hello");
matcher = SECRET_CONFIG_KV_PREFIX_PATH_PATTERN.matcher(PROPERTY + ".hello");
assertTrue(matcher.matches());
assertEquals("hello", matcher.group(1));

matcher = SECRET_CONFIG_KV_PATH_PATTERN.matcher("quarkus.vault.secret-config-kv-path.\"mp.jwt.verify\"");
matcher = SECRET_CONFIG_KV_PREFIX_PATH_PATTERN.matcher(PROPERTY + ".\"mp.jwt.verify\"");
assertTrue(matcher.matches());
assertEquals("mp.jwt.verify", matcher.group(2));
}
Expand Down