Spring Vault

Overview

Spring application with vault integration

Github: https://github.com/gitorko/project76

Vault

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.

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

 1# Project 76
 2
 3Spring Boot - Vault & Property Refresh
 4
 5[https://gitorko.github.io/spring-vault/](https://gitorko.github.io/spring-vault/)
 6
 7### Version
 8
 9Check version
10
11```bash
12$java --version
13openjdk version "21.0.3" 2024-04-16 LTS
14
15$vault --version
16Vault v1.5.0 ('340cc2fa263f6cbd2861b41518da8a62c153e2e7+CHANGES')
17```
18
19### Postgres DB
20
21```
22docker run -p 5432:5432 --name pg-container -e POSTGRES_PASSWORD=password -d postgres:9.6.10
23docker ps
24docker exec -it pg-container psql -U postgres -W postgres
25CREATE USER test WITH PASSWORD 'test@123';
26CREATE DATABASE "test-db" WITH OWNER "test" ENCODING UTF8 TEMPLATE template0;
27grant all PRIVILEGES ON DATABASE "test-db" to test;
28
29docker stop pg-container
30docker start pg-container
31```
32
33## Vault
34
35To install vault on mac run the command, for other OS download and install vault.
36
37```bash
38brew install vault
39```
40
41Start the dev server
42
43```bash
44vault server -dev -log-level=INFO -dev-root-token-id=00000000-0000-0000-0000-000000000000
45```
46
47Once vault is up, insert some values
48
49```bash
50export VAULT_ADDR=http://localhost:8200
51export VAULT_SKIP_VERIFY=true
52export VAULT_TOKEN=00000000-0000-0000-0000-000000000000
53vault kv put secret/myapp/dev username=test password=test@123 dbname=test-db myKey=foobar featureFlag=true
54vault kv put secret/myapp/prod username=test password=test@123 dbname=test-db myKey=fooprod featureFlag=true
55```
56
57You can login to vault UI with token '00000000-0000-0000-0000-000000000000'
58
59Vault UI: [http://127.0.0.1:8200/](http://127.0.0.1:8200/)
60
61To update property value
62
63```bash
64vault kv patch secret/myapp/dev featureFlag=true
65vault kv patch secret/myapp/dev featureFlag=false
66```
67
68### Dev
69
70To run the code.
71
72```bash
73./gradlew clean build
74./gradlew bootRun --args='--spring.profiles.active=dev'
75./gradlew bootRun --args='--spring.profiles.active=prod'
76```

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

References

https://cloud.spring.io/spring-cloud-vault/reference/html/

https://www.vaultproject.io/

https://spring.io/guides/gs/accessing-vault/

comments powered by Disqus