Encrypt Spring application data with Vault
Some applications process sensitive or personally identifiable information (PII). You should encrypt this information in-transit and at-rest to protect the data. Vault helps centralize management of cryptographic services for data protection.
In this tutorial, you'll use the Vault transit secrets engine to encrypt data processed by a Spring application before writing it into a database. Encrypting the data before writing ensures that only those with access to the key in Vault can decrypt it. Any service or user without access to the key and decryption endpoint in Vault cannot decrypt the data.
Prerequisites
Set up application dependencies
Retrieve the configuration by cloning or downloading the hashicorp-education/learn-vault-spring-cloud repository from GitHub.
Clone the repository.
$ git clone https://github.com/hashicorp-education/learn-vault-spring-cloud
Or download the repository.
The repository contains supporting content for this Spring application tutorial.
Change directories to the
vault-transit/
directory.$ cd learn-vault-spring-cloud/vault-transit/
Review
pom.xml
to review the application's dependencies. The Spring application in this demo uses the Spring Cloud Vault library. It provides lightweight client-side support for connecting to Vault in a distributed environment. The application also needs JDBC and PostgreSQL to access the database.pom.xml
<?xml version="1.0" encoding="UTF-8"?><project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.5</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.hashicorp</groupId> <artifactId>vault-transit</artifactId> <version>0.0.1-SNAPSHOT</version> <name>vault-transit</name> <description>Demo code for Vault transit secrets engine with Spring</description> <properties> <java.version>22</java.version> <spring-cloud.version>2023.0.1</spring-cloud.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-vault-config</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-jdbc</artifactId> </dependency> <dependency> <groupId>org.postgresql</groupId> <artifactId>postgresql</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>${spring-cloud.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Configure application access to Vault and database
Spring Cloud Vault requires the Vault address and authentication method passed as application properties. This tutorial uses the Vault token authentication method. The application also passes the transit secrets engine path and database connection as configuration properties.
Review
application.properties
in/src/main/resources
. The application setsspring.cloud.vault.uri
andspring.cloud.vault.token
to the address and root token of the Vault server in dev mode. You can change the Vault authentication to any supported authentication methods based on your organization's Vault setup.application.properties
spring.application.name=vault-transit # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # create schema in databasespring.sql.init.mode=always # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/paymentsspring.datasource.username=postgresspring.datasource.password=postgres-admin-password # configure path to Vault transit secrets enginetransit.path=transittransit.key=payments
Additional properties in
application.properties
set up application access to the database and initializes the database table.application.properties
spring.application.name=vault-transit # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # create schema in databasespring.sql.init.mode=always # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/paymentsspring.datasource.username=postgresspring.datasource.password=postgres-admin-password # configure path to Vault transit secrets enginetransit.path=transittransit.key=payments
Review the
schema.sql
file in/src/main/resources
. Spring supports database initialization, which sets up the initial table in the database. The database initializes the first time you run the application.src/main/resources/schema.sql
set time zone 'UTC';create extension if not exists pgcrypto; CREATE TABLE if not exists payments( id VARCHAR(255) PRIMARY KEY NOT NULL, name VARCHAR(255) NOT NULL, cc_info VARCHAR(255) NOT NULL, created_at TIMESTAMP NOT NULL);
Add custom configuration properties for transit secrets engine
The application needs the path to the transit secrets engine and key in Vault. Rather than hard-code the path into the application, set up custom configuration properties for the transit secrets engine path and key.
Review the
VaultTransitProperties.java
file in/src/main/java/com/hashicorp/vaulttransit
. This class enables a configuration property with the prefixtransit
and fields for path and key.VaultTransitProperties.java
package com.hashicorp.vaulttransit; import org.springframework.boot.context.properties.ConfigurationProperties;import org.springframework.context.annotation.Configuration; @Configuration@ConfigurationProperties(prefix = "transit")class VaultTransitProperties { private String path; private String key; public String getPath() { return path; } public void setPath(String path) { this.path = path; } public String getKey() { return key; } public void setKey(String key) { this.key = key; }}
The
main
function inVaultTransitApplication.java
includes a Spring annotation to add the custom configuration properties.VaultTransitProperties.java
@SpringBootApplication@EnableConfigurationProperties(VaultTransitProperties.class)public class VaultTransitApplication { public static void main(String[] args) { SpringApplication.run(VaultTransitApplication.class, args); }}
Return to
application.properties
. It includes the custom propertiestransit.path
andtransit.key
to use the transit secrets engine in Vault.application.properties
spring.application.name=vault-transit # configure access to Vaultspring.cloud.vault.uri=${VAULT_ADDR:http://127.0.0.1:8200}spring.cloud.vault.token=${VAULT_TOKEN:vault-root-password} # create schema in databasespring.sql.init.mode=always # configure access to databasespring.datasource.url=jdbc:postgresql://localhost/paymentsspring.datasource.username=postgresspring.datasource.password=postgres-admin-password # configure path to Vault transit secrets enginetransit.path=transittransit.key=payments
Implement encrypt and decrypt methods
Spring Cloud Vault uses the Spring Vault library to make requests to Vault. Spring Vault includes an interface with operations for the transit secrets engine. The interface simplifies calls to encrypt and decrypt payloads.
Tip
If you use a JPA implementation, you can use an AttributeConverter interface to encrypt and decrypt the payload before inserting and retrieving it from the database.
Review
VaultTransit.java
in/src/main/java/com/hashicorp/vaulttransit
. This class injects sets up theVaultOperations
interface and passes the transit secrets engine's path and key. It implements methods to encrypt and decrypt a plaintext credit card number used by the application.VaultTransit.java
package com.hashicorp.vaulttransit; import org.springframework.vault.core.VaultOperations;import org.springframework.vault.core.VaultTemplate; class VaultTransit { private final VaultOperations vault; private final String path; private final String key; VaultTransit(VaultTransitProperties properties, VaultTemplate vaultTemplate) { this.vault = vaultTemplate; this.path = properties.getPath(); this.key = properties.getKey(); } String decrypt(String ccInfo) { return vault.opsForTransit(path).decrypt(key, ccInfo); } String encrypt(String ccInfo) { return vault.opsForTransit(path).encrypt(key, ccInfo); }}
Review
VaultTransitApplication.java
. This file contains a controller definition for the application. Each time someone creates a payment with the application's API, the plaintext credit card number gets encrypted by Vault.VaultTransitApplication.java
@Controller@ResponseBodyclass PaymentsController { private final JdbcClient db; private final VaultTransit vaultTransit; PaymentsController(DataSource dataSource, VaultTransitProperties vaultTransitProperties, VaultTemplate vaultTemplate) { this.db = JdbcClient.create(dataSource); this.vaultTransit = new VaultTransit(vaultTransitProperties, vaultTemplate); } // omitted @PostMapping(path = "/payments", consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) Collection<Payment> createPayment(@RequestBody Payment request) { var id = UUID.randomUUID().toString(); var statement = String.format( "INSERT INTO payments(id, name, cc_info, created_at) " + "VALUES('%s', '%s', '%s', '%s')", id, request.name, vaultTransit.encrypt(request.ccInfo), Instant.now().toString()); this.db.sql(statement).update(); return this.db .sql(String.format("SELECT * FROM payments WHERE id = '%s'", id)) .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), rs.getString("cc_info"), rs.getTimestamp("created_at").toInstant() )).list(); } record Payment(String id, String name, @JsonProperty(value = "cc_info") String ccInfo, Instant createdAt) {}}
Continue reviewing
VaultTransitApplication.java
. Each time someone gets a list of payments from the database, the controller decrypts the credit card information with Vault transit secrets engine.Tip
For applications that do not need to decrypt and use the plaintext data, create a Vault policy that only allows access to the "/encrypt" endpoint. This approach further protects sensitive information from unauthorized access.VaultTransitApplication.java
@Controller@ResponseBodyclass PaymentsController { private final JdbcClient db; private final VaultTransit vaultTransit; PaymentsController(DataSource dataSource, VaultTransitProperties vaultTransitProperties, VaultTemplate vaultTemplate) { this.db = JdbcClient.create(dataSource); this.vaultTransit = new VaultTransit(vaultTransitProperties, vaultTemplate); } @GetMapping("/payments") Collection<Payment> getPayments() { return this.db .sql("SELECT * FROM payments") .query((rs, rowNum) -> new Payment( rs.getString("id"), rs.getString("name"), vaultTransit.decrypt(rs.getString("cc_info")), rs.getTimestamp("created_at").toInstant() )) .list(); } // omitted record Payment(String id, String name, @JsonProperty(value = "cc_info") String ccInfo, Instant createdAt) {}}
Run the application
Build and run the application on your local machine to test how Vault encrypts the data before inserting it into the database and decrypts the data for a list of payments.
Return to the
vault-transit
directory.Use Docker Compose to create a Vault server in dev mode, PostgreSQL database for the application, and Vault configuration container to set up the transit secrets engine.
$ docker compose up -d [+] Running 4/4✔ Network vault-transit_default Created✔ Container vault-transit-vault-1 Healthy✔ Container vault-transit-postgres-1 Started✔ Container vault-transit-vault-configure-1 Started
After you start the Vault server, verify that it has the transit secrets engine enabled with an encryption key at
transit/keys/payments
.$ VAULT_TOKEN=vault-root-password VAULT_ADDR=http://127.0.0.1:8200 vault read transit/keys/payments Key Value--- -----allow_plaintext_backup falseauto_rotate_period 0sdeletion_allowed truederived falseexportable falseimported_key falsekeys map[1:1715181141]latest_version 1min_available_version 0min_decryption_version 1min_encryption_version 0name paymentssupports_decryption truesupports_derivation truesupports_encryption truesupports_signing falsetype aes256-gcm96
In your terminal, build and run the application using the Apache Maven Wrapper included in the repository.
$ ./mvnw spring-boot:run ... omitted ... 2024-05-09T09:45:20.962-04:00 INFO 86573 --- [vault-transit] [ main] c.h.v.VaultTransitApplication : Starting VaultTransitApplication using Java 22.0.12024-05-09T09:45:20.963-04:00 INFO 86573 --- [vault-transit] [ main] c.h.v.VaultTransitApplication : No active profile set, falling back to 1 default profile: "default"2024-05-09T09:45:21.340-04:00 INFO 86573 --- [vault-transit] [ main] o.s.cloud.context.scope.GenericScope : BeanFactory id=2faf401b-f830-3f49-8f84-97413410036b2024-05-09T09:45:21.512-04:00 INFO 86573 --- [vault-transit] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)2024-05-09T09:45:21.519-04:00 INFO 86573 --- [vault-transit] [ main] o.apache.catalina.core.StandardService : Starting service [Tomcat]2024-05-09T09:45:21.519-04:00 INFO 86573 --- [vault-transit] [ main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/10.1.20]2024-05-09T09:45:21.553-04:00 INFO 86573 --- [vault-transit] [ main] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring embedded WebApplicationContext2024-05-09T09:45:21.553-04:00 INFO 86573 --- [vault-transit] [ main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 566 ms2024-05-09T09:45:21.890-04:00 INFO 86573 --- [vault-transit] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...2024-05-09T09:45:21.975-04:00 INFO 86573 --- [vault-transit] [ main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection org.postgresql.jdbc.PgConnection@13213f262024-05-09T09:45:21.976-04:00 INFO 86573 --- [vault-transit] [ main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.2024-05-09T09:45:22.027-04:00 INFO 86573 --- [vault-transit] [ main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path ''2024-05-09T09:45:22.035-04:00 INFO 86573 --- [vault-transit] [ main] c.h.v.VaultTransitApplication : Started VaultTransitApplication in 1.337 seconds (process running for 1.535)
Open another terminal session. Send the application an API request to create a payment with a name and credit card number in plaintext. The application returns a response with a credit card number encrypted by Vault transit secrets engine.
$ curl -XPOST -d '{"name": "Test Customer", "cc_info": "4242424242424242"}' -H 'Content-Type:application/json' localhost:8080/payments [{"cc_info":"vault:v1:ebQHYut4EpirzpBTmLBR30NB+Tfslk9C6PhRs5PV/3i6xGEyJu/vsO8kQPQ=","id":"b7703d9a-ff7b-4898-91ba-4b5adba8b7e5","name":"Test Customer","createdAt":"2024-05-09T17:51:21.099933Z"}]
Query the database for the
payments
database for the encrypted credit card number.$ docker exec -it vault-transit-postgres-1 psql -U postgres -d payments -c "select * from payments;" id | name | cc_info | created_at--------------------------------------+---------------+-----------------------------------------------------------------------+---------------------------- b7703d9a-ff7b-4898-91ba-4b5adba8b7e5 | Test Customer | vault:v1:ebQHYut4EpirzpBTmLBR30NB+Tfslk9C6PhRs5PV/3i6xGEyJu/vsO8kQPQ= | 2024-05-09 13:51:21.099933(1 row)
Verify that application successfully decrypts the payload using Vault. Request the application for a list of payments in the database using its API. The request returns the credit card information in plaintext.
$ curl localhost:8080/payments [{"cc_info":"4242424242424242","id":"b7703d9a-ff7b-4898-91ba-4b5adba8b7e5","name":"Test Customer","createdAt":"2024-05-09T17:51:21.099933Z"}]
Clean up
Stop the application and remove the Vault server and database.
Stop the application running with Maven.
Remove the Vault server and database with Docker Compose.
$ docker compose down
Next steps
In this tutorial, you learned how to encrypt and decrypt application payloads with Vault transit secrets engine and Spring Cloud Vault. Encrypting the payloads before storing them in a database protects sensitive information in-transit and at-rest, ensuring only authorized identities can decrypt and use the payload. In the case of a data breach, you can rotate the encryption key in Vault and rewrap the data in the table with a new key.
For more information, review the following resources:
- Spring Cloud Vault
- Spring Vault, the core library for Spring Cloud Vault
- Transit secrets engine for Vault