Keystore vault (#19644)

* KeystoreVault SPI

* added KeystoreVault - a Vault SPI implementation (#19281)

Closes #17252

Signed-off-by: Peter Zaoral <pzaoral@redhat.com>
This commit is contained in:
Peter Zaoral 2023-05-24 18:20:30 +02:00 committed by GitHub
parent b8b56ece65
commit 72b238fb48
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 483 additions and 112 deletions

View file

@ -3,6 +3,8 @@
== Using a vault to obtain secrets == Using a vault to obtain secrets
Keycloak currently provides two out-of-the-box implementations of the Vault SPI: a plain-text file-based vault and Java KeyStore-based vault.
To obtain a secret from a vault rather than entering it directly, enter the following specially crafted string into the appropriate field: To obtain a secret from a vault rather than entering it directly, enter the following specially crafted string into the appropriate field:
[source] [source]
@ -11,7 +13,7 @@ ${vault.key}
---- ----
where the `key` is the name of the secret recognized by the vault. where the `key` is the name of the secret recognized by the vault.
To prevent secrets from leaking across realms, {project_name} combines the realm name with the `key` obtained from the vault expression. This method means that the `key` does not directly map to an entry in the vault but creates the final entry name according to the algorithm used to combine the `key` with the realm name. To prevent secrets from leaking across realms, {project_name} combines the realm name with the `key` obtained from the vault expression. This method means that the `key` does not directly map to an entry in the vault but creates the final entry name according to the algorithm used to combine the `key` with the realm name. In case of the file-based vault, such combination reflects to a specific filename, for the Java KeyStore-based vault it's a specific alias name.
You can obtain the secret from the vault in the following fields: You can obtain the secret from the vault in the following fields:

View file

@ -2,31 +2,41 @@
<#import "/templates/kc.adoc" as kc> <#import "/templates/kc.adoc" as kc>
<@tmpl.guide <@tmpl.guide
title="Using Kubernetes secrets" title="Using a vault"
summary="Learn how to use Kubernetes/OpenShift secrets in Keycloak" summary="Learn how to use and configure a vault in Keycloak"
priority=30 priority=30
includedOptions="vault vault-*"> includedOptions="vault vault-*">
Keycloak supports a file-based vault implementation for Kubernetes/OpenShift secrets. Mount Kubernetes secrets into the Keycloak Container, and the data fields will be available in the mounted folder with a flat-file structure. Keycloak provides two out-of-the-box implementations of the Vault SPI: a plain-text file-based vault and Java KeyStore-based vault.
The file-based vault implementation is especially useful for Kubernetes/OpenShift secrets. You can mount Kubernetes secrets into the Keycloak Container, and the data fields will be available in the mounted folder with a flat-file structure.
The Java KeyStore-based vault implementation is useful for storing secrets in bare metal installations. You can use the KeyStore vault, which is encrypted using a password.
== Available integrations == Available integrations
You can use Kubernetes/OpenShift secrets for the following purposes: Secrets stored in the vaults can be used at the following places of the Administration Console:
* Obtain the SMTP Mail server Password * Obtain the SMTP Mail server Password
* Obtain the LDAP Bind Credential when using LDAP-based User Federation * Obtain the LDAP Bind Credential when using LDAP-based User Federation
* Obtain the OIDC identity providers Client Secret when integrating external identity providers * Obtain the OIDC identity providers Client Secret when integrating external identity providers
== Enabling the vault == Enabling a vault
Enable the file based vault by building Keycloak using the following build option: For enabling the file-based vault you need to build Keycloak first using the following build option:
<@kc.build parameters="--vault=file"/> <@kc.build parameters="--vault=file"/>
== Setting the base directory to lookup secrets Analogically, for the Java KeyStore-based you need to specify the following build option:
<@kc.build parameters="--vault=keystore"/>
== Configuring the file-based vault
=== Setting the base directory to lookup secrets
Kubernetes/OpenShift secrets are basically mounted files. To configure a directory where these files should be mounted, enter this command: Kubernetes/OpenShift secrets are basically mounted files. To configure a directory where these files should be mounted, enter this command:
<@kc.start parameters="--vault-dir=/my/path"/> <@kc.start parameters="--vault-dir=/my/path"/>
== Realm-specific secret files === Realm-specific secret files
Kubernetes/OpenShift Secrets are used on a per-realm basis in Keycloak, which requires a naming convention for the file in place: Kubernetes/OpenShift Secrets are used on a per-realm basis in Keycloak, which requires a naming convention for the file in place:
[source, bash] [source, bash]
---- ----
@ -46,6 +56,25 @@ sso__realm_ldap__credential
---- ----
Note the doubled underscores between __sso__ and __realm__ and also between __ldap__ and __credential__. Note the doubled underscores between __sso__ and __realm__ and also between __ldap__ and __credential__.
== Configuring the Java KeyStore-based vault
In order to use the Java KeyStore-based vault, you need to create a KeyStore file first. You can use the following command for doing so:
[source, bash]
----
keytool -importpass -alias <realm-name>_<alias> -keystore keystore.p12 -storepass keystorepassword
----
and then enter a value you want to store in the vault. Note that the format of the `-alias` parameter depends on the key resolver used. The default key resolver is `REALM_UNDERSCORE_KEY`.
This by default results to storing the value in a form of generic PBEKey (password based encryption) within SecretKeyEntry.
You can then start Keycloak using the following runtime options:
<@kc.start parameters=" --vault-file=/path/to/keystore.p12 --vault-pass=<value> --vault-type=<value>"/>
Note that the `--vault-type` parameter is optional and defaults to `PKCS12`.
Secrets stored in the vault can then be accessed in a realm via the following placeholder (assuming using the `REALM_UNDERSCORE_KEY` key resolver): `${r"${vault.realm-name_alias}"}`.
== Example: Use an LDAP bind credential secret in the Admin Console == Example: Use an LDAP bind credential secret in the Admin Console
.Example setup .Example setup

View file

@ -1,22 +1,49 @@
package org.keycloak.config; package org.keycloak.config;
import java.io.File; import java.nio.file.Path;
public class VaultOptions { public class VaultOptions {
public enum Provider { public enum VaultType {
file;
file("file"),
keystore("keystore");
private final String provider;
VaultType(String provider) {
this.provider = provider;
}
public String getProvider() {
return provider;
}
} }
public static final Option VAULT = new OptionBuilder<>("vault", Provider.class) public static final Option<VaultOptions.VaultType> VAULT = new OptionBuilder<>("vault", VaultType.class)
.category(OptionCategory.VAULT) .category(OptionCategory.VAULT)
.description("Enables a vault provider.") .description("Enables a vault provider.")
.buildTime(true) .buildTime(true)
.build(); .build();
public static final Option VAULT_DIR = new OptionBuilder<>("vault-dir", File.class) public static final Option<Path> VAULT_DIR = new OptionBuilder<>("vault-dir", Path.class)
.category(OptionCategory.VAULT) .category(OptionCategory.VAULT)
.description("If set, secrets can be obtained by reading the content of files within the given directory.") .description("If set, secrets can be obtained by reading the content of files within the given directory.")
.build(); .build();
public static final Option<String> VAULT_PASS = new OptionBuilder<>("vault-pass", String.class)
.category(OptionCategory.VAULT)
.description("Password for the vault keystore.")
.build();
public static final Option<Path> VAULT_FILE = new OptionBuilder<>("vault-file", Path.class)
.category(OptionCategory.VAULT)
.description("Path to the keystore file.")
.build();
public static final Option<String> VAULT_TYPE = new OptionBuilder<>("vault-type", String.class)
.category(OptionCategory.VAULT)
.description("Specifies the type of the keystore file.")
.defaultValue("PKCS12")
.build();
} }

View file

@ -110,6 +110,7 @@ import org.keycloak.url.DefaultHostnameProviderFactory;
import org.keycloak.url.FixedHostnameProviderFactory; import org.keycloak.url.FixedHostnameProviderFactory;
import org.keycloak.url.RequestHostnameProviderFactory; import org.keycloak.url.RequestHostnameProviderFactory;
import org.keycloak.util.JsonSerialization; import org.keycloak.util.JsonSerialization;
import org.keycloak.vault.FilesKeystoreVaultProviderFactory;
import org.keycloak.vault.FilesPlainTextVaultProviderFactory; import org.keycloak.vault.FilesPlainTextVaultProviderFactory;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
@ -176,6 +177,7 @@ class KeycloakProcessor {
DefaultHostnameProviderFactory.class, DefaultHostnameProviderFactory.class,
FixedHostnameProviderFactory.class, FixedHostnameProviderFactory.class,
RequestHostnameProviderFactory.class, RequestHostnameProviderFactory.class,
FilesKeystoreVaultProviderFactory.class,
FilesPlainTextVaultProviderFactory.class, FilesPlainTextVaultProviderFactory.class,
BlacklistPasswordPolicyProviderFactory.class, BlacklistPasswordPolicyProviderFactory.class,
ClasspathThemeResourceProviderFactory.class, ClasspathThemeResourceProviderFactory.class,

View file

@ -17,6 +17,18 @@ final class VaultPropertyMappers {
fromOption(VaultOptions.VAULT_DIR) fromOption(VaultOptions.VAULT_DIR)
.to("kc.spi-vault-file-dir") .to("kc.spi-vault-file-dir")
.paramLabel("dir") .paramLabel("dir")
.build(),
fromOption(VaultOptions.VAULT_FILE)
.to("kc.spi-vault-keystore-file")
.paramLabel("file")
.build(),
fromOption(VaultOptions.VAULT_PASS)
.to("kc.spi-vault-keystore-pass")
.paramLabel("pass")
.build(),
fromOption(VaultOptions.VAULT_TYPE)
.to("kc.spi-vault-keystore-type")
.paramLabel("type")
.build() .build()
}; };
} }

