Supporting for Vault’s Secret Engines

Spring Vault ships with several extensions to support Vault’s various secret engines.

Specifically, Spring Vault ships with extensions for:

You can use all other backends through methods on VaultTemplate directly (VaultTemplate.read(…), VaultTemplate.write(…)).

Key-Value Version 1 ("unversioned secrets")

The kv secrets engine is used to store arbitrary secrets within the configured physical storage for Vault.

When running the kv secrets engine in a non-versioned way, only the most recently written value for a key is preserved. The benefits of non-versioned kv is a reduced storage size for each key, since no additional metadata or history is stored. Additionally, requests going to a backend configured this way are more performant because there are fewer storage calls and no locking for any given request.

Spring Vault ships with a dedicated Key-Value API to encapsulate differences between the individual Key-Value API implementations. VaultKeyValueOperations follows the Vault CLI design. That’s the primary command line tool for Vault providing commands such as vault kv get, vault kv put and so on.

You can use this API with both Key-Value engine versions by specifying the version and mount path. The following example uses the Key-Value version 1:

VaultOperations operations = new VaultTemplate(new VaultEndpoint());
VaultKeyValueOperations keyValueOperations = operations.opsForKeyValue("secret",
							VaultKeyValueOperationsSupport.KeyValueBackend.KV_1);

keyValueOperations.put("elvis", Collections.singletonMap("password", "409-52-2002"));

VaultResponse read = keyValueOperations.get("elvis");
read.getRequiredData().get("social-security-number");

VaultKeyValueOperations supports all Key-Value operations such as put, get, delete, list.

Alternatively, the API can be used through VaultTemplate because of its direct mapping and simple use, as keys and responses map directly to input and output keys. The following example illustrates writing and reading a secret at mykey. The kv secrets engine is mounted at secret:

VaultOperations operations = new VaultTemplate(new VaultEndpoint());

operations.write("secret/elvis", Collections.singletonMap("social-security-number", "409-52-2002"));

VaultResponse read = operations.read("secret/elvis");
read.getRequiredData().get("social-security-number");

You can find more details about the Vault Key-Value version 1 API in the Vault reference documentation.

Key-Value Version 2 ("versioned secrets")

You can run the kv secrets engine in one of two versions. This section explains using version 2. When running version 2 of the kv backend a key can retain a configurable number of versions. You can retrieve the metadata and data of the older versions. Additionally, you can use check-and-set operations to avoid unintentionally overwriting data.

Similar to Key-Value Version 1 ("unversioned secrets"), Spring Vault ships with a dedicated Key-Value API to encapsulate differences between the individual Key-Value API implementations. Spring Vault ships with a dedicated Key-Value API to encapsulate differences between the individual Key-Value API implementations. VaultKeyValueOperations follows the Vault CLI design. That is the primary command line tool for Vault, providing commands such as vault kv get, vault kv put, and so on.

You can use this API with both Key-Value engine versions by specifying the version and mount path. The following example uses Key-Value version 2:

VaultOperations operations = new VaultTemplate(new VaultEndpoint());
VaultKeyValueOperations keyValueOperations = operations.opsForKeyValue("secret",
							VaultKeyValueOperationsSupport.KeyValueBackend.KV_2);

keyValueOperations.put("elvis", Collections.singletonMap("social-security-number", "409-52-2002"));

VaultResponse read = keyValueOperations.get("elvis");
read.getRequiredData().get("social-security-number");

VaultKeyValueOperations supports all Key-Value operations, such as put, get, delete, list.

You can also interact with the specifics of the versioned key-value API. This is useful if you want to obtain a specific secret or you need access to the metadata.

VaultOperations operations = new VaultTemplate(new VaultEndpoint());
VaultVersionedKeyValueOperations versionedOperations = operations.opsForVersionedKeyValue("secret");

Versioned.Metadata metadata = versionedOperations.put("elvis",							(1)
					Collections.singletonMap("social-security-number", "409-52-2002"));

Version version = metadata.getVersion();												(2)

Versioned<Object> ssn = versionedOperations.get("elvis", Version.from(42));				(3)

Versioned<SocialSecurityNumber> mappedSsn = versionedOperations.get("elvis",			(4)
											Version.from(42), SocialSecurityNumber.class);

Versioned<Map<String,String>> versioned = Versioned.create(Collections					(5)
						.singletonMap("social-security-number", "409-52-2002"),
						Version.from(42));

versionedOperations.put("elvis", version);
1 Store secrets at elvis in that is available under the secret/ mount.
2 Storing data in the versioned backend returns metadata such as the version number.
3 The versioned Key-Value API allows retrieval of specific versions identified by the version number.
4 Versioned key-value secrets can be mapped into value objects.
5 When updating versioned secrets using CAS, the input must refer to the previously obtained version.

