Vault: Spring Boot web app using Spring Cloud Vault to fetch secrets

HashiCorp Vault is a secret and encryption management system that allows your organization to secure sensitive information such as API keys, certificates, and passwords.

In this article, I will show how a Java Spring Boot web application deployed into a Kubernetes cluster can fetch a secret directly from the Vault server using the Spring Cloud Vault library.

This means that sensitive values no longer need to be baked into the image, set in kubernetes yaml manifest files, placed into base64-encoded Kubernetes secrets, or volume mounted to the filesystem of the container.

Prerequisite

I am assuming you already have a Vault server installed, with the Kubernetes auth method enabled.  If you do not, see my previous article on installing a development Vault server on a Kubernetes cluster.

Deploy Vault enabled Spring app to Kubernetes

We will go through implementation details in later sections, but first let’s just deploy the Spring Boot web app into your Kubernetes cluster so we can see the end result of fetching a Vault secret.

Example Vault secret and details

This Spring app was created generic enough that you can specify your own secret path and role, pointing at any remote Vault server.  But for this article, I will be using the values setup in my previous article and you should feel free to replace it with your environment specific details.

Based on the previous article, there exists a kv2 Vault secret at “secret/webapp/config” with the following keys:

  • foo=bar
  • username=static-user
  • password=static-password

There is a Vault role named “myrole” and policy named “mypolicy” which provides ‘read’ permission to the secret path above.

Kubernetes auth is enabled on the Vault server, using the Kubernetes Service Account “vault-auth” in the Kubernetes namespace “vault”.

Apply yaml manifest to Kubernetes cluster

# fetch source code for this article
cd ~
git clone https://github.com/fabianlee/spring-boot-web-with-spring-cloud-vault.git && cd $(basename $_ .git)

# Kubernetes deployment name, namespace, and service account
export app=spring-boot-web-vault
export namespace_k8s=vault
export service_account_k8s=vault-auth
echo "Deployment '$app' going into k8s namespace '$namespace_k8s' running under the service account '$service_account_k8s'"

# Vault server location
export vault_uri=http://vault.vault.svc.cluster.local:8200

# Your specific Vault role and secret path
export vault_role=myrole
export vault_backend=secret
export vault_context=webapp
export vault_profile=config
echo "going to fetch kv2 secret using role '$vault_role' at path: $vault_backend/data/$vault_context/$vault_profile"

# generate from template and deploy to cluster
cat src/main/resources/kubernetes/deployment.yaml | DOLLAR_SIGN='$' envsubst | kubectl apply -f -

# deployment should now exist
kubectl get deployment $app -n $namespace_k8s

Validate fetch of secret from Vault server

First, we test an explicit fetch of the secret from the Vault server using the VaultTemplate class, which returns a VaultResponse.

# fetch secret using Spring VaultTemplate class
$ kubectl exec -it deployment/$app -n $namespace_k8s -c main -- curl http://localhost:8080/secret
The secret is loaded using VaulTemplate from 'secret/data/webapp/config':
password=static-password
foo=bar
username=static-username

Another mechanism is to have Spring ConfigData automatically fetch the secret, based on the settings in application.properties and place it into an Environment.

# secret auto-fetched by Spring ConfigData using values in application.properties
$ kubectl exec -it deployment/$app -n $namespace_k8s -c main -- curl http://localhost:8080/secretConfigData
The secret is loaded by ConfigData based on Spring application.properties settings:
password=static-password
foo=bar
username=static-user

Implementation Details

Now that you’ve seen the Spring Boot web app successfully fetch the Vault secret directly and show its keys, let’s look at some critical sections of configuration and code.

Spring Cloud Vault library

Add the spring-cloud-starter-vault-config to your build.gradle dependencies.  This will include Spring Cloud Vault library.

dependencies {
...
   implementation 'org.springframework.cloud:spring-cloud-starter-vault-config'
}

Spring application.properties keys

For configuration of the Spring Cloud Vault libraries, use the following basic keys in application.properties.

spring.cloud.vault.enabled=true

# URL can be to remote Vault server, in-cluster, or local Vault sidecar proxy
spring.cloud.vault.uri=http://vault.vault.svc.cluster.local:8200
#spring.cloud.vault.namespace=default #only enterprise version

# 'kubernetes' authentication mode for Vault
spring.cloud.vault.authentication=KUBERNETES
spring.cloud.vault.kubernetes.role=myrole
spring.cloud.vault.kubernetes.kubernetes-path=kubernetes
spring.cloud.vault.kubernetes.service-account-token-file=/var/run/secrets/kubernetes.io/serviceaccount/token

Add the keys below to support auto-fetch of a secret with ConfigData.

# Spring ConfigData
spring.cloud.vault.kv.enabled=true
spring.config.import=vault://
# used to build full path to secret
spring.cloud.vault.kv.backend=secret
spring.cloud.vault.kv.default-context=webapp
spring.cloud.vault.kv.profiles=config

Pulling Vault Secret with VaultTemplate

Our MyVaultTemplateService.java uses the VaultTemplate to fetch the Vault secret.

// fullSecretPath is '/secret/data/webapp/config'
response = vaultTemplate.read(fullSecretPath);

// go through response from Vault server
Object vaultdata = response.getRequiredData().get("data");
LinkedHashMap <String,String>secretmap =(LinkedHashMap<String,String>)vaultdata;
    		
// go through each key in secret, add to Properties for return
Iterator secretmapit = secretmap.keySet().iterator();
while(secretmapit.hasNext()) {
	String secretkey = secretmapit.next();
	String secretval = secretmap.get(secretkey);
	props.put(secretkey, secretval);
}

Spring ConfigData auto-pull of secret

The Spring ConfigData can also automatically fetch a secret, if the ‘spring.cloud.vault.kv.enabled=true’ and ‘spring.config.import=vault://’ settings in application.properties are enabled.

It uses the keys ‘spring.cloud.vault.kv.backend’, ‘spring.cloud.vault.kv.default-context’, and ‘spring.cloud.vault.kv.profiles’ to construct the full path to the secret, and places it into an Environment.  If you already know the expect key name in the secret, you can simply call Environment.getProperty()

@Autowired Environment env;

...

// ConfigData already fetched secret and placed into Environment
public String getSecretConfigData(String key) {
    return env.getProperty(key);
}

Unfortunately, you cannot simply enumerate the secret keys in the Environment object, so I also wrote a SpringEnvironmentUtils.java that can assist in pulling out just the secret keys from the Environment.

Properties props = SpringEnvironmentUtils.getAllKnownProperties(env,targetPropertySourceType,targetVaultPath);

GreetingController for exposing web endpoints

This is a Spring Boot web app, and we use the GreetingController.java to expose the HTTP endpoints used in this testing.

@GetMapping("/secret")
@ResponseBody
public String secretVaultTemplate() {
...
}

@GetMapping("/secretConfigData")
@ResponseBody
public String secretConfigData() {
...
}

 

REFERENCES

Spring docs, Spring Cloud Vault intro

HashiCorp, why vault?

Vault sidecar annotations

Github source code for this article spring-boot-web-with-spring-cloud-vault

HashiCorp, compare sidecar to CSI to Operator

Baeldung, Spring Vault and Kubernetes showing different methods of access (explicit, semi-explicit, sidecar, Secret CSI, Secret Operator)

Spring docs, VaultTemplate explanation

Spring docs, ConfigData explanation