Using HashiCorp Vault with Databases
The most common use case when working with Vault is to keep confidential the database connection credentials. There are several approaches that are supported in Quarkus, with different levels of sophistication and security:
-
Property fetched from the KV Secret Engine using the Vault MicroProfile Config Source
-
Quarkus Credentials Provider
-
Vault Dynamic DB Credentials
This guide aims at providing examples for each of those approaches. We will reuse the application implemented in the Vault guide and enhance it with a simple persistence use case.
Prerequisites
To complete this guide, you need:
-
to complete the Vault guide
-
roughly 20 minutes
-
an IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.8.1+
-
Docker installed
Application
We assume the Vault guide application has been developed, a Vault container is running, and the root token is known. In this section we are going to start a PostgreSQL database, and add a persistence service in the application.
Add the Hibernate and PostgreSQL extensions to the application:
mvn quarkus:add-extension -Dextensions="io.quarkus:quarkus-hibernate-orm,io.quarkus:quarkus-jdbc-postgresql"
Create a simple service:
@ApplicationScoped
public class SantaClausService {
@Inject
EntityManager em;
@Transactional
public List<Gift> getGifts() {
return (List<Gift>) em.createQuery("select g from Gift g").getResultList();
}
}
With its Gift
entity:
@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
@Path("/gift-count")
@Produces(MediaType.TEXT_PLAIN)
public int geGiftCount() {
return santaClausService.getGifts().size();
}
Start a PostgreSQL database:
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
Now we are ready to configure Vault and Quarkus to be able to connect to this database from the application.
Vault MicroProfile Config Source
The simplest approach is to write the database password in the KV secret engine under the path that is fetched from the Vault Config Source.
Open a new shell, docker exec
in the Vault container and set the root token:
docker exec -it dev-vault sh
export VAULT_TOKEN=s.5VUS8pte13RqekCB2fmMT3u2
Add a dbpassword
property in the config
path of the KV secret engine, beside the original a-private-key
property:
vault kv put secret/myapps/vault-quickstart/config a-private-key=123456 dbpassword=connor
Add the following configuration in Quarkus to use the value of property dbpassword
as our database password:
# configure your datasource
quarkus.datasource.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.password = ${dbpassword}
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydatabase
# drop and create the database at startup (use `update` to only update the schema)
quarkus.hibernate-orm.database.generation=drop-and-create
Compile and start the application:
./mvnw package
java -jar target/quarkus-app/quarkus-run.jar
Test it with the gift-count
endpoint:
curl http://localhost:8080/hello/gift-count
You should see:
0
This approach is certainly the simplest of all. It has also the big advantage of working with any subsystem that requires a secret information in the configuration (i.e. not just Agroal). The only drawback is that the password will never be fetched again from Vault after the initial property loading. This means that if the db password was changed while running, the application would have to be restarted after Vault has been updated with the new password. This contrasts with the credentials provider approach, which fetches the password from Vault every time a connection creation is attempted.
Credentials Provider
In this approach we introduce a new abstraction called the Credentials Provider that acts as an intermediary component between the Agroal datasource and Vault. The additional configuration required is small, and it has the major advantage of handling gracefully database password change while the application is running, without any restart. Since all new connections go through the Credentials Provider to fetch their password, we make sure we have a fresh value every time.
Create a new path (different than the config
path) in Vault where the database password will be added:
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 else
we 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 need first 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.db-kind = postgresql
quarkus.datasource.username = sarah
quarkus.datasource.credentials-provider = mydatabase
quarkus.datasource.jdbc.url = jdbc:postgresql://localhost:5432/mydatabase
Recompile, start and test the gift-count
endpoint. You should see 0
again.
Dynamic Database Credentials
The two previous approaches work well and are very popular. However they rely on a well known user configured in the application (i.e. the database user), and the security comes from the confidentiality of the password. If the password was stolen, we would have to change it in the database and in Vault. Regularly rotating passwords is actually a very good practice to limit (in time) the impact of getting the password stolen.
A more sophisticated approach consists in letting Vault create and retire database accounts on a regular basis. This is supported in Vault with the Database secret engine. A number of databases are supported, such as PostgreSQL.
First we need to enable the database
secret engine, configure the postgresql-database-plugin
and create
the database role mydbrole
(replace 10.0.0.3
by the actual host that is running the PostgreSQL container;
for simplicity we disabled TLS between Vault and the PostgreSQL database):
vault secrets enable database
vault write database/config/mydb \
plugin_name=postgresql-database-plugin \
allowed_roles=mydbrole \
connection_url=postgresql://{{username}}:{{password}}@10.0.0.3:5432/mydatabase?sslmode=disable \
username=sarah \
password=connor
cat <<EOF > vault-postgres-creation.sql
CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO "{{name}}";
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public to "{{name}}";
EOF
vault write database/roles/mydbrole \
db_name=mydb creation_statements=@vault-postgres-creation.sql \
default_ttl=1h \
max_ttl=24h \
revocation_statements="ALTER ROLE \"{{name}}\" NOLOGIN;" \
renew_statements="ALTER ROLE \"{{name}}\" VALID UNTIL '{{expiration}}';"
For this use case, user |
Then we need to give a read capability to the Quarkus application on path database/creds/mydbrole
.
cat <<EOF | vault policy write vault-quickstart-policy -
path "secret/data/myapps/vault-quickstart/*" {
capabilities = ["read", "create"]
}
path "database/creds/mydbrole" {
capabilities = [ "read" ]
}
EOF
Now that Vault knows how to create users in PostgreSQL, we just need to change the mydatabase
credentials
provider to use credentials-role
& credentials-mount
.
quarkus.vault.credentials-provider.mydatabase.credentials-role=mydbrole
quarkus.vault.credentials-provider.mydatabase.credentials-mount=database
When using |
Finally, we need to tell our Quarkus database configuration to recycle connections before the lease TTL we configured in Vault. This will ensure the database connection will always use valid credentials.
In this example the default TTL is set to 1 hour, so we will recycle connections every 30 minutes:
quarkus.datasource.jdbc.max-lifetime=30m
When using a reactive datasource you will need to use the |
Recompile with ./mvnw package
, start and test the gift-count
endpoint. You should see 0
again.
Notice in the logs:
2020-04-22 14:29:48,522 DEBUG [io.qua.vau.run.VaultDbManager] (Agroal_682171661) generated mydbrole credentials: {leaseId: database/creds/mydbrole/L6PxoI68gZDeVPXP0RAA4c0a, renewable: true, leaseDuration: 60s, valid_until: Wed Apr 22 14:30:48 CEST 2020, username: v-userpass-mydbrole-HeOMJCmy9coEnO2my2AR-1587558588, password:***}
If you connect to the PostgreSQL database, and list all users configured on mydatabase
, you will see the
sarah
super user, but also the technical users dynamically created by Vault:
docker exec -it postgres-quarkus-hibernate bash psql mydatabase sarah mydatabase=# \du List of roles Role name | Attributes | Member of -----------------------------------------------------+------------------------------------------------------------+----------- sarah | Superuser, Create role, Create DB, Replication, Bypass RLS | {} v-userpass-mydbrole-HeOMJCmy9coEnO2my2AR-1587558588 | Password valid until 2020-04-22 12:30:53+00 | {} v-userpass-mydbrole-N2ITbBXxoqMQ3A3cZL88-1587558572 | Cannot login +| {} | Password valid until 2020-04-22 12:30:37+00 |
As you can see 2 users have been created:
-
v-userpass-mydbrole-N2ITbBXxoqMQ3A3cZL88-1587558572
that has expired, which was created while we were executing the tests. -
v-userpass-mydbrole-HeOMJCmy9coEnO2my2AR-1587558588
that is valid until12:30:53
.
As expected, users have been created dynamically by Vault, with expiration dates, allowing a rotation to occur, without breaking existing connections, allowing a greater level of security than the traditional password based approaches.