View file

@ -0,0 +1,43 @@
/*
* Copyright 2021 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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
*
* http://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.keycloak.quarkus.runtime.vault;
import org.keycloak.Config;
import org.keycloak.provider.EnvironmentDependentProviderFactory;
import org.keycloak.quarkus.runtime.configuration.Configuration;
public class FilesKeystoreVaultProviderFactory extends org.keycloak.vault.FilesKeystoreVaultProviderFactory
implements EnvironmentDependentProviderFactory {
public static final String ID = "keystore";
@Override
public String getId() {
return ID;
}
@Override
public boolean isSupported() {
return false;
}
@Override
public boolean isSupported(Config.Scope config) {
return getId().equals(Configuration.getRawValue("kc.vault"));
}
}

View file

@ -1 +1,2 @@
org.keycloak.quarkus.runtime.vault.FilesPlainTextVaultProviderFactory org.keycloak.quarkus.runtime.vault.FilesPlainTextVaultProviderFactory
org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory

View file

@ -28,10 +28,11 @@ import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
import io.smallrye.config.SmallRyeConfig;
import io.smallrye.config.SmallRyeConfigProviderResolver;
import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.H2Dialect;
import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.PostgreSQLDialect;
import io.quarkus.runtime.LaunchMode; import io.quarkus.runtime.LaunchMode;
import io.smallrye.config.SmallRyeConfig;
import org.eclipse.microprofile.config.ConfigProvider; import org.eclipse.microprofile.config.ConfigProvider;
import org.eclipse.microprofile.config.spi.ConfigProviderResolver; import org.eclipse.microprofile.config.spi.ConfigProviderResolver;
import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.MariaDBDialect;
@ -44,8 +45,8 @@ import org.keycloak.quarkus.runtime.configuration.KeycloakConfigSourceProvider;
import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider; import org.keycloak.quarkus.runtime.configuration.MicroProfileConfigProvider;
import io.quarkus.runtime.configuration.ConfigUtils; import io.quarkus.runtime.configuration.ConfigUtils;
import io.smallrye.config.SmallRyeConfigProviderResolver;
import org.keycloak.quarkus.runtime.Environment; import org.keycloak.quarkus.runtime.Environment;
import org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory;
import org.keycloak.quarkus.runtime.vault.FilesPlainTextVaultProviderFactory; import org.keycloak.quarkus.runtime.vault.FilesPlainTextVaultProviderFactory;
import org.mariadb.jdbc.MariaDbDataSource; import org.mariadb.jdbc.MariaDbDataSource;
import org.postgresql.xa.PGXADataSource; import org.postgresql.xa.PGXADataSource;
@ -142,6 +143,12 @@ public class ConfigurationTest {
assertEquals("/foo/bar", config.get("dir")); assertEquals("/foo/bar", config.get("dir"));
assertTrue(config.getPropertyNames() assertTrue(config.getPropertyNames()
.contains("kc.spi-vault-".concat(FilesPlainTextVaultProviderFactory.ID).concat("-dir"))); .contains("kc.spi-vault-".concat(FilesPlainTextVaultProviderFactory.ID).concat("-dir")));
putEnvVar("KC_VAULT_TYPE", "JKS");
config = initConfig("vault", FilesKeystoreVaultProviderFactory.ID);
assertEquals("JKS", config.get("type"));
assertTrue(config.getPropertyNames()
.contains("kc.spi-vault-".concat(FilesKeystoreVaultProviderFactory.ID).concat("-type")));
} }
@Test @Test
@ -215,6 +222,11 @@ public class ConfigurationTest {
assertEquals(1, config.getPropertyNames().size()); assertEquals(1, config.getPropertyNames().size());
assertEquals("secrets", config.get("dir")); assertEquals("secrets", config.get("dir"));
System.setProperty(CLI_ARGS, "--vault-type=JKS");
config = initConfig("vault", FilesKeystoreVaultProviderFactory.ID);
assertEquals(1, config.getPropertyNames().size());
assertEquals("JKS", config.get("type"));
System.getProperties().remove(CLI_ARGS); System.getProperties().remove(CLI_ARGS);
System.setProperty("kc.spi-client-registration-openid-connect-static-jwk-url", "http://c.jwk.url"); System.setProperty("kc.spi-client-registration-openid-connect-static-jwk-url", "http://c.jwk.url");
config = initConfig("client-registration", "openid-connect"); config = initConfig("client-registration", "openid-connect");

View file

@ -80,7 +80,7 @@ Metrics:
Vault: Vault:
--vault <provider> Enables a vault provider. Possible values are: file. --vault <provider> Enables a vault provider. Possible values are: file, keystore.
Security: Security:

View file

@ -173,9 +173,12 @@ Proxy:
Vault: Vault:
--vault <provider> Enables a vault provider. Possible values are: file. --vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the --vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory. given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging: Logging:

View file

@ -236,9 +236,12 @@ Proxy:
Vault: Vault:
--vault <provider> Enables a vault provider. Possible values are: file. --vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the --vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory. given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging: Logging:

View file

@ -179,9 +179,12 @@ Proxy:
Vault: Vault:
--vault <provider> Enables a vault provider. Possible values are: file. --vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the --vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory. given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging: Logging:

View file

@ -242,9 +242,12 @@ Proxy:
Vault: Vault:
--vault <provider> Enables a vault provider. Possible values are: file. --vault <provider> Enables a vault provider. Possible values are: file, keystore.
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the --vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory. given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging: Logging:

View file

@ -125,6 +125,9 @@ Vault:
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the --vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory. given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging: Logging:

View file

@ -144,6 +144,9 @@ Vault:
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the --vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
given directory. given directory.
--vault-file <file> Path to the keystore file.
--vault-pass <pass> Password for the vault keystore.
--vault-type <type> Specifies the type of the keystore file. Default: PKCS12.
Logging: Logging:

View file

@ -0,0 +1,71 @@
package org.keycloak.vault;
import java.nio.ByteBuffer;
import org.jboss.logging.Logger;
import javax.annotation.Nonnull;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.Key;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableEntryException;
import java.security.cert.CertificateException;
import java.util.List;
import java.util.Optional;
public class FilesKeystoreVaultProvider extends AbstractVaultProvider {
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass());
private final Path keystorePath;
private final String keystorePass;
private final String keystoreType;
/**
* Creates a new {@link FilesKeystoreVaultProvider}.
*
* @param keystorePath A path to a vault. Can not be null.
* @param keystorePass A password to a vault. Can not be null.
* @param keystoreType Specifies a type of keystore. Can not be null. Default value is PKCS12.
* @param realmName A realm name. Can not be null.
*/
public FilesKeystoreVaultProvider(@Nonnull Path keystorePath, @Nonnull String keystorePass, @Nonnull String keystoreType,
@Nonnull String realmName, @Nonnull List<VaultKeyResolver> resolvers) {
super(realmName, resolvers);
this.keystorePath = keystorePath;
this.keystorePass = keystorePass;
this.keystoreType = keystoreType;
logger.debugf("KeystoreVaultProvider will operate in %s directory", keystorePath.toAbsolutePath());
}
@Override
protected VaultRawSecret obtainSecretInternal(String alias) {
KeyStore ks;
Key key;
try {
if (!Files.exists(keystorePath.toRealPath())) {
throw new VaultNotFoundException("The keystore file for Keycloak Vault was not found");
}
ks = KeyStore.getInstance(keystoreType);
ks.load(Files.newInputStream(keystorePath.toRealPath()), keystorePass.toCharArray());
key = ks.getKey(alias, keystorePass.toCharArray());
if (key == null) {
logger.warnf("Cannot find secret %s in %s", alias, keystorePath);
return DefaultVaultRawSecret.forBuffer(Optional.empty());
}
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException | UnrecoverableEntryException e) {
throw new RuntimeException(e);
}
return DefaultVaultRawSecret.forBuffer(Optional.of(ByteBuffer.wrap(new String(key.getEncoded()).getBytes())));
}
@Override
public void close() {
}
}

