Contents
Overview
HashiCorp Vault is a commonly used Vault in many microservices. The APIs are REST-based and Helidon implements them using reactive client.
Maven Coordinates
To enable HashiCorp Vault add the following dependency to your project’s pom.xml (see Managing Dependencies).
<dependency>
<groupId>io.helidon.integrations.vault</groupId>
<artifactId>helidon-integrations-vault</artifactId>
</dependency>The following is a list of maven coordinates of all Vault modules available:
<dependency>
<groupId>io.helidon.integrations.vault.auths</groupId>
<artifactId>helidon-integrations-vault-auths-token</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.auths</groupId>
<artifactId>helidon-integrations-vault-auths-approle</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.auths</groupId>
<artifactId>helidon-integrations-vault-auths-k8s</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.secrets</groupId>
<artifactId>helidon-integrations-vault-secrets-kv1</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.secrets</groupId>
<artifactId>helidon-integrations-vault-secrets-kv2</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.secrets</groupId>
<artifactId>helidon-integrations-vault-secrets-cubbyhole</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.secrets</groupId>
<artifactId>helidon-integrations-vault-secrets-transit</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.secrets</groupId>
<artifactId>helidon-integrations-vault-secrets-database</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.integrations.vault.sys</groupId>
<artifactId>helidon-integrations-vault-sys</artifactId>
</dependency>Usage
Vault integration supports the following:
Secret Engines: Key/Value version 2, Key/Value version 1, Cubbyhole, PKI, Transit, Database
Authentication Methods: Token, Kubernetes (k8s), AppRole
Other Sys Operations and Configurations
Each of these features is implemented as a separate module, with the Vault class binding them together. Code to set up Vault and obtain a specific secret engine:
Vault vault = Vault.builder()
.config(config.get("vault"))
.build();
Kv2SecretsRx secrets = vault.secrets(Kv2SecretsRx.ENGINE);Similar code can be used for any secret engine available:
Kv2SecretsRx - Key/Value Version 2 Secrets (versioned secrets, default)
Kv1SecretsRx - Key/Value Version 1 Secrets (unversioned secrets, legacy)
CubbyholeSecretsRx - Cubbyhole secrets (token bound secrets)
DbSecretsRx - Database secrets (for generating temporary DB credentials)
PkiSecretsRx - PKI secrets (for generating keys and X.509 certificates)
TransitSecretsRx - Transit operations (encryption, signatures, HMAC)
In addition to these features, Vault itself can be authenticated as follows:
Token authentication - token is configured when connecting to Vault
vault: address: "http://localhost:8200" token: "my-token"
AppRole authentication - AppRole ID and secret ID are configured, integration exchanges these for a temporary token that is used to connect to Vault
vault:
auth:
app-role:
role-id: "app-role-id"
secret-id: app-role-secret-idK8s authentication - the k8s JWT token is discovered on current node and used to obtain a temporary token that is used to connect to Vault
vault:
auth:
k8s:
token-role: "my-role" - The token role must be configured in Vault Minimal configuration to connect to Vault:
Code to get the Sys operations of Vault:
SysRx sys = vault.sys(SysRx.API);Extensibility
New secret engines and authentication methods can be implemented quite easily, as the integration is based on service providers (using ServiceLoader). This gives us (or you, as the users) the option to add new secret engines and/or authentication methods without adding a plethora of methods to the Vault class.
See the following SPIs:
io.helidon.integrations.vault.spi.AuthMethodProvider
io.helidon.integrations.vault.spi.SecretsEngineProvider
io.helidon.integrations.vault.spi.SysProvider
io.helidon.integrations.vault.spi.VaultAuth
io.helidon.integrations.vault.spi.InjectionProviderExamples
The following example shows usage of Vault to encrypt a secret.
Usage with WebServer
Configure the Vault object using token base configuration:
Config config = buildConfig();
Vault tokenVault = Vault.builder()
.config(config.get("vault.token"))
.updateWebClient(it -> it.connectTimeout(5, TimeUnit.SECONDS)
.readTimeout(5, TimeUnit.SECONDS))
.build();Then WebService has to be configured with endpoints routing registered:
SysRx sys = tokenVault.sys(SysRx.API);
WebServer webServer = WebServer.builder()
.config(config.get("server"))
.routing(Routing.builder()
.register("/cubbyhole", new CubbyholeService(sys, tokenVault.secrets(CubbyholeSecretsRx.ENGINE)))
.register("/kv1", new Kv1Service(sys, tokenVault.secrets(Kv1SecretsRx.ENGINE)))
.register("/kv2", new Kv2Service(sys, tokenVault.secrets(Kv2SecretsRx.ENGINE)))
.register("/transit", new TransitService(sys, tokenVault.secrets(TransitSecretsRx.ENGINE))))
.build()
.start()
.await();AppRole-based and Kubernetes authentications are available.
Cubbyhole secrets
Cubbyhole secrets engine operations:
@Override
public void update(Routing.Rules rules) {
rules.get("/create", this::createSecrets)
.get("/secrets/{path:.*}", this::getSecret);
}
private void createSecrets(ServerRequest req, ServerResponse res) {
secrets.create("first/secret", Map.of("key", "secretValue"))
.thenAccept(ignored -> res.send("Created secret on path /first/secret"))
.exceptionally(res::send);
}
private void getSecret(ServerRequest req, ServerResponse res) {
String path = req.path().param("path");
secrets.get(path)
.thenAccept(secret -> {
if (secret.isPresent()) {
// using toString so we do not need to depend on JSON-B
res.send(secret.get().values().toString());
} else {
res.status(Http.Status.NOT_FOUND_404);
res.send();
}
})
.exceptionally(res::send);
}- Create a secret from request entity.
- Get the secret on a specified path.
KV1 Secrets
Key/Value version 1 secrets engine operations:
@Override
public void update(Routing.Rules rules) {
rules.get("/enable", this::enableEngine)
.get("/create", this::createSecrets)
.get("/secrets/{path:.*}", this::getSecret)
.delete("/secrets/{path:.*}", this::deleteSecret)
.get("/disable", this::disableEngine);
}
private void disableEngine(ServerRequest req, ServerResponse res) {
sys.disableEngine(Kv1SecretsRx.ENGINE)
.thenAccept(ignored -> res.send("KV1 Secret engine disabled"))
.exceptionally(res::send);
}
private void enableEngine(ServerRequest req, ServerResponse res) {
sys.enableEngine(Kv1SecretsRx.ENGINE)
.thenAccept(ignored -> res.send("KV1 Secret engine enabled"))
.exceptionally(res::send);
}
private void createSecrets(ServerRequest req, ServerResponse res) {
secrets.create("first/secret", Map.of("key", "secretValue"))
.thenAccept(ignored -> res.send("Created secret on path /first/secret"))
.exceptionally(res::send);
}
private void deleteSecret(ServerRequest req, ServerResponse res) {
String path = req.path().param("path");
secrets.delete(path)
.thenAccept(ignored -> res.send("Deleted secret on path " + path));
}
private void getSecret(ServerRequest req, ServerResponse res) {
String path = req.path().param("path");
secrets.get(path)
.thenAccept(secret -> {
if (secret.isPresent()) {
// using toString so we do not need to depend on JSON-B
res.send(secret.get().values().toString());
} else {
res.status(Http.Status.NOT_FOUND_404);
res.send();
}
})
.exceptionally(res::send);
}- Disable the secrets engine on the default path.
- Enable the secrets engine on the default path.
- Create a secret from request entity.
- Delete the secret on a specified path.
- Get the secret on a specified path.
KV2 Secrets
Key/Value version 2 secrets engine operations:
@Override
public void update(Routing.Rules rules) {
rules.get("/create", this::createSecrets)
.get("/secrets/{path:.*}", this::getSecret)
.delete("/secrets/{path:.*}", this::deleteSecret);
}
private void createSecrets(ServerRequest req, ServerResponse res) {
secrets.create("first/secret", Map.of("key", "secretValue"))
.thenAccept(ignored -> res.send("Created secret on path /first/secret"))
.exceptionally(res::send);
}
private void deleteSecret(ServerRequest req, ServerResponse res) {
String path = req.path().param("path");
secrets.deleteAll(path)
.thenAccept(ignored -> res.send("Deleted secret on path " + path));
}
private void getSecret(ServerRequest req, ServerResponse res) {
String path = req.path().param("path");
secrets.get(path)
.thenAccept(secret -> {
if (secret.isPresent()) {
// using toString so we do not need to depend on JSON-B
Kv2Secret kv2Secret = secret.get();
res.send("Version " + kv2Secret.metadata().version() + ", secret: " + kv2Secret.values().toString());
} else {
res.status(Http.Status.NOT_FOUND_404);
res.send();
}
})
.exceptionally(res::send);
}- Create a secret from request entity.
- Delete the secret on a specified path.
- Get the secret on a specified path.
Transit secrets
Transit secrets engine operations:
@Override
public void update(Routing.Rules rules) {
rules.get("/enable", this::enableEngine)
.get("/keys", this::createKeys)
.delete("/keys", this::deleteKeys)
.get("/batch", this::batch)
.get("/encrypt/{text:.*}", this::encryptSecret)
.get("/decrypt/{text:.*}", this::decryptSecret)
.get("/sign", this::sign)
.get("/hmac", this::hmac)
.get("/verify/sign/{text:.*}", this::verify)
.get("/verify/hmac/{text:.*}", this::verifyHmac)
.get("/disable", this::disableEngine);
}
private void enableEngine(ServerRequest req, ServerResponse res) {
sys.enableEngine(TransitSecretsRx.ENGINE)
.thenAccept(ignored -> res.send("Transit Secret engine enabled"))
.exceptionally(res::send);
}
private void disableEngine(ServerRequest req, ServerResponse res) {
sys.disableEngine(TransitSecretsRx.ENGINE)
.thenAccept(ignored -> res.send("Transit Secret engine disabled"))
.exceptionally(res::send);
}
private void createKeys(ServerRequest req, ServerResponse res) {
CreateKey.Request request = CreateKey.Request.builder()
.name(ENCRYPTION_KEY);
secrets.createKey(request)
.flatMapSingle(ignored -> secrets.createKey(CreateKey.Request.builder()
.name(SIGNATURE_KEY)
.type("rsa-2048")))
.forSingle(ignored -> res.send("Created keys"))
.exceptionally(res::send);
}
private void deleteKeys(ServerRequest req, ServerResponse res) {
secrets.updateKeyConfig(UpdateKeyConfig.Request.builder()
.name(ENCRYPTION_KEY)
.allowDeletion(true))
.peek(ignored -> System.out.println("Updated key config"))
.flatMapSingle(ignored -> secrets.deleteKey(DeleteKey.Request.create(ENCRYPTION_KEY)))
.forSingle(ignored -> res.send("Deleted key."))
.exceptionally(res::send);
}
private void encryptSecret(ServerRequest req, ServerResponse res) {
String secret = req.path().param("text");
secrets.encrypt(Encrypt.Request.builder()
.encryptionKeyName(ENCRYPTION_KEY)
.data(Base64Value.create(secret)))
.forSingle(response -> res.send(response.encrypted().cipherText()))
.exceptionally(res::send);
}
private void decryptSecret(ServerRequest req, ServerResponse res) {
String encrypted = req.path().param("text");
secrets.decrypt(Decrypt.Request.builder()
.encryptionKeyName(ENCRYPTION_KEY)
.cipherText(encrypted))
.forSingle(response -> res.send(String.valueOf(response.decrypted().toDecodedString())))
.exceptionally(res::send);
}
private void hmac(ServerRequest req, ServerResponse res) {
secrets.hmac(Hmac.Request.builder()
.hmacKeyName(ENCRYPTION_KEY)
.data(SECRET_STRING))
.forSingle(response -> res.send(response.hmac()))
.exceptionally(res::send);
}
private void sign(ServerRequest req, ServerResponse res) {
secrets.sign(Sign.Request.builder()
.signatureKeyName(SIGNATURE_KEY)
.data(SECRET_STRING))
.forSingle(response -> res.send(response.signature()))
.exceptionally(res::send);
}
private void verifyHmac(ServerRequest req, ServerResponse res) {
String hmac = req.path().param("text");
secrets.verify(Verify.Request.builder()
.digestKeyName(ENCRYPTION_KEY)
.data(SECRET_STRING)
.hmac(hmac))
.forSingle(response -> res.send("Valid: " + response.isValid()))
.exceptionally(res::send);
}
private void verify(ServerRequest req, ServerResponse res) {
String signature = req.path().param("text");
secrets.verify(Verify.Request.builder()
.digestKeyName(SIGNATURE_KEY)
.data(SECRET_STRING)
.signature(signature))
.forSingle(response -> res.send("Valid: " + response.isValid()))
.exceptionally(res::send);
}- Enable the secrets engine on the default path.
- Disable the secrets engine on the default path.
- Create the encryption and signature keys.
- Delete the encryption and signature keys.
- Encrypt a secret.
- Decrypt a secret.
- Create an HMAC for text.
- Create a signature for text.
- Verify HMAC.
- Verify signature.
Authentication with Kubernetes
In order to use Kubernetes authentication:
class K8sExample {
private static final String SECRET_PATH = "k8s/example/secret";
private static final String POLICY_NAME = "k8s_policy";
private final Vault tokenVault;
private final String k8sAddress;
private final Config config;
private final SysRx sys;
private Vault k8sVault;
K8sExample(Vault tokenVault, Config config) {
this.tokenVault = tokenVault;
this.sys = tokenVault.sys(SysRx.API);
this.k8sAddress = config.get("cluster-address").asString().get();
this.config = config;
}
public Single<String> run() {
/*
The following tasks must be run before we authenticate
*/
return enableK8sAuth()
// Now we can login using k8s - must run within a k8s cluster (or you need the k8s configuration files locally)
.flatMapSingle(ignored -> workWithSecrets())
// Now back to token based Vault, as we will clean up
.flatMapSingle(ignored -> disableK8sAuth())
.map(ignored -> "k8s example finished successfully.");
}
private Single<ApiResponse> workWithSecrets() {
Kv2SecretsRx secrets = k8sVault.secrets(Kv2SecretsRx.ENGINE);
return secrets.create(SECRET_PATH, Map.of("secret-key", "secretValue",
"secret-user", "username"))
.flatMapSingle(ignored -> secrets.get(SECRET_PATH))
.peek(secret -> {
if (secret.isPresent()) {
Kv2Secret kv2Secret = secret.get();
System.out.println("k8s first secret: " + kv2Secret.value("secret-key"));
System.out.println("k8s second secret: " + kv2Secret.value("secret-user"));
} else {
System.out.println("k8s secret not found");
}
}).flatMapSingle(ignored -> secrets.deleteAll(SECRET_PATH));
}
private Single<ApiResponse> disableK8sAuth() {
return sys.deletePolicy(POLICY_NAME)
.flatMapSingle(ignored -> sys.disableAuth(K8sAuthRx.AUTH_METHOD.defaultPath()));
}
private Single<ApiResponse> enableK8sAuth() {
// enable the method
return sys.enableAuth(K8sAuthRx.AUTH_METHOD)
// add policy
.flatMapSingle(ignored -> sys.createPolicy(POLICY_NAME, VaultPolicy.POLICY))
.flatMapSingle(ignored -> tokenVault.auth(K8sAuthRx.AUTH_METHOD)
.configure(ConfigureK8s.Request.builder()
.address(k8sAddress)))
.flatMapSingle(ignored -> tokenVault.auth(K8sAuthRx.AUTH_METHOD)
// this must be the same role name as is defined in application.yaml
.createRole(CreateRole.Request.builder()
.roleName("my-role")
.addBoundServiceAccountName("*")
.addBoundServiceAccountNamespace("default")
.addTokenPolicy(POLICY_NAME)))
.peek(ignored -> k8sVault = Vault.create(config))
.map(Function.identity());
}
}- Run the Kubernetes Authentication by enabling it.
- Create Kubernetes secrets.
- Disable Kubernetes authentication if needed.
- Function used to enable Kubernetes authentication.
Local testing
Vault is available as a docker image, so to test locally, you can simply:
docker run -e VAULT_DEV_ROOT_TOKEN_ID=my-token -d --name=vault -p8200:8200 vaultThis will create a Vault docker image, run it in background and open it on localhost:8200 with a custom root token my-token, using name vault. This is of course only suitable for local testing, as the root token has too many rights, but it can be easily used with the examples below.