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
|
== 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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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()
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.FilesPlainTextVaultProviderFactory
|
||||||
|
org.keycloak.quarkus.runtime.vault.FilesKeystoreVaultProviderFactory
|
||||||
|
|
|
@ -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");
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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:
|
||||||
|
|
||||||
|
|
|
@ -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
|
# 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
|
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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();
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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"));
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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": {
|
"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}"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
Binary file not shown.
Loading…
Reference in a new issue