Using HashiCorp Vault’s Transit Secret Engine
Vault’s Transit Secret Engine offers an "encryption as a service" functionality. It allows to store encryption keys into Vault, and provides services to encrypt/decrypt and sign/verify arbitrary pieces of data. This brings several advantages, such as:
-
limited key exposure: keys never leave the Vault. Instead the data is sent to Vault to get encrypted/decrypted/signed/verified.
-
reliable and consistent algorithms: algorithms are implemented by Vault. It supports a wide variety, and there is only one implementation used for a given algorithm type, regardless of the client’s technology.
-
it relieves developers from having to embed cryptographic libraries into their applications.
In the context of Quarkus, the main (non administration) services are being covered:
-
encrypt: encrypt some data and return the cipher text
-
decrypt: decrypt the cipher text and return the clear data
-
rewrap: reencrypt a cipher text using the most recent key version
-
sign: sign a piece of data
-
verify signature: verify that a signature is correct for a piece of data
Prerequisites
To complete this guide, you need:
-
to complete the "Starting Vault" section of the Vault guide
-
roughly 15 minutes
-
an IDE
-
JDK 11+ installed with
JAVA_HOME
configured appropriately -
Apache Maven 3.8.1+
-
Docker installed
Setup
We assume there is a Vault running from the Vault guide, and the root token is known. The first step consists in activating the Transit Secret Engine, and creating the different keys:
docker exec -it dev-vault sh
export VAULT_TOKEN=s.5VUS8pte13RqekCB2fmMT3u2
vault secrets enable transit
# ==> Success! Enabled the transit secrets engine at: transit/
vault write -f transit/keys/my-encryption-key
# ==> Success! Data written to: transit/keys/my-encryption-key
vault write transit/keys/my-sign-key type=ecdsa-p256
# ==> Success! Data written to: transit/keys/my-sign-key
Note that you did not have to provide the key value. You only provide its name, and Vault will generate it for you and keep it secured.
Once the keys have been created, we now need to create a policy that provides access for it:
cat <<EOF | vault policy write vault-transit-quickstart-policy -
path "transit/encrypt/my-encryption-key" {
capabilities = [ "update" ]
}
path "transit/decrypt/my-encryption-key" {
capabilities = [ "update" ]
}
path "transit/sign/my-sign-key" {
capabilities = [ "update"]
}
path "transit/verify/my-sign-key" {
capabilities = [ "update" ]
}
EOF
# ==> Success! Uploaded policy: vault-transit-quickstart-policy
And finally, let’s add the vault-transit-quickstart-policy
to user bob
that was created in the Vault guide:
vault write auth/userpass/users/bob password=sinclair policies=vault-quickstart-policy,vault-transit-quickstart-policy
To check that the configuration is correct, try logging in:
vault login -method=userpass username=bob password=sinclair
You should see:
Success! You are now authenticated. The token information displayed below
is already stored in the token helper. You do NOT need to run "vault login"
again. Future Vault requests will automatically use this token.
Key Value
--- -----
token s.s93BVzJPzBiIGuYJHBTkG8Uw
token_accessor OKNipTAgxWbxsrjixASNiwdY
token_duration 768h
token_renewable true
token_policies ["default" "vault-quickstart-policy"]
identity_policies []
policies ["default" "vault-quickstart-policy"]
token_meta_username bob
Now set VAULT_TOKEN
to the token
above (instead of the root token), and try encrypting some data:
export VAULT_TOKEN=s.s93BVzJPzBiIGuYJHBTkG8Uw
# note: "my secret data" in base64 is "bXkgc2VjcmV0IGRhdGEK"
vault write transit/encrypt/my-encryption-key plaintext=bXkgc2VjcmV0IGRhdGEK
You should see:
Key Value
--- -----
ciphertext vault:v1:vIQxsLANFbcfKofJL55zjoIXV6MqAzvjKUUQLGg5pWTz0W2Qab/B4nEJaQ==
Encrypt and Decrypt
First, let’s create a simple Quarkus application with Vault and Jackson extensions:
mvn io.quarkus.platform:quarkus-maven-plugin:3.12.0:create \
-DprojectGroupId=org.acme \
-DprojectArtifactId=vault-transit-quickstart \
-DclassName="org.acme.quickstart.GreetingResource" \
-Dpath="/hello" \
-Dextensions="resteasy,vault,resteasy-jackson"
cd vault-transit-quickstart
Now, configure access to Vault from the application.properties
:
# vault url
quarkus.vault.url=http://localhost:8200
# vault authentication
quarkus.vault.authentication.userpass.username=bob
quarkus.vault.authentication.userpass.password=sinclair
Note that you did not need to specify the existence of a particular encryption key in the configuration. You only do so in special cases such as specifying the key type for upsert, or changing the signature algorithm, … Check the complete configuration for more information.
We can then add a new endpoint that will allow us to encrypt and decrypt data:
@Path("/transit")
@Produces(TEXT_PLAIN)
@Consumes(TEXT_PLAIN)
public class TransitResource {
@Inject
public VaultTransitSecretEngine transitSecretEngine;
@POST
@Path("/encrypt")
public String encrypt(String clearData) {
return transitSecretEngine.encrypt("my-encryption-key", clearData);
}
@POST
@Path("/decrypt")
public String decrypt(String cipherText) {
return transitSecretEngine.decrypt("my-encryption-key", cipherText).asString();
}
}
After compiling and starting the Quarkus application, let’s encrypt some data:
curl -X POST --data 'some secret data' --header "Content-Type: text/plain" http://localhost:8080/transit/encrypt
# ==> vault:v1:fN4P7WNjIegpb3lD/pSuhXvyONhGrI21gcKNcedk+5jpjguOw6JkqXYXlkY=
And decrypt back this cipher text:
curl -X POST --data 'vault:v1:fN4P7WNjIegpb3lD/pSuhXvyONhGrI21gcKNcedk+5jpjguOw6JkqXYXlkY=' --header "Content-Type: text/plain" http://localhost:8080/transit/decrypt
# ==> some secret data
Sign and Verify
Let’s add 2 new methods to our TransitResource
:
@POST
@Path("/sign")
public String sign(String input) {
return transitSecretEngine.sign("my-sign-key", input);
}
public static class VerifyRequest {
public String signature;
public String input;
}
@POST
@Path("/verify")
@Consumes(APPLICATION_JSON)
public Response verify(VerifyRequest request) {
transitSecretEngine.verifySignature("my-sign-key", request.signature, request.input);
return Response.accepted().build();
}
And start signing some data:
curl -X POST --data 'some secret data' --header "Content-Type: text/plain" http://localhost:8080/transit/sign
# ==> vault:v1:MEUCIQDl+nE4y4E878bkugGG6FG1/RsttaQnoWfZHppeuk4TnQIgTGWTtMhVPCzN8VH/EEr2qp5h34lI1bnEP6L1F+QQoPI=
And finally, let’s make sure the signature is matching our input data:
curl -v -X POST --data '{"input":"some secret data","signature":"vault:v1:MEUCIQDl+nE4y4E878bkugGG6FG1/RsttaQnoWfZHppeuk4TnQIgTGWTtMhVPCzN8VH/EEr2qp5h34lI1bnEP6L1F+QQoPI="}' --header "Content-Type: application/json" http://localhost:8080/transit/verify
# ==> ... < HTTP/1.1 202 Accepted
Conclusion
The Transit Secret Engine is a powerful tool in the enterprise. We have seen the most obvious functions of the interface, but the rest of the methods or flavors should not be overlooked:
-
For instance batch oriented methods are strongly recommended for mass operations (encrypt, decrypt, …)
-
Transit contexts allow key derivation where one key is used to derive other keys for specific named contexts (e.g. person names, person addresses, …)
-
Rewrapping allows to reencrypt data with the most recent key version when the Vault administrator decides to rotate keys
Feel free to look at the VaultTransitSecretEngine
interface plus the dedicated Transit Secret Engine configuration
properties in the Vault guide for all the details.