How to Use Vault and Spring Cloud Config to Secure Secrets

Back in 2013, a feature was released on GitHub that let users scan code in public repositories. Almost immediately, it was partially deactivated. People suspect that the reason for this was that the feature exposed all kinds of secrets. Then, in 2014, 50,000 uber drivers had their information stolen. This happened because a hacker accessed Uber’s database using credentials they got from a public GitHub repository. The following year, Hashicorp Vault (a tool for managing secrets and encrypting data in transit) was announced. And, two years after that, Spring Vault (the integration of Spring and Vault) came into being.

While this may seem like old news at this point, the leakage of secrets is still pervasive today. It happens to a whole host of developers (see this study from NC State University). This exposure of secrets leads to more cyber-attacks, loss or corruption of data, breaches, and crypto-jacking (cryptocurrency mining using a victim’s cloud computer power). Hashicorp’s Vault and Spring Cloud Vault can reduce this risk.

Since it’s not recommended to store secret values in code, this tutorial will offer these alternatives:

Prerequisites: Java 8+ and Docker.

Use Environment Variables for Secrets; a Precursor to Spring Vault

Spring Boot applications can bind property values from environment variables. To demonstrate, create a vault-demo-app with OpenID Connect authentication, using the Spring Initializr. Then add web, okta, and cloud-config-client dependencies, some of which will be required later in the tutorial:

Java
 




x


1
curl https://start.spring.io/starter.zip \
2
  -d dependencies=web,okta,cloud-config-client \
3
  -d groupId=com.okta.developer \
4
  -d artifactId=vault-demo-app  \
5
  -d name="Spring Boot Application" \
6
  -d description="Demo project of a Spring Boot application with Vault protected secrets" \
7
  -d packageName=com.okta.developer.vault \
8
  -o vault-demo-app.zip


Unzip the file and open the project. Modify its src/main/java/.../Application.java class to add the / HTTP endpoint:

Java
 




xxxxxxxxxx
1
23


1
package com.okta.developer.vault;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.security.core.annotation.AuthenticationPrincipal;
6
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
7
import org.springframework.web.bind.annotation.GetMapping;
8
import org.springframework.web.bind.annotation.RestController;
9
 
          
10
@RestController
11
@SpringBootApplication
12
public class Application {
13
 
          
14
    public static void main(String[] args) {
15
        SpringApplication.run(Application.class, args);
16
    }
17
 
          
18
    @GetMapping("/")
19
    String hello(@AuthenticationPrincipal OidcUser user) {
20
        return String.format("Welcome, %s", user.getFullName());
21
    }
22
 
          
23
}


For the Okta authentication set up, register for a free developer account. After you log in, go to API > Authorization Servers and copy your Issuer URI into a text editor.

Then go to Applications and create a new Web application. Configure it as follows:

Click Done and copy the Client ID and Client secret into a text editor for later.

Instead of storing Okta credentials in application.properties, Spring Boot allows you to bind properties from environment variables. You can see this in action by starting your application with the Maven command below:

Java
 




xxxxxxxxxx
1


1
SPRING_SECURITY_OAUTH2_CLIENT_PROVIDER_OKTA_ISSUER_URI={yourIssuerURI} \
2
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OKTA_CLIENT_ID={yourClientId} \
3
SPRING_SECURITY_OAUTH2_CLIENT_REGISTRATION_OKTA_CLIENT_SECRET={yourClientSecret} \
4
./mvnw spring-boot:run


In an incognito window, go to http://localhost:8080. Here, you should see the Okta login page:

Sign in page

In the application logs, you’ll see the security filter chain initializes an OAuth 2.0 authentication flow:

Java
 




