Quarkus - Working with HashiCorp Vault
mounting a map of properties stored into theVault kv secret engine as an Eclipse MicroProfile config source
fetch credentials from Vault when configuring an Agroal datasource
access the Vault kv secret engine programmatically
Under the hood, the Quarkus Vault extension takes care of authentication when negotiating a client Vault token plusany transparent token or lease renewals according to ttl and max-ttl.
To complete this guide, you need:
roughly 20 minutes
an IDE
JDK 1.8+ installed with configured appropriately
Apache Maven 3.5.3+
Docker installed
Starting Vault
Let’s start Vault in development mode:
You can check that vault is running with:
docker logs dev-vault
You should see:
==> Vault server configuration:
Api Address: http://0.0.0.0:8200
Cgo: disabled
Cluster Address: https://0.0.0.0:8201
Listener 1: tcp (addr: "0.0.0.0:8200", cluster address: "0.0.0.0:8201", max_request_duration: "1m30s", max_request_size: "33554432", tls: "disabled")
Log Level: info
Mlock: supported: true, enabled: false
Storage: inmem
Version: Vault v1.2.2
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
and starts unsealed with a single unseal key. The root token is already
authenticated to the CLI, so you can immediately begin using Vault.
You may need to set the following environment variable:
$ export VAULT_ADDR='http://0.0.0.0:8200'
The unseal key and root token are displayed below in case you want to
seal/unseal the Vault or re-authenticate.
Unseal Key: 0lZ2/vzpa92pH8gersSn2h9b5tmzd4m5sqIdMC/4PDs=
Root Token: s.5VUS8pte13RqekCB2fmMT3u2
Development mode should NOT be used in production installations!
==> Vault server started! Log data will stream in below:
In development mode, Vault gets configured with several options that makes it convenient:
Vault is already initialized with one key share (whereas in normal mode this has to be done explicitly and thenumber of key shares is 5 by default)
the unseal key and the root token are displayed in the logs (please write down the root token, we will need itin the following step)
Vault is unsealed
in-memory storage
TLS is disabled
a kv secret engine v2 is mounted at
secret/
First open a shell inside the vault container:
docker exec -it dev-vault sh
Set the VAULT_TOKEN
with the value that was printed in the logs:
export VAULT_TOKEN=s.5VUS8pte13RqekCB2fmMT3u2
You can check Vault’s status using the CLI command vault status
:
Key Value
--- -----
Seal Type shamir
Initialized true
Sealed false
Total Shares 1
Threshold 1
Version 1.2.2
Cluster Name vault-cluster-b07e80d8
Cluster ID 55bd74b6-eaaf-3862-f7ce-3473ab86c57f
HA Enabled false
For simplicity reasons, we are going to use a kv secret engine version 1 (which is the default) mounted at pathsecret
instead of the pre-configured kv version 2 available in dev mode. So let’s disable the current kv engine,and recreate a new one:
# this will disable the current kv version 2 secret engine mounted at path 'secret'
vault secrets disable secret
vault secrets enable -path=secret kv
Now let’s add a secret configuration for our application:
vault kv put secret/myapps/vault-quickstart/config a-private-key=123456
We have defined a path secret/myapps/vault-quickstart
in Vault that we need to give access to from the Quarkus application.
Create a policy that gives read access to secret/myapps/vault-quickstart
and subpaths:
cat <<EOF | vault policy write vault-quickstart-policy -
path "secret/myapps/vault-quickstart/*" {
capabilities = ["read"]
}
EOF
And finally, let’s enable the userpass auth secret engine, and create user bob
with access to the vault-quickstart-policy
:
vault auth enable userpass
vault write auth/userpass/users/bob password=sinclair policies=vault-quickstart-policy
To check that the configuration is correct, try logging in:
You should see:
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.s93BVzJPzBiIGuYJHBTkG8Uw
token_accessor OKNipTAgxWbxsrjixASNiwdY
token_duration 768h
token_renewable true
token_policies ["default" "vault-quickstart-policy"]
identity_policies []
policies ["default" "vault-quickstart-policy"]
token_meta_username bob
Now set VAULT_TOKEN
to the above (instead of the root token), and try reading the vault-quickstart secret config:
export VAULT_TOKEN=s.s93BVzJPzBiIGuYJHBTkG8Uw
vault kv get secret/myapps/vault-quickstart/config
You should see:
======== Data ========
Key Value
--- -----
a-private-key 123456
mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=vault-quickstart \
-DclassName="org.acme.quickstart.GreetingResource" \
-Dpath="/hello" \
-Dextensions="vault,hibernate-orm,jdbc-postgresql"
cd vault-quickstart
Configure access to vault from the application.properties
:
# vault url
quarkus.vault.url=http://localhost:8200
# vault authentication
quarkus.vault.authentication.userpass.username=bob
quarkus.vault.authentication.userpass.password=sinclair
# path within the kv secret engine where is located the vault-quickstart secret configuration
quarkus.vault.secret-config-kv-path=myapps/vault-quickstart/config
This should mount whatever keys are stored in secret/myapps/vault-quickstart
as MicroProfile config properties.
Let’s verify that by adding a new endpoint in GreetingResource:
@Path("/hello")
public class GreetingResource {
@ConfigProperty(name = "a-private-key")
String privateKey;
...
@GET
@Path("/private-key")
@Produces(MediaType.TEXT_PLAIN)
public String privateKey() {
return privateKey;
}
}
Now compile the application and run it:
./mvnw clean install
java -jar target/vault-quickstart-1.0-SNAPSHOT-runner.jar
Finally test the new endpoint:
curl http://localhost:8080/hello/private-key
You should see:
123456
Fetching credentials from Vault with Hibernate ORM
Start a PostgreSQL database:
Create a simple service:
@ApplicationScoped
public class SantaClausService {
EntityManager em;
@Transactional
public List<Gift> getGifts() {
return (List<Gift>) em.createQuery("select g from Gift g").getResultList();
}
}
@Entity
public class Gift {
private Long id;
private String name;
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator="giftSeq")
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
Finally, add a new endpoint in GreetingResource
:
@Inject
SantaClausService santaClausService;
@GET
@Produces(MediaType.TEXT_PLAIN)
public int geGiftCount() {
return santaClausService.getGifts().size();
}
Create a new path in Vault where the database password will be added:
# set the root token again
export VAULT_TOKEN={root-token}
vault kv put secret/myapps/vault-quickstart/db password=connor
Since we allowed read access on secret/myapps/vault-quickstart/
subpaths in the policy, there is nothing elsewe have to do to allow the application to read it.
When fetching credentials from Vault that are intended to be used by the Agroal connection pool, we needfirst to create a named Vault credentials provider in the application.properties:
quarkus.vault.credentials-provider.mydatabase.kv-path=myapps/vault-quickstart/db
This defines a credentials provider mydatabase
that will fetch the password from key password
at path myapps/vault-quickstart/db
.
The credentials provider can now be used in the datasource configuration, in place of the password
property:
# configure your datasource
quarkus.datasource.url = jdbc:postgresql://localhost:5432/mydatabase
quarkus.datasource.driver = org.postgresql.Driver
quarkus.datasource.username = sarah
#quarkus.datasource.password = connor
quarkus.datasource.credentials-provider = mydatabase
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation=drop-and-create
Restart the application after rebuilding it, and test it with the new endpoint:
curl http://localhost:8080/hello/gift-count
You should see:
0
Sometimes secrets are retrieved from an arbitrary path that is known only at runtime through an applicationspecific property, or a method argument for instance.In that case it is possible to inject a Quarkus VaultKVSecretEngine, and retrieve secrets programmatically.
For instance, in GreetingResource
, add:
@Inject
VaultKVSecretEngine kvSecretEngine;
...
@GET
@Path("/secrets/{vault-path}")
@Produces(MediaType.TEXT_PLAIN)
public String getSecrets(@PathParam("vault-path") String vaultPath) {
return kvSecretEngine.readSecret("myapps/vault-quickstart/" + vaultPath).toString();
}
Restart the application after rebuilding it, and test it with the new endpoint:
You should see:
{password=connor}
TLS
In production mode, TLS should be activated between the Quarkus application and Vault to prevent man-in-the-middle attacks.
There are several ways to configure the Quarkus application:
using property
quarkus.vault.tls.ca-cert
, which should refer to a pem encoded file.
If quarkus.vault.tls.ca-cert
is not set and the Quarkus application is using the Kubernetes authentication,TLS will be active and use the CA certificate bundle located in /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
.If you want to disable this behavior (for instance when using a trust store), you need to set it explicitly using:quarkus.vault.tls.use-kubernetes-ca-cert=false
.
The last relevant property is quarkus.vault.tls.skip-verify
, which allows to communicate with Vault using TLS,but without checking the certificate authenticity. This may be convenient in development, but is stronglydiscouraged in production as it is not more secure than talking to Vault in plain HTTP.
As a general matter, you should consider reading the extensive Vault documentationand apply best practices.
The features exposed today through the Vault extension covers only a small fraction of what the product is capable of.Still, it addresses already some of the most common microservices scenarii (e.g. sensitive configuration and databasecredentials), which goes a long way towards creating secured applications.