Vault Cubbyhole authentication and its integration with Spring framework

 

1. Background

For micro service, it's essential not to bundle secrets into our code base, but to move them to an external central managed secrets store.  In this article, I'm going to introduce how to use HashiCorp’s Vault to externalize JAVA Spring micro service application configurations. With Vault you have a central place to manage external secret properties for applications across all environments.

There are multiple authentication methods from Vault, but most of them are not suitable for Spring micro service, or not safe enough. When the application starts up I want all the properties to be automatically loaded into Spring context without interaction with human, so that the secrets can be used just like they are loaded from Spring's properties. The solution is to use single-use Vault Cubbyhole wrapping token to deliver the original token to the application.

2. Vault server

We simply use docker compose to set up a Vault server.
docker-compose.yml:
version: '3.4'

services:
    vault:
      image: vault:1.9.3
      volumes:
        - ./config:/vault/config
        - vault-policies:/vault/policies
        - vault-data:/vault/data
      ports:
        - 8200:8200
      environment:
        - VAULT_ADDR=http://0.0.0.0:8200
        - VAULT_API_ADDR=http://0.0.0.0:8200
        - VAULT_ADDRESS=http://0.0.0.0:8200
      cap_add:
        - IPC_LOCK
      command: vault server -config=/vault/config/vault.json
volumes:
  vault-data:
  vault-policies:

config/vault.json:


{    
    "ui":true,
    "listener":  {                     
      "tcp":  {                        
        "address":  "0.0.0.0:8200",  
        "tls_disable":  "true"         
      }                                
    },                                 
    "backend": {                       
      "file": {                        
        "path": "/vault/file"          
      }                                
    },                                 
    "default_lease_ttl": "168h",       
    "max_lease_ttl": "0h",
    "api_addr": "http://0.0.0.0:8200"
  }  

3. Cubbyhole wrapping token

The term cubbyhole comes from an Americanism where you get a "locker" or "safe place" to store your belongings or valuables. In Vault, the cubbyhole is your "locker". All secrets are namespaced under your token. If that token expires or is revoked, all the secrets in its cubbyhole are revoked as well.

Think of a case where you have a trusted entity (Java application, Jenkins, etc.) which reads secrets from Vault. This trusted entity must obtain a token. If the trusted entity or its host machine was rebooted, it must re-authenticate with Vault using a valid token. How can you securely distribute the initial token to the trusted entity?

Use Vault's cubbyhole wrapping token where the initial token is stored in the cubbyhole secrets engine. The wrapped secret can be unwrapped using the single-use wrapping token. Even the user or the system created the initial token won't see the original value. The wrapping token is short-lived and can be revoked just like any other tokens so that the risk of unauthorized access can be minimized.

Let's say if you already set up a Vault server and have a token to login the secrets store, then you just need the following REST API to wrap your original token so that it can be safely delivered to your Java application. For example, you can put the wrapping token into the destination server's Environment variables. You don't need admin permission for a wrapping token, every user can wrap their own tokens.

POST http://localhost:8200/v1/auth/token/renew-self
X-Vault-Token: s.TArdSqRh0ScZCoRCNfVIUsSh
X-Vault-Wrap-TTL: 36000
{
"request_id": "",
"lease_id": "",
"renewable": false,
"lease_duration": 0,
"data": null,
"wrap_info": {
"token": "s.QoKtaz80lFZdiHekBatcCYSq",
"accessor": "biVtDRn4ZeqinbbLaTTOFiq2",
"ttl": 36000,
"creation_time": "2022-02-09T08:19:48.96912585Z",
"creation_path": "auth/token/renew-self",
"wrapped_accessor": "tFpJdDmqLtRc4rU5oc5OZD49"
},
"warnings": null,
"auth": null
}

As you can see, we just need supply a header - X-Vault-Wrap-TTL to tell the Cubbyhole engine how long we want the wrapping token to last, then we will get the response with the wrapping token, which is a single-use token that can be used by the application to unwrap the original token. 

Benefits of using the Cubbyhole wrapping token:

  • It provides cover by ensuring that the value being transmitted across the wire is not the actual secret. It's a reference to the secret.
  • It provides malfeasance detection by ensuring that only a single party can ever unwrap the token and see what's inside
  • It limits the lifetime of the secret exposure
    • The TTL of the response-wrapping token is separate from the wrapped secret's lease TTL

4. Spring Vault configuration

Spring provides a vault client library that can be used to pull secrets from Vault when the application starts up. 

The following sample code configures Cubbyhole wrapping token authentication for Spring boot application. So the property - 'token' should be supplied as a wrapping token rather than original token. You can get the wrapping token by step 2.  The configuration will also load all the k/v secrets from the path specified with 'vauleSources' into Spring context.

<dependency>
<groupId>org.springframework.vault</groupId>
<artifactId>spring-vault-core</artifactId>
<version>2.3.2</version>
</dependency>
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.vault.authentication.CubbyholeAuthentication;
import org.springframework.vault.authentication.CubbyholeAuthenticationOptions;
import org.springframework.vault.core.env.VaultPropertySource;
import org.springframework.vault.authentication.ClientAuthentication;
import org.springframework.vault.client.VaultEndpoint;
import org.springframework.vault.config.AbstractVaultConfiguration;
import org.springframework.vault.core.VaultTemplate;
import org.springframework.vault.support.VaultToken;

import javax.annotation.PostConstruct;
import java.net.URI;
import java.util.List;

@Configuration
@PropertySource("vault.properties")
public class SpringVaultAuthConfiguration extends AbstractVaultConfiguration {

@Value("${vault.uri}")
private URI vaultUri;

@Value("${vault.token}")
private String token;

@Value("#{'${vault.sources:}'.split(',')}")
private List<String> vaultSources;

@Autowired
private ConfigurableEnvironment environment;

@Autowired
private VaultTemplate vaultTemplate;

@Override
public VaultEndpoint vaultEndpoint() {
return VaultEndpoint.from(vaultUri);
}

@Override
public ClientAuthentication clientAuthentication() {
CubbyholeAuthenticationOptions.CubbyholeAuthenticationOptionsBuilder builder = CubbyholeAuthenticationOptions.builder().wrapped().initialToken(VaultToken.of(token));
return new CubbyholeAuthentication(builder.build(), this.restOperations());
}

@PostConstruct
public void setPropertySource() {
MutablePropertySources sources = environment.getPropertySources();
vaultSources.stream().forEach(vs -> {
sources.addFirst(new VaultPropertySource(vaultTemplate, vs));
});
}
}

Comments

Popular posts from this blog

Build J2EE micro services architecture by using Spring Boot, Spring Cloud, Spring Security OAuth2, KeyCloak

NGINX and HTTPs with Let’s Encrypt, Certbot, and Cron dockerization in production