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

Secret Keys Handlers #833

Merged
merged 8 commits into from
Mar 21, 2023
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Compile and test the project:
mvn verify
----

Generate the documentation (from the documentation folder):
Generate the documentation (from the `documentation` folder):

[source,bash]
----
Expand Down
2 changes: 2 additions & 0 deletions documentation/mkdocs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ nav:
- 'FileSystem': config-sources/filesystem.md
- 'ZooKeeper': config-sources/zookeeper.md
- 'HOCON': config-sources/hocon.md
- 'KeyStore': config-sources/keystore.md
- Converters:
- 'Custom': converters/custom.md
- 'JSON': converters/json.md
Expand Down Expand Up @@ -87,6 +88,7 @@ theme:

markdown_extensions:
- toc:
toc_depth: 3
permalink: '#'
- admonition
- smarty
Expand Down
25 changes: 25 additions & 0 deletions documentation/src/main/docs/config-sources/keystore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# KeyStore Config Source

This Config Source allows to use a Java `KeyStore` to load configuration values. It uses an ordinal of `100`.

The following dependency is required in the classpath to use the KeyStore Config Source:

```xml
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-source-keystore</artifactId>
<version>{{attributes['version']}}</version>
</dependency>
```

## Configuration

| Configuration Property | Type | Default |
|---------------------------------------------------------------------------------------------------------------------|--- |----|
| `smallrye.config.source.keystore."name".path`<br>The KeyStore path. | String | |
| `smallrye.config.source.keystore."name".password`<br>The KeyStore password. | String | |
| `smallrye.config.source.keystore."name".type`<br>The KeyStore type. | String | `PKCS12` |
| `smallrye.config.source.keystore."name".handler`<br>An Optional secret keys handler. | String | |
| `smallrye.config.source.keystore."name".aliases."key".name`<br>An Optional aliases key name. | String | |
| `smallrye.config.source.keystore."name".aliases."key".password`<br>An Optional aliases key password. | String | |
| `smallrye.config.source.keystore."name".aliases."key".handler`<br>An Optional aliases key secret keys handler. | String | |
69 changes: 67 additions & 2 deletions documentation/src/main/docs/config/secret-keys.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,76 @@
# Secret Keys

## Secret Keys Expressions

In SmallRye Config, a secret configuration may be expressed as `${handler::value}`, where the `handler` is the name of
a `io.smallrye.config.SecretKeysHandler` to decode or decrypt the `value`. Consider:

```properties
my.secret=${aes-gcm-nopadding::DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg}
```

A lookup to `my.secret` will use the `SecretKeysHandler` name `aes-gcm-nopadding` to decode the value
`DJNrZ6LfpupFv6QbXyXhvzD8eVDnDa_kTliQBpuzTobDZxlg`.

It is possible to create a custom `SecretKeysHandler` and provide different ways to decode or decrypt configuration
values.

A custom `SecretKeysHandler` requires an implementation of `io.smallrye.config.SecretKeysHandler` or
`io.smallrye.config.SecretKeysHandlerFactory`. Each implementation requires registration via the `ServiceLoader`
mechanism, either in `META-INF/services/io.smallrye.config.SecretKeysHandler` or
`META-INF/services/io.smallrye.config.SecretKeysHandlerFactory` files.

### Crypto

The `smallrye-config-crypto` artifact contains a few out-of-the-box `SecretKeysHandler`s ready for use. It requires
the following dependency:

```xml
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-crypto</artifactId>
<version>{{attributes['version']}}</version>
</dependency>
```

#### AES/GCM/NoPadding `${aes-gcm-nopadding::...}`

##### Configuration

| Configuration Property | Type | Default |
|--- |--- |--- |
| `smallrye.config.secret-handler.aes-gcm-nopadding.encryption-key`<br>The encription key to use to decode secrets encoded by the `AES/GCM/NoPadding` algorithm. | String | |

### Jasypt

