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
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:
[source]
@ -11,7 +13,7 @@ ${vault.key}
----
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:

View file

@ -2,31 +2,41 @@
<#import "/templates/kc.adoc" as kc>
<@tmpl.guide
title="Using Kubernetes secrets"
summary="Learn how to use Kubernetes/OpenShift secrets in Keycloak"
title="Using a vault"
summary="Learn how to use and configure a vault in Keycloak"
priority=30
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
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 LDAP Bind Credential when using LDAP-based User Federation
* Obtain the OIDC identity providers Client Secret when integrating external identity providers
== Enabling the vault
Enable the file based vault by building Keycloak using the following build option:
== Enabling a vault
For enabling the file-based vault you need to build Keycloak first using the following build option:
<@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:
<@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:
[source, bash]
----
@ -46,6 +56,25 @@ sso__realm_ldap__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 setup

View file

@ -1,22 +1,49 @@
package org.keycloak.config;
import java.io.File;
import java.nio.file.Path;
public class VaultOptions {
public enum Provider {
file;
public enum VaultType {
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)
.description("Enables a vault provider.")
.buildTime(true)
.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)
.description("If set, secrets can be obtained by reading the content of files within the given directory.")
.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.RequestHostnameProviderFactory;
import org.keycloak.util.JsonSerialization;
import org.keycloak.vault.FilesKeystoreVaultProviderFactory;
import org.keycloak.vault.FilesPlainTextVaultProviderFactory;
import jakarta.persistence.Entity;
@ -176,6 +177,7 @@ class KeycloakProcessor {
DefaultHostnameProviderFactory.class,
FixedHostnameProviderFactory.class,
RequestHostnameProviderFactory.class,
FilesKeystoreVaultProviderFactory.class,
FilesPlainTextVaultProviderFactory.class,
BlacklistPasswordPolicyProviderFactory.class,
ClasspathThemeResourceProviderFactory.class,

View file

@ -17,6 +17,18 @@ final class VaultPropertyMappers {
fromOption(VaultOptions.VAULT_DIR)
.to("kc.spi-vault-file-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()
};
}

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.FilesKeystoreVaultProviderFactory

View file

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

View file

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

View file

@ -173,9 +173,12 @@ Proxy:
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
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:

View file

@ -236,9 +236,12 @@ Proxy:
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
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:

View file

@ -179,9 +179,12 @@ Proxy:
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
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:

View file

@ -242,9 +242,12 @@ Proxy:
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
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:

View file

@ -125,6 +125,9 @@ Vault:
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
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:

View file

@ -144,6 +144,9 @@ Vault:
--vault-dir <dir> If set, secrets can be obtained by reading the content of files within the
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:

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
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 (testContext.getTestClass().isAnnotationPresent(EnableVault.class)) {
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();
}

View file

@ -33,53 +33,18 @@ public @interface EnableVault {
enum PROVIDER_ID {
PLAINTEXT("files-plaintext",
new String[] {
"/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"
});
PLAINTEXT("files-plaintext"),
KEYSTORE("files-keystore");
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.cliInstallationCommands = cliInstallationCommands;
this.cliRemovalCommands = cliRemovalCommands;
}
public String getName() {
return this.name;
}
public String[] getCliInstallationCommands() {
return this.cliInstallationCommands;
}
public String[] getCliRemovalCommands() {
return this.cliRemovalCommands;
}
};
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.SuiteContext;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.arquillian.containers.AbstractQuarkusDeployableContainer;
import java.util.ArrayList;
import java.util.List;
/**
* @author mhajas
@ -32,6 +36,20 @@ public class VaultUtils {
if (serverInfo.isUndertow()) {
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) {

View file

@ -17,12 +17,11 @@
package org.keycloak.testsuite.vault;
import org.jetbrains.annotations.NotNull;
import org.junit.Assert;
import org.junit.Test;
import org.keycloak.models.KeycloakSession;
import org.keycloak.representations.idm.RealmRepresentation;
import org.keycloak.testsuite.AbstractKeycloakTest;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
import org.keycloak.testsuite.runonserver.RunOnServer;
import org.keycloak.testsuite.utils.io.IOUtil;
import org.keycloak.vault.VaultStringSecret;
@ -37,22 +36,14 @@ import java.util.Optional;
*
* @author <a href="mailto:sguilhen@redhat.com">Stefan Guilhen</a>
*/
@EnableVault
public class KeycloakVaultTest extends AbstractKeycloakTest {
public abstract class AbstractKeycloakVaultTest extends AbstractKeycloakTest {
@Override
public void addTestRealms(List<RealmRepresentation> testRealms) {
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 {
private String testKey;
@ -65,31 +56,30 @@ public class KeycloakVaultTest extends AbstractKeycloakTest {
@Override
public void run(KeycloakSession session) {
VaultTranscriber transcriber = session.vault();
Assert.assertNotNull(transcriber);
// use the transcriber to obtain a secret from the vault.
try (VaultStringSecret secret = transcriber.getStringSecret(testKey)){
Optional<String> optional = secret.get();
Assert.assertTrue(optional.isPresent());
String secretString = optional.get();
Assert.assertEquals(expectedSecret, secretString);
}
VaultTranscriber transcriber = getVaultTranscriber(session);
// obtain an existing secret from the vault.
Optional<String> optional = getSecret(transcriber, testKey);
Assert.assertTrue(optional.isPresent());
Assert.assertEquals(expectedSecret, optional.get());
// try obtaining a secret using a key that does not exist in the vault.
String invalidEntry = "${vault.invalid_entry}";
try (VaultStringSecret secret = transcriber.getStringSecret(invalidEntry)) {
Optional<String> optional = secret.get();
Assert.assertFalse(optional.isPresent());
}
optional = getSecret(transcriber, "${vault.invalid_entry}");
Assert.assertFalse(optional.isPresent());
// invoke the transcriber using a string that is not a vault expression.
try (VaultStringSecret secret = transcriber.getStringSecret("mysecret")) {
Optional<String> optional = secret.get();
Assert.assertTrue(optional.isPresent());
String secretString = optional.get();
Assert.assertEquals("mysecret", secretString);
}
optional = getSecret(transcriber, "mysecret");
Assert.assertTrue(optional.isPresent());
Assert.assertEquals("mysecret", optional.get());
}
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;
import org.junit.Test;
import org.keycloak.testsuite.arquillian.annotation.EnableVault;
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
* the session and then use it to obtain secrets from the configured provider.
* <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)
public class KeycloakElytronCSVaultTest extends KeycloakVaultTest {
// run the same tests of the superclass using the elytron credential store provider.
@EnableVault(providerId = EnableVault.PROVIDER_ID.KEYSTORE)
public class KeycloakKeystoreVaultTest extends AbstractKeycloakVaultTest {
@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": {
"dir": "target/dependency/vault",
"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}"
}
},