View file

@ -0,0 +1,70 @@
package org.keycloak.vault;
import org.jboss.logging.Logger;
import org.keycloak.Config;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;
import java.lang.invoke.MethodHandles;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
public class FilesKeystoreVaultProviderFactory extends AbstractVaultProviderFactory {
private static final Logger logger = Logger.getLogger(MethodHandles.lookup().lookupClass());
public static final String PROVIDER_ID = "files-keystore";
private Path keystoreFile;
private String keystorePass;
private String keystoreType;
@Override
public VaultProvider create(KeycloakSession session) {
if (keystoreFile == null) {
logger.debug("Can not create a vault since it's not initialized correctly");
return null;
}
return new FilesKeystoreVaultProvider(keystoreFile, keystorePass, keystoreType, getRealmName(session), super.keyResolvers);
}
@Override
public void init(Config.Scope config) {
super.init(config);
String pathConfigProperty = config.get("file");
if (pathConfigProperty == null) {
logger.debug("Path to the vault keystore is not configured");
return;
}
keystoreFile = Paths.get(pathConfigProperty);
if (!Files.exists(keystoreFile)) {
throw new VaultNotFoundException("The vault does not exist on the path " + keystoreFile.toAbsolutePath());
}
keystorePass = config.get("pass");
if (keystorePass == null) {
logger.debug("Password for the vault keystore is not configured");
return;
}
keystoreType = config.get("type", "PKCS12");
logger.debugf("A type of the provided keystore is %s", keystoreType);
logger.debugf("Configured KeystoreVaultProviderFactory with the keystore file located in %s", keystoreFile.toString());
}
@Override
public void postInit(KeycloakSessionFactory factory) {
}
@Override
public void close() {
}
@Override
public String getId() {
return PROVIDER_ID;
}
}