[Jasypt](http://www.jasypt.org) is a java library which allows the developer to add basic encryption capabilities. It
requires the following dependency:

```xml
<dependency>
<groupId>io.smallrye.config</groupId>
<artifactId>smallrye-config-jasypt</artifactId>
<version>{{attributes['version']}}</version>
</dependency>
```

#### Jasypt ``${jasypt::...}``

##### Configuration

| Configuration Property | Type | Default |
|--- |--- |--- |
| `smallrye.config.secret-handler.jasypt.password`<br>The Jasypt password to use | String | |
| `smallrye.config.secret-handler.jasypt.algorithm`<br>The Jasypt algorithm to use | String | |

## Secret Keys Names

When configuration properties contain passwords or other kinds of secrets, Smallrye Config can hide them to prevent
accidental exposure of such values.

**This is no way a replacement for securing secrets.** Proper security mechanisms must still be used to secure
secrets. However, there is still the basic problem that passwords and secrets are generally encoded simply as strings.
Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled.
secrets. However, there is still the fundamental problem that passwords and secrets are generally encoded simply as
strings. Secret Keys provides a way to "lock" the configuration so that secrets do not appear unless explicitly enabled.

To mark specific keys as secrets, register an instance of `io.smallrye.config.SecretKeysConfigSourceInterceptor` by
using the interceptor factory as follows:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,13 @@ private boolean validExtension(final Path fileName) {
}

private boolean validExtension(final String resourceName) {
for (String s : getFileExtensions()) {
String[] fileExtensions = getFileExtensions();

if (fileExtensions.length == 0) {
return true;
}

for (String s : fileExtensions) {
if (resourceName.endsWith(s)) {
return true;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,4 +154,7 @@ IllegalArgumentException converterException(@Cause Throwable converterException,

@Message(id = 45, value = "The %s class is not a ConfigMapping")
IllegalArgumentException classIsNotAMapping(Class<?> type);

@Message(id = 46, value = "Could not find a secret key handler for %s")
NoSuchElementException secretKeyHandlerNotFound(String handler);
}
20 changes: 20 additions & 0 deletions implementation/src/main/java/io/smallrye/config/ConfigValue.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ public class ConfigValue implements org.eclipse.microprofile.config.ConfigValue
private final int configSourcePosition;
private final int lineNumber;

private final String extendedExpressionHandler;
private final List<Problem> problems;

private ConfigValue(final ConfigValueBuilder builder) {
Expand All @@ -43,6 +44,7 @@ private ConfigValue(final ConfigValueBuilder builder) {
this.configSourceOrdinal = builder.configSourceOrdinal;
this.configSourcePosition = builder.configSourcePosition;
this.lineNumber = builder.lineNumber;
this.extendedExpressionHandler = builder.extendedExpressionHandler;
this.problems = builder.problems;
}

Expand Down Expand Up @@ -99,6 +101,10 @@ public String getLocation() {
return lineNumber != -1 ? configSourceName + ":" + lineNumber : configSourceName;
}

public String getExtendedExpressionHandler() {
return extendedExpressionHandler;
}

List<Problem> getProblems() {
return Collections.unmodifiableList(problems);
}
Expand Down Expand Up @@ -131,6 +137,10 @@ public ConfigValue withLineNumber(final int lineNumber) {
return from().withLineNumber(lineNumber).build();
}

public ConfigValue withExtendedExpressionHandler(final String extendedExpressionHandler) {
return from().withExtendedExpressionHandler(extendedExpressionHandler).build();
}

public ConfigValue noProblems() {
return from().noProblems().build();
}
Expand Down Expand Up @@ -190,6 +200,7 @@ public ConfigValueBuilder from() {
.withConfigSourceOrdinal(configSourceOrdinal)
.withConfigSourcePosition(configSourcePosition)
.withLineNumber(lineNumber)
.withExtendedExpressionHandler(extendedExpressionHandler)
.withProblems(problems);
}

Expand All @@ -206,6 +217,7 @@ public static class ConfigValueBuilder {
private int configSourceOrdinal;
private int configSourcePosition;
private int lineNumber = -1;
private String extendedExpressionHandler;
private final List<Problem> problems = new ArrayList<>();

public ConfigValueBuilder withName(final String name) {
Expand Down Expand Up @@ -248,6 +260,11 @@ public ConfigValueBuilder withLineNumber(final int lineNumber) {
return this;
}

public ConfigValueBuilder withExtendedExpressionHandler(final String extendedExpressionHandler) {
this.extendedExpressionHandler = extendedExpressionHandler;
return this;
}

public ConfigValueBuilder noProblems() {
this.problems.clear();
return this;
Expand All @@ -264,6 +281,9 @@ public ConfigValueBuilder addProblem(final Problem problem) {
}

public ConfigValue build() {
if (!this.problems.isEmpty()) {
this.value = null;
}
return new ConfigValue(this);
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,15 @@
package io.smallrye.config;

import static io.smallrye.common.expression.Expression.Flag.DOUBLE_COLON;
import static io.smallrye.common.expression.Expression.Flag.LENIENT_SYNTAX;
import static io.smallrye.common.expression.Expression.Flag.NO_SMART_BRACES;
import static io.smallrye.common.expression.Expression.Flag.NO_TRIM;
import static io.smallrye.config.ConfigMessages.msg;

import java.util.ArrayList;
import java.util.List;
import java.util.function.BiConsumer;

import jakarta.annotation.Priority;

import org.eclipse.microprofile.config.Config;

import io.smallrye.common.expression.Expression;
import io.smallrye.common.expression.ResolveContext;
import io.smallrye.config.ConfigValidationException.Problem;
Expand All @@ -39,7 +36,7 @@ public ConfigValue getValue(final ConfigSourceInterceptorContext context, final
}

private ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name, final int depth) {
if (depth == MAX_DEPTH) {
if (depth >= MAX_DEPTH) {
throw msg.expressionExpansionTooDepth(name);
}

Expand All @@ -53,25 +50,39 @@ private ConfigValue getValue(final ConfigSourceInterceptorContext context, final
return null;
}

List<Problem> problems = new ArrayList<>();
ConfigValue.ConfigValueBuilder value = configValue.from();
Expression expression = Expression.compile(escapeDollarIfExists(configValue.getValue()), LENIENT_SYNTAX, NO_TRIM,
NO_SMART_BRACES);
NO_SMART_BRACES, DOUBLE_COLON);
radcortez marked this conversation as resolved.
Show resolved Hide resolved
String expanded = expression.evaluate(new BiConsumer<ResolveContext<RuntimeException>, StringBuilder>() {
@Override
public void accept(ResolveContext<RuntimeException> resolveContext, StringBuilder stringBuilder) {
ConfigValue resolve = getValue(context, resolveContext.getKey(), depth + 1);
String key = resolveContext.getKey();

// Requires a handler lookup
int index = key.indexOf("::");
if (index != -1) {
value.withExtendedExpressionHandler(key.substring(0, index));
stringBuilder.append(key, index + 2, key.length());
return;
}

// Expression lookup
ConfigValue resolve = getValue(context, key, depth + 1);
if (resolve != null) {
stringBuilder.append(resolve.getValue());
problems.addAll(resolve.getProblems());
if (resolve.getProblems().isEmpty()) {
stringBuilder.append(resolve.getValue());
} else {
value.withProblems(resolve.getProblems());
}
} else if (resolveContext.hasDefault()) {
resolveContext.expandDefault();
} else {
problems.add(new Problem(msg.expandingElementNotFound(resolveContext.getKey(), configValue.getName())));
value.addProblem(new Problem(msg.expandingElementNotFound(key, configValue.getName())));
}
}
});

return problems.isEmpty() ? configValue.withValue(expanded) : configValue.withValue(null).withProblems(problems);
return value.withValue(expanded).build();
}

/**
Expand Down
11 changes: 5 additions & 6 deletions implementation/src/main/java/io/smallrye/config/SecretKeys.java
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package io.smallrye.config;

import java.io.Serializable;
import java.util.function.Supplier;

@SuppressWarnings("squid:S5164")
public final class SecretKeys {
private static final ThreadLocal<Boolean> LOCKED = new ThreadLocal<>();
public final class SecretKeys implements Serializable {
private static final long serialVersionUID = -3226034787747746735L;

private SecretKeys() {
throw new UnsupportedOperationException();
}
private static final ThreadLocal<Boolean> LOCKED = new ThreadLocal<>();

public static boolean isLocked() {
Boolean result = LOCKED.get();
return result == null ? true : result;
return result == null || result;
}

public static void doUnlocked(Runnable runnable) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public SecretKeysConfigSourceInterceptor(final Set<String> secrets) {

@Override
public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) {
if (SecretKeys.isLocked() && isSecret(name)) {
if (SecretKeys.isLocked() && secrets.contains(name)) {
throw ConfigMessages.msg.notAllowed(name);
}
return context.proceed(name);
Expand Down Expand Up @@ -55,8 +55,4 @@ public Iterator<ConfigValue> iterateValues(final ConfigSourceInterceptorContext
}
return context.iterateValues();
}

private boolean isSecret(final String name) {
return secrets.contains(name);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.smallrye.config;

import io.smallrye.common.annotation.Experimental;

@Experimental("")
public interface SecretKeysHandler {
String decode(String secret);

String getName();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package io.smallrye.config;

import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class SecretKeysHandlerConfigSourceInterceptor implements ConfigSourceInterceptor {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes sense to me. We can look up secret values separately from extracting them from an expression, which also allows specialized config sources that might encode them in an idiomatic way. 👍

private static final long serialVersionUID = -5228028387733656005L;

private final Map<String, SecretKeysHandler> handlers = new HashMap<>();

public SecretKeysHandlerConfigSourceInterceptor(final List<SecretKeysHandler> handlers) {
for (SecretKeysHandler handler : handlers) {
this.handlers.put(handler.getName(), handler);
}
}

@Override
public ConfigValue getValue(final ConfigSourceInterceptorContext context, final String name) {
ConfigValue configValue = context.proceed(name);
if (configValue != null && configValue.getValue() != null) {
String handler = configValue.getExtendedExpressionHandler();
if (handler != null) {
return configValue.withValue(getSecretValue(handler, configValue.getValue()));
}
}
return configValue;
}

private String getSecretValue(final String handlerName, final String secretName) {
SecretKeysHandler handler = handlers.get(handlerName);
if (handler != null) {
return handler.decode(secretName);
}
throw ConfigMessages.msg.secretKeyHandlerNotFound(handlerName);
}
}