While using the kv v2 secrets engine through VaultTemplate is possible. It is not the most convenient approach since the API offers a different approach to context paths and how input/output is represented. Specifically, interaction with the actual secrets requires wrapping and unwrapping of the data section and introducing a data/ path segment between the mount and the secrets key.

VaultOperations operations = new VaultTemplate(new VaultEndpoint());

operations.write("secret/data/elvis", Collections.singletonMap("data",
			Collections.singletonMap("social-security-number", "409-52-2002")));

VaultResponse read = operations.read("secret/data/ykey");
Map<String,String> data = (Map<String, String>) read.getRequiredData().get("data");
data.get("social-security-number");

You can find more details about the Vault Key-Value version 2 API in the Vault reference documentation.

PKI (Public Key Infrastructure)

The pki secrets engine represents a backend for certificates by implementing certificate authority operations.

The PKI secrets engine generates dynamic X.509 certificates. With this secrets engine, services can get certificates without going through the usual manual process of generating a private key and CSR, submitting to a CA, and waiting for a verification and signing process to complete. Vault’s built-in authentication and authorization mechanisms provide the verification functionality.

Spring Vault supports issuing, signing, revoking certificates, and CRL retrieval through VaultPkiOperations. All other PKI functionality can be used through VaultOperations.

The following examples explain briefly the use of how to issue and revoke certificates:

VaultOperations operations = new VaultTemplate(new VaultEndpoint());
VaultPkiOperations pkiOperations = operations.opsForPki("pki");

VaultCertificateRequest request = VaultCertificateRequest.builder()								(1)
			.ttl(Duration.ofHours(48))
			.altNames(Arrays.asList("prod.dc-1.example.com", "prod.dc-2.example.com"))
			.withIpSubjectAltName("1.2.3.4")
			.commonName("hello.example.com")
			.build();

VaultCertificateResponse response = pkiOperations.issueCertificate("production", request); 		(2)
CertificateBundle certificateBundle = response.getRequiredData();

KeyStore keyStore = certificateBundle.createKeyStore("my-keystore");							(3)

KeySpec privateKey = certificateBundle.getPrivateKeySpec();										(4)
X509Certificate certificate = certificateBundle.getX509Certificate();
X509Certificate caCertificate = certificateBundle.getX509IssuerCertificate();

pkiOperations.revoke(certificateBundle.getSerialNumber());										(5)
1 Construct a certificate request by using the VaultCertificateRequest builder.
2 Request a certificate from Vault. Vault acts as certificate authority and responds with a signed X.509 certificate. The actual response is a CertificateBundle.
3 You can obtain generated certificates directly as Java KeyStore that contains public and private keys as well as the issuer certificate. KeyStore has a wide range of uses, which makes this format suitable to configure (for example a HTTP client, a database driver, or an SSL-secured HTTP server).
4 CertificateBundle allows accessing the private key and the public and issuer certificates directly through the Java Cryptography Extension API.
5 Once a certificate is no longer in use (or it was compromised), you can revoke it through its serial number. Vault includes the revoked certificate in its CRL.

You can find more details about the Vault PKI secrets API in the Vault reference documentation.

Token Authentication Backend

This backend is an authentication backend that does not interact with actual secrets. Rather, it gives access to access token management. You can read more about Token-based authentication in the authentication methods chapter.

The token authentication method is built-in and automatically available at /auth/token. It lets users authenticate using a token, as well to create new tokens, revoke secrets by token, and more.

When any other auth method returns an identity, Vault core invokes the token method to create a new unique token for that identity.

You can also use the token store to bypass any other auth method. You can create tokens directly, as well as perform a variety of other operations on tokens, such as renewal and revocation.

Spring Vault uses this backend to renew and revoke the session tokens supplied by the configured authentication method.

The following examples show how to request, renew and revoke a Vault token from within your application:

VaultOperations operations = new VaultTemplate(new VaultEndpoint());
VaultTokenOperations tokenOperations = operations.opsForToken();

VaultTokenResponse tokenResponse = tokenOperations.create();                          (1)
VaultToken justAToken = tokenResponse.getToken();

VaultTokenRequest tokenRequest = VaultTokenRequest.builder().withPolicy("policy-for-myapp")
									.displayName("Access tokens for myapp")
									.renewable()
									.ttl(Duration.ofHours(1))
									.build();

VaultTokenResponse appTokenResponse = tokenOperations.create(tokenRequest);          (2)
VaultToken appToken = appTokenResponse.getToken();

tokenOperations.renew(appToken);                                                     (3)