View file

@ -1 +1,2 @@
org.keycloak.vault.FilesPlainTextVaultProviderFactory org.keycloak.vault.FilesPlainTextVaultProviderFactory
org.keycloak.vault.FilesKeystoreVaultProviderFactory

View file

@ -0,0 +1,69 @@
package org.keycloak.vault;
import org.junit.Assume;
import org.junit.Before;
import org.junit.Test;
import org.keycloak.common.util.Environment;
import java.nio.file.Paths;
import java.util.Arrays;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertThrows;
import static org.keycloak.vault.SecretContains.secretContains;
/**
* Tests for {@link FilesKeystoreVaultProvider}.
*
* @author Peter Zaoral
*/
public class KeystoreVaultProviderTest {
@Before
public void before() {
// TODO: improve when the supported keystore types for FIPS will be unified across the codebase
Assume.assumeFalse("Java is in FIPS mode. Skipping the test.", Environment.isJavaInFipsMode());
}
@Test
public void shouldObtainSecret() {
//given
// keytool -importpass -storetype pkcs12 -alias test_alias -keystore myks -storepass keystorepassword
VaultProvider provider = new FilesKeystoreVaultProvider(Paths.get(Scenario.EXISTING.getAbsolutePathAsString() + "/myks"), "keystorepassword", "PKCS12","test",
Arrays.asList(AbstractVaultProviderFactory.AvailableResolvers.REALM_UNDERSCORE_KEY.getVaultKeyResolver()));
//when
VaultRawSecret secret1 = provider.obtainSecret("alias");
//then
assertNotNull(secret1);
assertNotNull(secret1.get().get());
assertThat(secret1, secretContains("topsecret"));
}
@Test
public void shouldObtainSecretFromDifferentKeystoreType() {
//given
VaultProvider provider = new FilesKeystoreVaultProvider(Paths.get(Scenario.EXISTING.getAbsolutePathAsString() + "/myks.jceks"), "keystorepassword", "JCEKS", "test",
Arrays.asList(AbstractVaultProviderFactory.AvailableResolvers.REALM_UNDERSCORE_KEY.getVaultKeyResolver()));
//when
VaultRawSecret secret1 = provider.obtainSecret("alias");
//then
assertNotNull(secret1);
assertNotNull(secret1.get().get());
assertThat(secret1, secretContains("topsecret"));
}
@Test
public void shouldFailBecauseOfTypeMismatch() {
//given
VaultProvider provider = new FilesKeystoreVaultProvider(Paths.get(Scenario.EXISTING.getAbsolutePathAsString() + "/myks"), "keystorepassword", "JCEKS", "test",
Arrays.asList(AbstractVaultProviderFactory.AvailableResolvers.REALM_UNDERSCORE_KEY.getVaultKeyResolver()));
//when
assertThrows("java.io.IOException: Invalid keystore format", RuntimeException.class, () -> provider.obtainSecret("alias"));
}
}