xxxxxxxxxx
1
19


 
1
2020-05-01 00:19:45.952  INFO 12058 --- [main] o.s.s.web.DefaultSecurityFilterChain: Creating filter chain: any request, 
2
 [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@5c080ef3, 
3
 org.springframework.security.web.context.SecurityContextPersistenceFilter@6ecdbab8, 
4
 org.springframework.security.web.header.HeaderWriterFilter@5a2fa51f, 
5
 org.springframework.security.web.csrf.CsrfFilter@2016f509, 
6
 org.springframework.security.web.authentication.logout.LogoutFilter@23a5818e, 
7
 org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@14823f76, 
8
 org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter@7b6e5c12, 
9
 org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter@7979b8b7, 
10
 org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@17d32e9b, 
11
 org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@188cbcde, 
12
 org.springframework.security.web.savedrequest.RequestCacheAwareFilter@19f7222e, 
13
 org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5ba26eb0, 
14
 org.springframework.security.web.authentication.AnonymousAuthenticationFilter@4ee6291f, 
15
 org.springframework.security.oauth2.client.web.OAuth2AuthorizationCodeGrantFilter@2def7a7a, 
16
 org.springframework.security.web.session.SessionManagementFilter@22a0d4ea, 
17
 org.springframework.security.web.access.ExceptionTranslationFilter@73d4066e, 
18
 org.springframework.security.web.access.intercept.FilterSecurityInterceptor@7342e05d]
19
Using environment variables for passing secrets to containerized applications is now considered bad practice because the environment can be inspected or logged in a number of cases. So, let’s move on to using Spring Cloud Config server for secrets storage.


Spring Cloud Config with Secrets Encryption

In microservice architectures, managing configuration with a centralized config server is essential. Secret encryption is desirable at rest and when in transit. Spring Cloud Config Server is a popular implementation. Let’s configure the server to store encrypted secrets.

NOTE: To use encryption and decryption features in Java 8, you must install the Java Cryptography Extension (JCE) in your JVM, which is not included by default. Otherwise, the requests to the /encrypt endpoint in the config server will fail with “Illegal key size”.

Using the Spring Initializr API, create a Vault + Config Server application:

Java
 




xxxxxxxxxx
1


1
curl https://start.spring.io/starter.zip \
2
  -d dependencies=cloud-config-server \
3
  -d groupId=com.okta.developer \
4
  -d artifactId=vault-config-server  \
5
  -d name="Spring Boot Configuration Server" \
6
  -d description="Demo project of a Spring Boot application with Vault protected secrets" \
7
  -d packageName=com.okta.developer.vault \
8
  -o vault-config-server.zip


Unzip the downloaded file. Rename src/main/resource/application.properties to src/main/resource/application.yml, edit the file to specify the port, add a native profile, and specify config search locations:

Java
 




xxxxxxxxxx
1


1
server:
2
  port: 8888
3
 
          
4
spring:
5
  profiles:
6
    active: native


Edit:src/main/java/com/okta/developer/vault/SpringBootConfigurationServerApplication.java and add a @EnableConfigServer annotation:

Java
 




xxxxxxxxxx
1
15


1
package com.okta.developer.vault;
2
 
          
3
import org.springframework.boot.SpringApplication;
4
import org.springframework.boot.autoconfigure.SpringBootApplication;
5
import org.springframework.cloud.config.server.EnableConfigServer;
6
 
          
7
@EnableConfigServer
8
@SpringBootApplication
9
public class SpringBootConfigurationServerApplication {
10
 
          
11
    public static void main(String[] args) {
12
        SpringApplication.run(SpringBootConfigurationServerApplication.class, args);
13
    }
14
 
          
15
}


Start the server, as you are going to encrypt your Okta secrets using the /encrypt endpoint. For this example, you are using a symmetric (shared) encryption key, passed through the environment variable ENCRYPT_KEY. Before running the command below, you should replace {encryptKey} with a random string of characters.

Java
 




xxxxxxxxxx
1


1
ENCRYPT_KEY={encryptKey} ./mvnw spring-boot:run


Then, in another terminal, encrypt your client ID and secret.

Java
 




xxxxxxxxxx
1


1
curl localhost:8888/encrypt -d {yourOktaClientId}
2
curl localhost:8888/encrypt -d {yourOktaClientSecret}


In the vault-config-server project folder, create a src/main/resources/config/vault-demo-app-dev.yml file to store the secrets for the dev profile, with the following contents:

