Spring Vault
Overview
HashiCorp's vault is a tool to store and secure secrets along with tight access control. You can store tokens, passwords, certificates, API keys and other secrets. Spring Vault provides spring abstractions to vault. Sometimes you need your running application to detect the changed property value in order to provide a toggle on/off feature.
Github: https://github.com/gitorko/project76
Code
Based on the spring profile the respective properties get loaded from vault.
After the feature flag is changed, the new property value is detected by the application without needing a restart.
1package com.demo.project76;
2
3import java.util.Arrays;
4
5import com.demo.project76.domain.Customer;
6import com.demo.project76.domain.MySecrets;
7import com.demo.project76.repository.CustomerRepository;
8import lombok.RequiredArgsConstructor;
9import lombok.extern.slf4j.Slf4j;
10import org.springframework.beans.factory.annotation.Value;
11import org.springframework.boot.CommandLineRunner;
12import org.springframework.boot.SpringApplication;
13import org.springframework.boot.autoconfigure.SpringBootApplication;
14import org.springframework.boot.context.properties.EnableConfigurationProperties;
15import org.springframework.context.annotation.Bean;
16import org.springframework.core.env.ConfigurableEnvironment;
17import org.springframework.vault.core.VaultKeyValueOperationsSupport;
18import org.springframework.vault.core.VaultOperations;
19import org.springframework.vault.core.VaultSysOperations;
20import org.springframework.vault.core.VaultTemplate;
21import org.springframework.vault.core.VaultTransitOperations;
22import org.springframework.vault.support.VaultMount;
23import org.springframework.vault.support.VaultResponse;
24
25@SpringBootApplication
26@Slf4j
27@RequiredArgsConstructor
28@EnableConfigurationProperties(MySecrets.class)
29public class Main {
30
31 private final VaultTemplate vaultTemplate;
32 private final MySecrets mySecrets;
33 private final VaultOperations operations;
34
35 @Value("${my-group.username}")
36 private String userName;
37
38 @Value("${my-group.appType}")
39 private String appType;
40
41 public static void main(String[] args) {
42 SpringApplication.run(Main.class, args);
43 }
44
45 @Bean
46 public CommandLineRunner onStart(CustomerRepository customerRepository, ConfigurableEnvironment environment) {
47 return args -> {
48 log.info("Value injected via @Value userName : {}", userName);
49 log.info("Value injected via @Value environment : {}", environment);
50 log.info("Value injected via @Value appType : {}", appType);
51 log.info("Value injected via class mySecrets: {}", mySecrets);
52
53 //Reading directly.
54 if (Arrays.stream(environment.getActiveProfiles()).anyMatch(t -> t.equals("dev"))) {
55 VaultResponse response = vaultTemplate.opsForKeyValue("secret",
56 VaultKeyValueOperationsSupport.KeyValueBackend.KV_2).get("myapp/dev");
57 log.info("Value of myKey: {} ", response.getData().get("myKey"));
58 } else {
59 VaultResponse response = vaultTemplate.opsForKeyValue("secret",
60 VaultKeyValueOperationsSupport.KeyValueBackend.KV_2).get("myapp/prod");
61 log.info("Value of myKey: {} ", response.getData().get("myKey"));
62 }
63
64 //Writing new values to different path.
65 VaultTransitOperations transitOperations = vaultTemplate.opsForTransit();
66 VaultSysOperations sysOperations = vaultTemplate.opsForSys();
67 if (!sysOperations.getMounts().containsKey("transit/")) {
68 sysOperations.mount("transit", VaultMount.create("transit"));
69 transitOperations.createKey("foo-key");
70 }
71
72 // Encrypt a plain-text value
73 String ciphertext = transitOperations.encrypt("foo-key", "Secure message");
74 log.info("Encrypted value: {}", ciphertext);
75
76 // Decrypt
77 String plaintext = transitOperations.decrypt("foo-key", ciphertext);
78 log.info("Decrypted value: {}", plaintext);
79
80 //Save to db, connection established via vault credentials
81 Customer customer = customerRepository.save(Customer.builder().firstName("John").lastName("Rambo").build());
82 log.info("Customer: {}", customer);
83
84 };
85 }
86}
1package com.demo.project76.controller;
2
3
4import lombok.extern.slf4j.Slf4j;
5import org.springframework.beans.factory.annotation.Value;
6import org.springframework.cloud.context.config.annotation.RefreshScope;
7import org.springframework.web.bind.annotation.GetMapping;
8import org.springframework.web.bind.annotation.RestController;
9
10@RestController
11@RefreshScope
12@Slf4j
13public class HomeController {
14
15 @Value("${featureFlag}")
16 private Boolean featureFlag;
17
18 @GetMapping(value = "/greet")
19 public String greet() {
20 log.info("featureFlag: {}", featureFlag);
21 return featureFlag ? "Good Morning" : "Good Bye";
22 }
23}
1package com.demo.project76.domain;
2
3import lombok.Data;
4import org.springframework.boot.context.properties.ConfigurationProperties;
5import org.springframework.context.annotation.Configuration;
6
7@Data
8@Configuration
9@ConfigurationProperties("my-group")
10public class MySecrets {
11 String username;
12 String password;
13 String dbname;
14}
1my-group:
2 dbname: ${dbname}
3 username: ${username}
4 password: ${password}
5 appType: dev
6spring:
7 application:
8 name: myapp
9 main:
10 banner-mode: "off"
11 datasource:
12 driver-class-name: org.postgresql.Driver
13 host: localhost
14 url: jdbc:postgresql://${spring.datasource.host}:5432/${my-group.dbname}
15 username: ${my-group.username}
16 password: ${my-group.password}
17 jpa:
18 show-sql: false
19 hibernate.ddl-auto: create-drop
20 properties.hibernate.temp.use_jdbc_metadata_defaults: false
21 database-platform: org.hibernate.dialect.PostgreSQLDialect
22 defer-datasource-initialization: true
23management:
24 endpoints:
25 web:
26 exposure:
27 include: refresh
1spring:
2 cloud:
3 # Configuration for a vault server running in dev mode
4 vault:
5 scheme: http
6 host: localhost
7 port: 8200
8 connection-timeout: 5000
9 read-timeout: 15000
10 authentication: TOKEN
11 token: 00000000-0000-0000-0000-000000000000
12 kv:
13 enabled=true:
14 application-name: myapp
To provide a feature toggle feature you can use the @RefreshScope annotation and trigger a refresh using spring actuator.
Setup
Project 76
Spring Boot - Vault & Property Refresh
https://gitorko.github.io/spring-vault/
Version
Check version
1$java --version
2openjdk 17.0.3 2022-04-19 LTS
3
4$vault --version
5Vault v1.5.0 ('340cc2fa263f6cbd2861b41518da8a62c153e2e7+CHANGES')
Postgres DB
1docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
2docker ps
3docker exec -it pg-container psql -U postgres -W postgres
4CREATE USER test WITH PASSWORD 'test@123';
5CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
6grant all PRIVILEGES ON DATABASE "test-db" to test;
7
8docker stop pg-container
9docker start pg-container
Vault
To install vault on mac run the command, for other OS download and install vault.
1brew install vault
Start the dev server
1vault server -dev -log-level=INFO -dev-root-token-id=00000000-0000-0000-0000-000000000000
Once vault is up, insert some values
1export VAULT_ADDR=http://localhost:8200
2export VAULT_SKIP_VERIFY=true
3export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
4vault kv put secret/myapp/dev username=test password=test@123 dbname=test-db myKey=foobar featureFlag=true
5vault kv put secret/myapp/prod username=test password=test@123 dbname=test-db myKey=fooprod featureFlag=true
You can login to vault UI with token '00000000-0000-0000-0000-000000000000'
Vault UI: http://127.0.0.1:8200/
To update property value
1vault kv patch secret/myapp/dev featureFlag=true
2vault kv patch secret/myapp/dev featureFlag=false
Dev
To run the code.
1./gradlew clean build
2./gradlew bootRun --args='--spring.profiles.active=dev'
3./gradlew bootRun --args='--spring.profiles.active=prod'
Testing
You should now see the values being fetched from vault.
You can now invoke greet api to see a 'Good Morning' response.
1curl --location --request GET 'localhost:8080/greet'
Now lets change the feature flag to false in vault
1vault kv patch secret/myapp/dev featureFlag=false
In order for the values to be refreshed by spring context you need to make a call to actuator api
1curl --location --request POST 'http://localhost:8080/actuator/refresh'
Now the values will be refreshed and invoking greet api will show 'Good Bye' response.
1curl --location --request GET 'localhost:8080/greet'
Few more vault commands to try out
1vault kv get -field=username secret/myapp/dev
2vault kv delete secret/myapp/dev
3vault kv delete secret/myapp/prod