tokenOperations.revoke(appToken);                                                    (4)
1 Create an token by applying role defaults.
2 Using the builder API, you can define fine-grained settings for the token to request. Requesting a token returns a VaultToken, which is used as value object for Vault tokens.
3 You can renew tokens through the Token API. Typically, that is done by SessionManager to keep track of the Vault session token.
4 Tokens can be revoked if needed through the Token API. Typically, that is done by SessionManager to keep track of the Vault session token.

You can find more details about the Vault Token Auth Method API in the Vault reference documentation.

Transit Backend

The transit secrets engine handles cryptographic functions on data in-transit. Vault does not store the data sent to this secrets engine. It can also be seen as "cryptography as a service" or "encryption as a service". The transit secrets engine can also sign and verify data, generate hashes and HMACs of data, and act as a random bytes source.

The primary use case for transit is to encrypt data from applications while still storing that encrypted data in some primary data store. This relieves the burden of proper encryption and decryption from application developers and pushes the burden onto the operators of Vault.

Spring Vault supports a wide range of Transit operations:

  • Key creation

  • Key reconfiguration

  • Encryption/Decryption/Rewrapping

  • HMAC computation

  • Signing and signature verification

All operations within transit are centered around keys. The Transit engine supports the versioning of keys and a variety of key types. Note that the key type may impose a limitation on which operations can used.

The following examples shows how to create a key and how to encrypt and decrypt data:

VaultOperations operations = new VaultTemplate(new VaultEndpoint());
VaultTransitOperations transitOperations = operations.opsForTransit("transit");

transitOperations.createKey("my-aes-key", VaultTransitKeyCreationRequest.ofKeyType("aes128-gcm96"));	(1)

String ciphertext = transitOperations.encrypt("my-aes-key", "plaintext to encrypt");					(2)

String plaintext = transitOperations.decrypt("my-aes-key", ciphertext);									(3)
1 First, we need a key to begin with. Each key requires the type to be specified. aes128-gcm96 supports encryption, decryption, key derivation, and convergent encryption, of which we need encryption and decryption for this example.
2 Next, we encrypt a String that contains the plain text that should be encrypted. The input String uses the default Charset to encode the string into its binary representation. Requesting a token returns a VaultToken, which is used as value object for Vault tokens. The encrypt method returns Base64-encoded ciphertext, typically starting with vault:.
3 To decrypt ciphertext into plain text, call the decrypt method. It decrypts the ciphertext and returns a String that is decoded using the default charset.

The preceeding example uses simple strings for cryptographic operations. While it is a simple approach, it bears the risk of charset misconfiguration and is not binary-safe. Binary safety is required when the plain text uses a binary representation for data such as images, compressed data, or binary data structures.

To encrypt and decrypt binary data, use the Plaintext and Ciphertext value objects that can hold binary values:

byte [] plaintext = "plaintext to encrypt".getBytes();

Ciphertext ciphertext = transitOperations.encrypt("my-aes-key", Plaintext.of(plaintext));			(1)

Plaintext decrypttedPlaintext = transitOperations.decrypt("my-aes-key", ciphertext);				(2)
1 Assuming a key my-aes-key is already in place, we’re encrypting the Plaintext object. In return, the encrypt method returns a Ciphertext object.
2 The Ciphertext object can be used directly for decryption and returns a Plaintext object.

Plaintext and Ciphertext come with a contextual object, VaultTransitContext. It is used to supply a nonce value for convergent encryption and for a context value to make use of key derivation.

Transit allows for signing plain text and verifying the signature for a given plain text. Sign operations require an asymmetric key, typically using Elliptic Curve Cryptography or RSA.

Signatures use the public/private key split to ensure authenticity.
The signer uses its private key to create a signature. Otherwise, anybody would be able to sign messages in your name. The verifier uses the public key part to verify the signature. The actual signature is typically a hash value.

Internally, the hash gets computed and encrypted using the private key to create the final signature. The verification decrypts the signature message, computes their own hash for the plain text and compares both hash values to check whether the signature is valid or not.
byte [] plaintext = "plaintext to sign".getBytes();

transitOperations.createKey("my-ed25519-key", VaultTransitKeyCreationRequest.ofKeyType("ed25519"));	(1)

Signature signature = transitOperations.sign("my-ed25519-key", Plaintext.of(plaintext));			(2)

boolean valid = transitOperations.verify("my-ed25519-key", Plaintext.of(plaintext), signature);		(3)
1 Signing requires an asymmetric key. You can use any Elliptic Curve Cryptography or RSA key type. Once the key is created, you have all the prerequisites in place to create a signature.
2 The signature gets created for a plain text message. The returned Signature contains an ASCII-safe string that uses Base64 characters.
3 To verify the signature, the verification requires a Signature object and the plain text message. As the return value, you get whether the signature was valid or not.

You can find more details about the Vault Transit Backend in the Vault reference documentation.