Java
 




xxxxxxxxxx
1
11


1
spring:
2
  security:
3
    oauth2:
4
      client:
5
        provider:
6
          okta:
7
            issuer-uri: {yourIssuerURI}
8
        registration:
9
          okta:
10
            client-id: '{cipher}encryptedClientId'
11
            client-secret: '{cipher}encryptedClientSecret'


The client-id and client-secret encrypted values must be prefixed with {cipher}. Restart the config server.

To consume the config server properties, the client application must set the server address in the bootstrap properties. In the vault-demo-app project folder, create the file src/main/resources/bootstrap.yml with the following content:

Java
 




xxxxxxxxxx
1


 
1
spring:
2
  application:
3
    name: vault-demo-app
4
  cloud:
5
    config:
6
      uri: http://localhost:8888
7
  profiles:
8
    active: dev


Start vault-demo-app without passing the environment variables:

Java
 




xxxxxxxxxx
1


1
./mvnw spring-boot:run


When requesting http://localhost:8080 it should again redirect to the Okta login.

In a real environment, the config server should be secured. Spring Cloud Config Server supports asymmetric key encryption as well, with the server encrypting with the public key, and the clients decrypting with the private key. However, the documentation warns about spreading the key management process around clients.

Vault as a Configuration Backend with Spring Cloud Vault

In the cloud, secrets management has become much more difficult. Vault is a secrets management and data protection tool from HashiCorp that provides secure storage, dynamic secret generation, data encryption, and secret revocation.

Vault encrypts the secrets prior to writing them to persistent storage. The encryption key is also stored in Vault, but encrypted with a master key not stored anywhere. The master key is split into shards using Shamir’s Secret Sharing algorithm, and distributed among a number of operators. The Vault unseal process allows you to reconstruct the master key by adding shards one at a time in any order until enough shards are present, then Vault becomes operative. Operations on secrets can be audited by enabling audit devices, which will send audit logs to a file, syslog or socket.

As Spring Cloud Config Server supports Vault as a configuration backend, the next step is to better protect the application secrets by storing them in Vault.

Pull the Vault docker image and start a container using the command below. Make sure to replace {hostPath} with a local directory path, such as /tmp/vault.

Java
 




xxxxxxxxxx
1


1
docker pull vault
2
docker run --cap-add=IPC_LOCK \
3
-e 'VAULT_DEV_ROOT_TOKEN_ID=00000000-0000-0000-0000-000000000000' \
4
-p 8200:8200 \
5
-v {hostPath}:/vault/logs \
6
--name my-vault vault


NOTE: The docker run command above will start a vault instance with the name my-vault. You can stop the container with docker stop my-vault and restart it with docker start my-vault. Note that all the secrets and data will be lost between restarts, as explained in the next paragraphs.

IPC_LOCK capability is required for Vault to be able to lock memory and not be swapped to disk, as this behavior is enabled by default. As the instance is run for development, the ID of the initially generated root token is set to the given value. We are mounting /vault/logs, as we are going to enable the file audit device to inspect the interactions.

Once it starts, you should notice the following logs:

Java
 




xxxxxxxxxx
1
16


1
...
2
WARNING! dev mode is enabled! In this mode, Vault runs entirely in-memory
3
and starts unsealed with a single unseal key. The root token is already
4
authenticated to the CLI, so you can immediately begin using Vault.
5
 
          
6
...
7
 
          
8
The unseal key and root token are displayed below in case you want to
9
seal/unseal the Vault or re-authenticate.
10
 
          
11
Unseal Key: wD2mT9W56zGWrG9PYajIA47spzSLEkIMYQX7Ocio1VQ=
12
Root Token: 00000000-0000-0000-0000-000000000000
13
 
          
14
Development mode should NOT be used in production installations!
15
 
          
16
==> Vault server started! Log data will stream in below:


It is clear Vault is running in dev mode, meaning it short-circuits a lot of setup to insecure defaults, which helps for the experimentation. Data is stored encrypted in-memory and lost on every restart. Copy the Unseal Key, as we are going to use it to test Vault sealing. Connect to the container and explore some vault commands:

Java
 




xxxxxxxxxx
1


1
docker exec -it my-vault /bin/sh


The command above will start a shell session with the container. After the prompt shows, run the following three commands:

Java
 




xxxxxxxxxx
1


1
export VAULT_TOKEN="00000000-0000-0000-0000-000000000000"
2
export VAULT_ADDR="http://127.0.0.1:8200"
3
vault status


The status command output shows if the vault instance is sealed:

Java
 




xxxxxxxxxx
1
11


 
1
Key             Value
2
---             -----
3
Seal Type       shamir
4
Initialized     true
5
Sealed          false
6
Total Shares    1
7
Threshold       1
8
Version         1.3.3
9
Cluster Name    vault-cluster-a80e6cd6
10
Cluster ID      769bfd8c-7c9e-5ef2-a2bd-667ae19b4180
11
HA Enabled      false


As you can see, in development mode Vault starts unsealed, meaning stored data can be decrypted/accessed.

Enable a file audit device to watch the interactions with Vault:

Java
 




xxxxxxxxxx
1


 
1
vault audit enable file file_path=/vault/logs/vault_audit.log


You should see a success message. Now store the Okta secrets for the vault-demo-app:

Java
 




xxxxxxxxxx
1


1
vault kv put secret/vault-demo-app,dev \
2
spring.security.oauth2.client.registration.okta.client-id="{yourClientId}" \
3
spring.security.oauth2.client.registration.okta.client-secret="{yourClientSecret}" \
4
spring.security.oauth2.client.provider.okta.issuer-uri="{yourIssuerURI}"
5
vault kv get secret/vault-demo-app,dev


As illustrated above, key-value pairs are stored with kv put command, and you can check the values with the kv get vault command.

Check vault_audit.log in your specified {hostPath} directory. Operations are logged in JSON format by default, with sensitive information hashed:

Java
 




xxxxxxxxxx
1
31


1
{
2
   "time":"2020-05-01T02:08:49.528995105Z",
3
   "type":"response",
4
   "auth":{...},
5
   "request":{
6
      "id":"1cc73d31-d678-88d4-0b8e-f16b3d961791",
7
      "operation":"read",
8
      "client_token":"...",
9
      "client_token_accessor":"...",
10
      "namespace":{
11
         "id":"root"
12
      },
13
      "path":"secret/data/vault-demo-app,dev",
14
      "remote_address":"127.0.0.1"
15
   },
16
   "response":{
17
      "data":{
18
         "data":{
19
            "spring.security.oauth2.client.provider.oidc.issuer-uri":"hmac-sha256:d44ecf9418576aba39752cf34a253bdf960a5ac475bd5eece78a776555035e1a",
20
            "spring.security.oauth2.client.registration.oidc.client-id":"hmac-sha256:d35fa23d933b5402a8c665ce4d73643506c7d13743e922e397a3cf78acde6c88",
21
            "spring.security.oauth2.client.registration.oidc.client-secret":"hmac-sha256:d6c38a298b067ac8ce76c427bd060fdda8558a024ebb1a60beb9cde60d9e5db8"
22
         },
23
         "metadata":{
24
            "created_time":"hmac-sha256:b5283ce3fbb0bb7a74c91e2e565e08d44d378d2e37946b1fa871e0c23947a6c1",
25
            "deletion_time":"hmac-sha256:81566d1c06213e53b6f7cf141388772d1ab59efcc4cfa9373c32098d90bda09a",
26
            "destroyed":false,
27
            "version":1
28
         }
29
      }
30
   }
31
}


Let’s assume you don’t want to configure the root token in the vault-demo-app. You can instead create a policy granting read permissions on the path where the secrets were stored. Go to the Vault Web UI at http://localhost:8200 and log in with the root token (00000000-0000-0000-0000-000000000000).

Vault sign in

Next, go to Policies and Create ACL policy. Create a vault-demo-app-policy with the following capabilities:

Java
 




xxxxxxxxxx
1
15


