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:
parent
b8b56ece65
commit
72b238fb48
31 changed files with 483 additions and 112 deletions
|
@ -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:
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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()
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
org.keycloak.quarkus.runtime.vault.FilesPlainTextVaultProviderFactory
|
||||
org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -1 +1,2 @@
|
|||
org.keycloak.vault.FilesPlainTextVaultProviderFactory
|
||||
org.keycloak.vault.FilesPlainTextVaultProviderFactory
|
||||
org.keycloak.vault.FilesKeystoreVaultProviderFactory
|
|
@ -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"));
|
||||
}
|
||||
}
|
BIN
services/src/test/resources/org/keycloak/vault/myks
Normal file
BIN
services/src/test/resources/org/keycloak/vault/myks
Normal file
Binary file not shown.
BIN
services/src/test/resources/org/keycloak/vault/myks.jceks
Normal file
BIN
services/src/test/resources/org/keycloak/vault/myks.jceks
Normal file
Binary file not shown.
Binary file not shown.
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
}
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
|
@ -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}"
|
||||
}
|
||||
},
|
||||
|
||||
|
|
Binary file not shown.
Loading…
Reference in a new issue