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

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