1
path "secret/data/vault-demo-app" {
2
  capabilities = [ "read" ]
3
}
4
 
          
5
path "secret/data/vault-demo-app,dev" {
6
  capabilities = [ "read" ]
7
}
8
 
          
9
path "secret/data/application" {
10
  capabilities = [ "read" ]
11
}
12
 
          
13
path "secret/data/application,dev" {
14
  capabilities = [ "read" ]
15
}


All the paths above will be requested by the config server to provide configuration for the vault-demo-app when it starts with the dev profile active.Create ACL policy

Now, go back to the container command line, and create a token with the vault-demo-app-policy.

Java
 




xxxxxxxxxx
1
12


1
vault token create -policy=vault-demo-app-policy
2
 
          
3
 
          
4
Key                  Value
5
---                  -----
6
token                s.4CO6wzq0M1WRUNsYviJB3wzz
7
token_accessor       2lYfyQJZtGPO4gyxsLmOnQyE
8
token_duration       768h
9
token_renewable      true
10
token_policies       ["default" "vault-demo-app-policy"]
11
identity_policies    []
12
policies             ["default" "vault-demo-app-policy"]


You are now ready to update the config server. In the vault-config-server project, edit src/main/resource/application.yml to add Vault as the config backend:

Java
 




xxxxxxxxxx
1
16


1
server:
2
  port: 8888
3
 
          
4
spring:
5
  profiles:
6
    active: vault
7
  cloud:
8
    config:
9
      server:
10
        vault:
11
          host: 127.0.0.1
12
          port: 8200
13
          kvVersion: 2
14
logging:
15
  level:
16
    root: TRACE       


Note that the logging level is set to TRACE to see the interaction between the server and Vault. Restart the vault-config-server.

Java
 




xxxxxxxxxx
1


1
./mvnw spring-boot:run


You should see the logs below if the server was configured correctly:

Java
 




xxxxxxxxxx
1


1
2020-05-01 01:19:10.105  INFO 14072 --- [main] SpringBootConfigurationServerApplication: 
2
 Started SpringBootConfigurationServerApplication in 4.525 seconds (JVM running for 4.859)


You will not see the config server trying to connect to Vault in the logs. That will happen when a config client requests the properties. Start the vault-demo-app, passing the token just created in the environment variable SPRING_CLOUD_CONFIG_TOKEN. For example:

Java
 




xxxxxxxxxx
1


1
SPRING_CLOUD_CONFIG_TOKEN=s.4CO6wzq0M1WRUNsYviJB3wzz \
2
./mvnw spring-boot:run


When the vault-demo-app starts, it will request the configuration to the config server, which in turn will make a REST to Vault. In the config server logs, with enough logging level, you will be able to see:

Java
 




xxxxxxxxxx
1


1
2020-05-01 01:21:02.691 DEBUG 21168 --- [nio-8888-exec-1] o.s.web.client.RestTemplate: 
2
 HTTP GET http://127.0.0.1:8200/v1/secret/data/vault-demo-app,dev


To increase logging so you see the above message, add the following to application.properties

logging.level.org.springframework.web=DEBUG

Go to http://localhost:8080 and verify that authentication with Okta works.

Finally, let’s seal Vault. Sealing allows you to lock Vault data to minimize damages when an intrusion is detected. In the container command line,

enter:

vault operator seal

Restart vault-demo-app and verify the configuration will not be retrieved as Vault is sealed. The vault-config-server logs should read:

503 Service Unavailable: [{"errors":["error performing token check: Vault is sealed"]}

To unseal Vault, run:

vault operator unseal {unsealKey}

Learn More About Encryption and Storing Secrets

Hopefully, you see the benefits of using a secrets management tool like Vault as a configuration backend, as opposed to storing secrets in a file, on a file system, or in a code repository. To learn more about Vault and Spring Cloud, check out the following links:

We have several related posts about encryption and storing secrets on this blog.

You can find the code for this tutorial at GitHub.

For more tutorials like this one, follow @oktadev on Twitter. We also have a YouTube channel you might like. If you have any questions, please leave a comment below!


 

 

 

 

Top