Encrypt Data Outside Vault Using the Transit Secrets Engine
Use HashiCorp Vault transit as encryption-as-a-service: send plaintext, store ciphertext elsewhere, and audit every key use.
04 May 2026, 08:35 UTC

The problem: encrypted columns without key management
Your application needs to store sensitive values—such as account numbers or identifiers—in a database or object bucket. You do not want the application to generate, rotate, or protect encryption keys itself, and you do not want Vault to hold the plaintext. HashiCorp Vault’s transit secrets engine fits this split: it acts as encryption-as-a-service. The application sends plaintext to Vault, receives ciphertext, and stores that ciphertext wherever it already stores data. Vault performs the cryptographic operation and does not retain the plaintext.
Useful takeaway: treat transit as a remote cryptographic boundary, not a secret store. Your application owns the ciphertext; Vault owns the key operations and the access decisions.
How the encrypt-then-store pattern works
Plaintext is the readable value your application wants to protect. Ciphertext is the output Vault returns after encrypting that value. A transit key is a named cryptographic key in Vault that performs encrypt, decrypt, rewrap, and datakey operations. Vault does not store the plaintext after the operation completes, which keeps Vault out of your data path for reads.
Access is controlled by Vault policies. A policy can grant an application role permission to encrypt with a specific key but not decrypt, or vice versa. Every operation is sent to an audit device, which logs the key name, operation type, and client identity. This gives you a record of who asked Vault to touch which key.
Worked example: local verification
Run these commands in a terminal where the Vault CLI is installed and can reach a local Vault instance. This example uses Vault dev mode, which is intended for testing only; data is lost when the dev server stops unless you configure persistence.
- Start Vault in dev mode and set the address:
vault server -dev -dev-root-token-id="dev-token"
export VAULT_ADDR=http://127.0.0.1:8200
vault login dev-token
Run the server command in its own terminal. The remaining commands require a token with permission to mount secrets engines and manage transit keys; the dev root token has these permissions.
- Enable the transit engine:
vault secrets enable transit
Expected check: Vault reports success and the engine appears under vault secrets list.
- Create a named transit key. Replace
<KEY_NAME>with an identifier for your application:
vault write -f transit/keys/<KEY_NAME>
Required capability: transit/keys/<KEY_NAME> with create and update permissions.
- Encrypt a value. Replace
<PLAINTEXT>with the value to protect:
vault write transit/encrypt/<KEY_NAME> plaintext=$(base64 <<< '<PLAINTEXT>')
Required capability: transit/encrypt/<KEY_NAME> with create permission. A successful call returns ciphertext. Do not use this exact command in production without reviewing how your application handles base64 encoding and ciphertext storage.
- Decrypt the value. Replace
<CIPHERTEXT>with the value returned by the encrypt step:
vault write transit/decrypt/<KEY_NAME> ciphertext=<CIPHERTEXT>
Required capability: transit/decrypt/<KEY_NAME> with create permission. Expected check: the decoded plaintext matches the original value.
- Confirm audit logging. If you enabled the file audit device, inspect its log:
vault audit list
tail -f /var/log/vault-audit.log
Look for entries containing the transit key name and the encrypt or decrypt operation. The exact path depends on your audit device configuration.
Risk: dev mode uses an in-memory backend and an unsealed state by default. Never use dev mode or a root token for production key material. Compare observed behavior against the release notes for your Vault version, because transit endpoints and defaults can change between major releases.
Key rotation does not rewrite old data
Rotating a transit key creates a new key version. Ciphertext created under an older version remains valid until you explicitly re-encrypt it or block older versions. Use min_encryption_version to tell Vault to reject encryption under versions below a threshold:
vault write transit/keys/<KEY_NAME> min_encryption_version=2
To refresh existing ciphertext, call the rewrap endpoint:
vault write transit/rewrap/<KEY_NAME> ciphertext=<OLD_CIPHERTEXT>
Your application must fetch the old ciphertext from its database, send it to Vault for rewrap, and store the result. Vault does not scan external systems and re-encrypt data automatically.
Trade-offs and operational limits
- Network dependency: Every encrypt or decrypt call is a round trip to Vault. Avoid calling transit on every read if latency matters; consider caching ciphertext or using a data key for bulk operations.
- Disaster recovery: Losing the Vault cluster, unseal keys, or backups can make existing ciphertext permanently undecryptable. Test backup and restore procedures before storing production ciphertext.
- Exportable keys: Avoid marking transit keys exportable unless you have a specific requirement. Exported key material weakens the boundary model.
- Seal status: A sealed Vault cannot perform transit operations. Cluster size and replication affect throughput under high volume.
Closing checklist
- Enable transit in a staging Vault instance, not dev mode.
- Create a least-privilege policy that allows only the required encrypt or decrypt operations on specific keys.
- Enable an audit device and verify that encrypt and decrypt calls appear in logs.
- Test key rotation, rewrap, and disaster-recovery restore with non-production data.
- Set
min_encryption_versionafter you have re-encrypted or confirmed you can still decrypt older ciphertext.
Transit works best when your application treats Vault as a cryptographic service and keeps ciphertext in its own data store. Verify round-trip behavior and audit logging in your environment before relying on it for production values.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.