Quarkus - Working with HashiCorp Vault

HashiCorp Vault is a multi-purpose tool aiming at protecting sensitive data, such ascredentials, certificates, access tokens, encryption keys, …​ In the context of Quarkus,it is being used for 3 primary use cases:

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

Prerequisites

To complete this guide, you need:

  • roughly 20 minutes

  • an IDE

  • JDK 1.8+ installed with JAVA_HOME configured appropriately

  • Apache Maven 3.5.3+

  • Docker installed

Starting Vault

Let’s start Vault in development mode:

  1. docker run --rm --cap-add=IPC_LOCK -e VAULT_ADDR=http://localhost:8200 -p 8200:8200 -d --name=dev-vault vault:1.2.2

You can check that vault is running with:

  1. docker logs dev-vault

You should see:

  1. ==> Vault server configuration:
  2. Api Address: http://0.0.0.0:8200
  3. Cgo: disabled
  4. Cluster Address: https://0.0.0.0:8201
  5. 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")
  6. Log Level: info
  7. Mlock: supported: true, enabled: false
  8. Storage: inmem
  9. Version: Vault v1.2.2
  10. WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
  11. and starts unsealed with a single unseal key. The root token is already
  12. authenticated to the CLI, so you can immediately begin using Vault.
  13. You may need to set the following environment variable:
  14. $ export VAULT_ADDR='http://0.0.0.0:8200'
  15. The unseal key and root token are displayed below in case you want to
  16. seal/unseal the Vault or re-authenticate.
  17. Unseal Key: 0lZ2/vzpa92pH8gersSn2h9b5tmzd4m5sqIdMC/4PDs=
  18. Root Token: s.5VUS8pte13RqekCB2fmMT3u2
  19. Development mode should NOT be used in production installations!
  20. ==> 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/

In the following step, we are going to add a userpass authentication that we will use from the Quarkus application,to access a secret stored in the kv secret engine.

First open a shell inside the vault container:

  1. docker exec -it dev-vault sh

Set the VAULT_TOKEN with the value that was printed in the logs:

  1. export VAULT_TOKEN=s.5VUS8pte13RqekCB2fmMT3u2

You can check Vault’s status using the CLI command vault status:

  1. Key Value
  2. --- -----
  3. Seal Type shamir
  4. Initialized true
  5. Sealed false
  6. Total Shares 1
  7. Threshold 1
  8. Version 1.2.2
  9. Cluster Name vault-cluster-b07e80d8
  10. Cluster ID 55bd74b6-eaaf-3862-f7ce-3473ab86c57f
  11. 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:

  1. # this will disable the current kv version 2 secret engine mounted at path 'secret'
  2. vault secrets disable secret
  3. # this will enable a new kv engine version 1 at path 'secret'
  4. vault secrets enable -path=secret kv

Now let’s add a secret configuration for our application:

  1. 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:

  1. cat <<EOF | vault policy write vault-quickstart-policy -
  2. path "secret/myapps/vault-quickstart/*" {
  3. capabilities = ["read"]
  4. }
  5. EOF