Binary file not shown.

View file

@ -44,7 +44,3 @@ spi-events-store-jpa-max-detail-length=2000
# set known protocol ports for basicsamltest # set known protocol ports for basicsamltest
spi-login-protocol-saml-known-protocols=http=8180,https=8543 spi-login-protocol-saml-known-protocols=http=8180,https=8543
# File-Based Vault
vault=file
vault-dir=${kc.home.dir}/secrets

View file

@ -42,16 +42,6 @@ public class VaultTestExecutionDecider implements TestExecutionDecider {
// if test was annotated with EnableVault, check if it has selected the elytron credential store provider. // if test was annotated with EnableVault, check if it has selected the elytron credential store provider.
if (testContext.getTestClass().isAnnotationPresent(EnableVault.class)) { if (testContext.getTestClass().isAnnotationPresent(EnableVault.class)) {
EnableVault.PROVIDER_ID providerId = testContext.getTestClass().getAnnotation(EnableVault.class).providerId(); EnableVault.PROVIDER_ID providerId = testContext.getTestClass().getAnnotation(EnableVault.class).providerId();
if (providerId == EnableVault.PROVIDER_ID.ELYTRON_CS_KEYSTORE) {
// if the auth server is undertow, skip the test.
SuiteContext suiteContext = testContext.getSuiteContext();
if (suiteContext != null && suiteContext.getAuthServerInfo() != null && suiteContext.getAuthServerInfo().isUndertow()) {
return ExecutionDecision.dontExecute("@EnableVault with Elytron credential store provider not supported on Undertow, skipping");
}
if (suiteContext != null && suiteContext.getAuthServerInfo() != null && suiteContext.getAuthServerInfo().isQuarkus()) {
return ExecutionDecision.dontExecute("@EnableVault with Elytron credential store provider not supported on Quarkus, skipping");
}
}
} }
return ExecutionDecision.execute(); return ExecutionDecision.execute();
} }