When using a kv secret engine version 2, secrets are written and fetched at path <mount>/data/<secret-path>as opposed to <mount>/<secret-path> in a kv secret engine version 1.It does not change any of the CLI commands (i.e. you do not specify data in your path).However it does change the policies, since capabilities are applied to the real path. In the example above,the path should be secret/data/myapps/vault-quickstart/* if you were working with a kv secret engine version 2.

And finally, let’s enable the userpass auth secret engine, and create user bob with access to the vault-quickstart-policy:

  1. vault auth enable userpass
  2. vault write auth/userpass/users/bob password=sinclair policies=vault-quickstart-policy
The Vault extension also supports alternate authentication methods such as:-approle-kubernetes when deploying the Quarkus applicationand Vault into KubernetesIt is also possible to directly pass a quarkus.vault.authentication.client-token, which will bypass the authentication process.This could be handy in development where revocation and ttl are not a concern.Check the extension configuration documentation for more information.

To check that the configuration is correct, try logging in:

  1. vault login -method=userpass username=bob password=sinclair

You should see:

  1. Success! You are now authenticated. The token information displayed below
  2. is already stored in the token helper. You do NOT need to run "vault login"
  3. again. Future Vault requests will automatically use this token.
  4. Key Value
  5. --- -----
  6. token s.s93BVzJPzBiIGuYJHBTkG8Uw
  7. token_accessor OKNipTAgxWbxsrjixASNiwdY
  8. token_duration 768h
  9. token_renewable true
  10. token_policies ["default" "vault-quickstart-policy"]
  11. identity_policies []
  12. policies ["default" "vault-quickstart-policy"]
  13. token_meta_username bob

Now set VAULT_TOKEN to the token above (instead of the root token), and try reading the vault-quickstart secret config:

  1. export VAULT_TOKEN=s.s93BVzJPzBiIGuYJHBTkG8Uw
  2. vault kv get secret/myapps/vault-quickstart/config

You should see:

  1. ======== Data ========
  2. Key Value
  3. --- -----
  4. a-private-key 123456

Create a quarkus application with a secret configuration

  1. mvn io.quarkus:quarkus-maven-plugin:1.0.0.CR1:create \
  2. -DprojectGroupId=org.acme \
  3. -DprojectArtifactId=vault-quickstart \
  4. -DclassName="org.acme.quickstart.GreetingResource" \
  5. -Dpath="/hello" \
  6. -Dextensions="vault,hibernate-orm,jdbc-postgresql"
  7. cd vault-quickstart

Configure access to vault from the application.properties:

  1. # vault url
  2. quarkus.vault.url=http://localhost:8200
  3. # vault authentication
  4. quarkus.vault.authentication.userpass.username=bob
  5. quarkus.vault.authentication.userpass.password=sinclair
  6. # path within the kv secret engine where is located the vault-quickstart secret configuration
  7. 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:

  1. @Path("/hello")
  2. public class GreetingResource {
  3. @ConfigProperty(name = "a-private-key")
  4. String privateKey;
  5. ...
  6. @GET
  7. @Path("/private-key")
  8. @Produces(MediaType.TEXT_PLAIN)
  9. public String privateKey() {
  10. return privateKey;
  11. }
  12. }

Now compile the application and run it:

  1. ./mvnw clean install
  2. java -jar target/vault-quickstart-1.0-SNAPSHOT-runner.jar

Finally test the new endpoint:

  1. curl http://localhost:8080/hello/private-key

You should see:

  1. 123456

Fetching credentials from Vault with Hibernate ORM

Start a PostgreSQL database:

  1. docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name postgres-quarkus-hibernate -e POSTGRES_USER=sarah -e POSTGRES_PASSWORD=connor -e POSTGRES_DB=mydatabase -p 5432:5432 postgres:10.5

Create a simple service:

  1. @ApplicationScoped
  2. public class SantaClausService {
  3. @Inject
  4. EntityManager em;
  5. @Transactional
  6. public List<Gift> getGifts() {
  7. return (List<Gift>) em.createQuery("select g from Gift g").getResultList();
  8. }
  9. }

With its Gift entity:

  1. @Entity
  2. public class Gift {
  3. private Long id;
  4. private String name;
  5. @Id
  6. @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="giftSeq")
  7. public Long getId() {
  8. return id;
  9. }
  10. public void setId(Long id) {
  11. this.id = id;
  12. }
  13. public String getName() {
  14. return name;
  15. }
  16. public void setName(String name) {
  17. this.name = name;
  18. }
  19. }

Finally, add a new endpoint in GreetingResource:

  1. @Inject
  2. SantaClausService santaClausService;
  3. @GET
  4. @Path("/gift-count")
  5. @Produces(MediaType.TEXT_PLAIN)
  6. public int geGiftCount() {
  7. return santaClausService.getGifts().size();
  8. }

Create a new path in Vault where the database password will be added:

  1. # set the root token again
  2. export VAULT_TOKEN={root-token}
  3. 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:

  1. quarkus.vault.credentials-provider.mydatabase.kv-path=myapps/vault-quickstart/db

This defines a credentials provider mydatabase that will fetch the password from key passwordat path myapps/vault-quickstart/db.

The credentials provider can now be used in the datasource configuration, in place of the passwordproperty:

  1. # configure your datasource
  2. quarkus.datasource.url = jdbc:postgresql://localhost:5432/mydatabase
  3. quarkus.datasource.driver = org.postgresql.Driver
  4. quarkus.datasource.username = sarah
  5. #quarkus.datasource.password = connor
  6. quarkus.datasource.credentials-provider = mydatabase
  7. # drop and create the database at startup (use `update` to only update the schema)
  8. quarkus.hibernate-orm.database.generation=drop-and-create

Restart the application after rebuilding it, and test it with the new endpoint:

  1. curl http://localhost:8080/hello/gift-count

You should see:

  1. 0
The Vault extension also supports using dynamic database credentialsthrough the database-credentials-role property on the credentials-provider. The Vault setup is moreinvolved and goes beyond the scope of that quickstart guide.

Programmatic access to the KV secret engine

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:

  1. @Inject
  2. VaultKVSecretEngine kvSecretEngine;
  3. ...
  4. @GET
  5. @Path("/secrets/{vault-path}")
  6. @Produces(MediaType.TEXT_PLAIN)
  7. public String getSecrets(@PathParam("vault-path") String vaultPath) {
  8. return kvSecretEngine.readSecret("myapps/vault-quickstart/" + vaultPath).toString();
  9. }

Restart the application after rebuilding it, and test it with the new endpoint:

  1. curl http://localhost:8080/hello/secrets/db

You should see:

  1. {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:

  • through the standard javax.net.ssl.trustStore system property, which should refer to a JKS truststore containingthe trusted certificates

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

Conclusion

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.

Configuration Reference

Configuration property fixed at build time - ️ Configuration property overridable at runtime

Configuration propertyTypeDefault
quarkus.vault.urlVault server url. Example: https://localhost:8200URL
quarkus.vault.authentication.client-tokenVault token, bypassing Vault authentication (kubernetes, userpass or approle). This is useful in development where an authentication mode might not have been set up. In production we will usually prefer some authentication such as userpass, or preferably kubernetes, where Vault tokens get generated with a TTL and some ability to revoke them.string
quarkus.vault.authentication.app-role.role-idRole Id for AppRole auth method. This property is required when selecting the app-role authentication type.string
quarkus.vault.authentication.app-role.secret-idSecret Id for AppRole auth method. This property is required when selecting the app-role authentication type.string
quarkus.vault.authentication.userpass.usernameUser for userpass auth method. This property is required when selecting the userpass authentication type.string
quarkus.vault.authentication.userpass.passwordPassword for userpass auth method. This property is required when selecting the userpass authentication type.string
quarkus.vault.authentication.kubernetes.roleKubernetes authentication role that has been created in Vault to associate Vault policies, with Kubernetes service accounts and/or Kubernetes namespaces. This property is required when selecting the Kubernetes authentication type.string
quarkus.vault.authentication.kubernetes.jwt-token-pathLocation of the file containing the Kubernetes JWT token to authenticate against in Kubernetes authentication mode.string/var/run/secrets/kubernetes.io/serviceaccount/token
quarkus.vault.renew-grace-periodRenew grace period duration. This value if used to extend a lease before it expires its ttl, or recreate a new lease before the current lease reaches its max_ttl. By default Vault leaseDuration is equal to 7 days (ie: 168h or 604800s). If a connection pool maxLifetime is set, it is reasonable to set the renewGracePeriod to be greater than the maxLifetime, so that we are sure we get a chance to renew leases before we reach the ttl. In any case you need to make sure there will be attempts to fetch secrets within the renewGracePeriod, because that is when the renewals will happen. This particularly important for db dynamic secrets because if the lease reaches its ttl or max_ttl, the password of the db user will become invalid and it will be not longer possible to log in. This value should also be smaller than the ttl, otherwise that would mean that we would try to recreate leases all the time.Duration1H
quarkus.vault.secret-config-cache-periodVault config source cache period. Properties fetched from vault as MP config will be kept in a cache, and will not be fetched from vault again until the expiration of that period. This property is ignored if secret-config-kv-path is not set.Duration10M
quarkus.vault.secret-config-kv-pathVault path in kv store, where all properties will be available as MP config.string
quarkus.vault.log-confidentiality-levelUsed to hide confidential infos, for logging in particular. Possible values are: - low: display all secrets. medium: display only usernames and lease ids (ie: passwords and tokens are masked). high: hide lease ids and dynamic credentials username.low, medium, highmedium
quarkus.vault.kv-secret-engine-versionKv secret engine version. see https://www.vaultproject.io/docs/secrets/kv/index.htmlint1
quarkus.vault.kv-secret-engine-mount-pathKv secret engine path. see https://www.vaultproject.io/docs/secrets/kv/index.htmlstringsecret
quarkus.vault.tls.skip-verifyAllows to bypass certificate validation on TLS communications. If true this will allow TLS communications with Vault, without checking the validity of the certificate presented by Vault. This is discouraged in production because it allows man in the middle type of attacks.booleanfalse
quarkus.vault.tls.ca-certCertificate bundle used to validate TLS communications with Vault. The path to a pem bundle file, if TLS is required, and trusted certificates are not set through javax.net.ssl.trustStore system property.string
quarkus.vault.tls.use-kubernetes-ca-certIf true and Vault authentication type is kubernetes, TLS will be active and the cacert path will be set to /var/run/secrets/kubernetes.io/serviceaccount/ca.crt. If set, this setting will take precedence over property quarkus.vault.tls.ca-cert. This means that if Vault authentication type is kubernetes and we want to use quarkus.vault.tls.ca-cert or system property javax.net.ssl.trustStore, then this property should be set to false.booleantrue
quarkus.vault.connect-timeoutTimeout to establish a connection with Vault.Duration5S
quarkus.vault.read-timeoutRequest timeout on Vault.Duration1S
quarkus.vault.credentials-provider."credentials-provider".database-credentials-roleDatabase credentials role, as defined by https://www.vaultproject.io/docs/secrets/databases/index.html One of database-credentials-role or kv-path needs to be defined. not both.string
quarkus.vault.credentials-provider."credentials-provider".kv-pathA path in vault kv store, where we will find the kv-key. One of database-credentials-role or kv-path needs to be defined. not both. see https://www.vaultproject.io/docs/secrets/kv/index.htmlstring
quarkus.vault.credentials-provider."credentials-provider".kv-keyKey name to search in vault path kv-path. The value for that key is the credential. kv-key should not be defined if kv-path is not. see https://www.vaultproject.io/docs/secrets/kv/index.htmlstringpassword
About the Duration formatThe format for durations uses the standard java.time.Duration format.You can learn more about it in the Duration#parse() javadoc.You can also provide duration values starting with a number.In this case, if the value consists only of a number, the converter treats the value as seconds.Otherwise, PT is implicitly prepended to the value to obtain a standard java.time.Duration format.