View file

@ -33,53 +33,18 @@ public @interface EnableVault {
enum PROVIDER_ID { enum PROVIDER_ID {
PLAINTEXT("files-plaintext", PLAINTEXT("files-plaintext"),
new String[] { KEYSTORE("files-keystore");
"/subsystem=keycloak-server/spi=vault/provider=files-plaintext/:add(enabled=true, " +
"properties={dir => \"${jboss.home.dir}/standalone/configuration/vault\"})"},
new String[] {}),
ELYTRON_CS_KEYSTORE("elytron-cs-keystore",
new String[] {
// create and populate an elytron credential store on the fly.
"/subsystem=elytron/credential-store=test-cred-store:add(location=standalone/configuration/vault/cred-store.jceks, create=true," +
"relative-to=jboss.home.dir, credential-reference={clear-text => \"secretpwd1!\"})",
"/subsystem=elytron/credential-store=test-cred-store:add-alias(alias=master_smtp__key, secret-value=secure_master_smtp_secret)",
"/subsystem=elytron/credential-store=test-cred-store:add-alias(alias=test_smtp__key, secret-value=secure_test_smtp_secret)",
// create the elytron-cs-keystore provider (using the masked form of the credential store password.
"/subsystem=keycloak-server/spi=vault/provider=elytron-cs-keystore/:add(enabled=true, " +
"properties={location => \"${jboss.home.dir}/standalone/configuration/vault/cred-store.jceks\", " +
"secret => \"MASK-2RukbhkyMOXq1WzXkcUcuK;abcd9876;321\", keyStoreType => \"JCEKS\"})"},
new String[] {
// remove the aliases from the credential store.
"/subsystem=elytron/credential-store=test-cred-store:remove-alias(alias=test_smtp__key)",
"/subsystem=elytron/credential-store=test-cred-store:remove-alias(alias=master_smtp__key)",
// remove the elytron credential store.
"/subsystem=elytron/credential-store=test-cred-store:remove"
});
final String name; final String name;
final String[] cliInstallationCommands;
final String[] cliRemovalCommands;
PROVIDER_ID(final String name, final String[] cliInstallationCommands, final String[] cliRemovalCommands) { PROVIDER_ID(final String name) {
this.name = name; this.name = name;
this.cliInstallationCommands = cliInstallationCommands;
this.cliRemovalCommands = cliRemovalCommands;
} }
public String getName() { public String getName() {
return this.name; return this.name;
} }
public String[] getCliInstallationCommands() {
return this.cliInstallationCommands;
}
public String[] getCliRemovalCommands() {
return this.cliRemovalCommands;
}
}; };
PROVIDER_ID providerId() default PROVIDER_ID.PLAINTEXT; PROVIDER_ID providerId() default PROVIDER_ID.PLAINTEXT;

View file

@ -20,6 +20,10 @@ package org.keycloak.testsuite.util;
import org.keycloak.testsuite.arquillian.ContainerInfo; import org.keycloak.testsuite.arquillian.ContainerInfo;
import org.keycloak.testsuite.arquillian.SuiteContext; import org.keycloak.testsuite.arquillian.SuiteContext;
import org.keycloak.testsuite.arquillian.annotation.EnableVault; import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.arquillian.containers.AbstractQuarkusDeployableContainer;
import java.util.ArrayList;
import java.util.List;
/** /**
* @author mhajas * @author mhajas
@ -32,6 +36,20 @@ public class VaultUtils {
if (serverInfo.isUndertow()) { if (serverInfo.isUndertow()) {
System.setProperty("keycloak.vault." + provider.getName() + ".provider.enabled", "true"); System.setProperty("keycloak.vault." + provider.getName() + ".provider.enabled", "true");
} }
else if (serverInfo.isQuarkus()) {
AbstractQuarkusDeployableContainer container = (AbstractQuarkusDeployableContainer)suiteContext.getAuthServerInfo().getArquillianContainer().getDeployableContainer();
List<String> additionalArgs = new ArrayList<>();
if (provider == EnableVault.PROVIDER_ID.KEYSTORE) {
additionalArgs.add("--vault=keystore");
additionalArgs.add("--vault-file=../secrets/myks");
additionalArgs.add("--vault-pass=keystorepassword");
} else if (provider == EnableVault.PROVIDER_ID.PLAINTEXT) {
additionalArgs.add("--vault=file");
additionalArgs.add("--vault-dir=../secrets");
}
container.setAdditionalBuildArgs(additionalArgs);
}
} }
public static void disableVault(SuiteContext suiteContext, EnableVault.PROVIDER_ID provider) { public static void disableVault(SuiteContext suiteContext, EnableVault.PROVIDER_ID provider) {

View file

@ -17,12 +17,11 @@
package org.keycloak.testsuite.vault; package org.keycloak.testsuite.vault;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert; import org.junit.Assert;
import org.junit.Test;
import org.keycloak.models.KeycloakSession; import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.RealmRepresentation; import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest; import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.runonserver.RunOnServer; import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.utils.io.IOUtil; import org.keycloak.testsuite.utils.io.IOUtil;
import org.keycloak.vault.VaultStringSecret; import org.keycloak.vault.VaultStringSecret;
@ -37,22 +36,14 @@ import java.util.Optional;
* *
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a> * @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/ */
@EnableVault
public class KeycloakVaultTest extends AbstractKeycloakTest { public abstract class AbstractKeycloakVaultTest extends AbstractKeycloakTest {
@Override @Override
public void addTestRealms(List<RealmRepresentation> testRealms) { public void addTestRealms(List<RealmRepresentation> testRealms) {
testRealms.add(IOUtil.loadRealm("/testrealm.json")); testRealms.add(IOUtil.loadRealm("/testrealm.json"));
} }
@Test
public void testKeycloakVault() throws Exception {
// run the test in two different realms to test the provider's ability to retrieve secrets with the same key in different realms.
testingClient.server().run(new KeycloakVaultServerTest("${vault.smtp_key}", "secure_master_smtp_secret"));
testingClient.server("test").run(new KeycloakVaultServerTest("${vault.smtp_key}", "secure_test_smtp_secret"));
}
static class KeycloakVaultServerTest implements RunOnServer { static class KeycloakVaultServerTest implements RunOnServer {
private String testKey; private String testKey;
@ -65,31 +56,30 @@ public class KeycloakVaultTest extends AbstractKeycloakTest {
@Override @Override
public void run(KeycloakSession session) { public void run(KeycloakSession session) {
VaultTranscriber transcriber = session.vault(); VaultTranscriber transcriber = getVaultTranscriber(session);
Assert.assertNotNull(transcriber); // obtain an existing secret from the vault.
Optional<String> optional = getSecret(transcriber, testKey);
// use the transcriber to obtain a secret from the vault. Assert.assertTrue(optional.isPresent());
try (VaultStringSecret secret = transcriber.getStringSecret(testKey)){ Assert.assertEquals(expectedSecret, optional.get());
Optional<String> optional = secret.get();
Assert.assertTrue(optional.isPresent());
String secretString = optional.get();
Assert.assertEquals(expectedSecret, secretString);
}
// try obtaining a secret using a key that does not exist in the vault. // try obtaining a secret using a key that does not exist in the vault.
String invalidEntry = "${vault.invalid_entry}"; optional = getSecret(transcriber, "${vault.invalid_entry}");
try (VaultStringSecret secret = transcriber.getStringSecret(invalidEntry)) { Assert.assertFalse(optional.isPresent());
Optional<String> optional = secret.get();
Assert.assertFalse(optional.isPresent());
}
// invoke the transcriber using a string that is not a vault expression. // invoke the transcriber using a string that is not a vault expression.
try (VaultStringSecret secret = transcriber.getStringSecret("mysecret")) { optional = getSecret(transcriber, "mysecret");
Optional<String> optional = secret.get(); Assert.assertTrue(optional.isPresent());
Assert.assertTrue(optional.isPresent()); Assert.assertEquals("mysecret", optional.get());
String secretString = optional.get(); }
Assert.assertEquals("mysecret", secretString);
} private Optional<String> getSecret(VaultTranscriber transcriber, String testKey) {
VaultStringSecret secret = transcriber.getStringSecret(testKey);
return secret.get();
} }
} }
@NotNull
private static VaultTranscriber getVaultTranscriber(KeycloakSession session) throws RuntimeException {
return session.vault();
}
} }

View file

@ -17,6 +17,7 @@
package org.keycloak.testsuite.vault; package org.keycloak.testsuite.vault;
import org.junit.Test;
import org.keycloak.testsuite.arquillian.annotation.EnableVault; import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.vault.VaultTranscriber; import org.keycloak.vault.VaultTranscriber;
@ -24,11 +25,17 @@ import org.keycloak.vault.VaultTranscriber;
* Tests the usage of the {@link VaultTranscriber} on the server side. The tests attempt to obtain the transcriber from * Tests the usage of the {@link VaultTranscriber} on the server side. The tests attempt to obtain the transcriber from
* the session and then use it to obtain secrets from the configured provider. * the session and then use it to obtain secrets from the configured provider.
* <p/> * <p/>
* This test differs from the superclass in that it uses the {@code elytron-cs-keystore} provider to obtain secrets. * This test differs from the abstract class in that it uses the {@code files-keystore} provider to obtain secrets.
* *
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a> * @author <a href="mailto:pzaoral@redhat.com">Peter Zaoral</a>
*/ */
@EnableVault(providerId = EnableVault.PROVIDER_ID.ELYTRON_CS_KEYSTORE) @EnableVault(providerId = EnableVault.PROVIDER_ID.KEYSTORE)
public class KeycloakElytronCSVaultTest extends KeycloakVaultTest { public class KeycloakKeystoreVaultTest extends AbstractKeycloakVaultTest {
// run the same tests of the superclass using the elytron credential store provider.
@Test
public void testKeycloakKeystoreVault() {
// run the test in two different realms to test the provider's ability to retrieve secrets with the same key in different realms.
testingClient.server().run(new KeycloakVaultServerTest("${vault.smtp_key}", "secure_master_smtp_secret"));
testingClient.server("test").run(new KeycloakVaultServerTest("${vault.smtp_key}", "secure_test_smtp_secret"));
}
} }

View file

@ -0,0 +1,42 @@
/*
* Copyright 2019 Red Hat, Inc. and/or its affiliates
* and other contributors as indicated by the @author tags.
*
* 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
*
* http://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.keycloak.testsuite.vault;
import org.junit.Test;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.vault.VaultTranscriber;
/**
* Tests the usage of the {@link VaultTranscriber} on the server side. The tests attempt to obtain the transcriber from
* the session and then use it to obtain secrets from the configured provider.
* <p/>
* This test differs from the abstract class in that it uses the {@code files-plaintext} provider to obtain secrets.
*
* @author <a href="mailto:pzaoral@redhat.com">Peter Zaoral</a>
*/
@EnableVault(providerId = EnableVault.PROVIDER_ID.PLAINTEXT)
public class KeycloakPlaintextVaultTest extends AbstractKeycloakVaultTest {
@Test
public void testKeycloakPlaintextVault() {
// run the test in two different realms to test the provider's ability to retrieve secrets with the same key in different realms.
testingClient.server().run(new KeycloakVaultServerTest("${vault.smtp_key}", "secure_master_smtp_secret"));
testingClient.server("test").run(new KeycloakVaultServerTest("${vault.smtp_key}", "secure_test_smtp_secret"));
}
}

View file

@ -368,6 +368,12 @@
"files-plaintext": { "files-plaintext": {
"dir": "target/dependency/vault", "dir": "target/dependency/vault",
"enabled": "${keycloak.vault.files-plaintext.provider.enabled:false}" "enabled": "${keycloak.vault.files-plaintext.provider.enabled:false}"
},
"files-keystore": {
"file": "target/dependency/vault/myks",
"pass": "keystorepassword",
"type": "PKCS12",
"enabled": "${keycloak.vault.files-keystore.provider.enabled:false}"
} }
}, },