The application encrypts sensitive fields (document numbers, phone numbers, tokens) through the Stronghold API and stores only ciphertext in the database. The encryption key never leaves Stronghold, so a leaked database dump does not expose the data.

Goal

Set up a Transit key for an application, encrypt and decrypt a field, apply envelope encryption for large objects, rotate the key, re-encrypt data (rewrap), and prohibit decryption with old key versions.

Prerequisites

To follow this example, you need:

  • A Stronghold token with permissions to enable secrets engines and create policies.
  • The base64, jq, openssl, and xxd utilities for the examples.

Step 1. Create a key

Enable the Transit secrets engine and create a key:

  1. Enable the secrets engine:

    d8 stronghold secrets enable transit
  2. Create a named key for the application:

    d8 stronghold write -f transit/keys/orders type=aes256-gcm96

    Use a separate key for each application or data set.

Step 2. Separate permissions

Create separate policies for the application and for the re-encryption job:

  1. Application policy: encryption and decryption only:

    d8 stronghold policy write orders-app - <<'POLICY'
    path "transit/encrypt/orders" {
      capabilities = ["update"]
    }
    path "transit/decrypt/orders" {
      capabilities = ["update"]
    }
    path "transit/datakey/plaintext/orders" {
      capabilities = ["update"]
    }
    POLICY
  2. Re-encryption job policy: rewrap only, with no access to plaintext:

    d8 stronghold policy write orders-rewrap - <<'POLICY'
    path "transit/rewrap/orders" {
      capabilities = ["update"]
    }
    path "transit/keys/orders" {
      capabilities = ["read"]
    }
    POLICY

Leave key management (transit/keys/orders/*) to administrators.

Step 3. Encrypt and decrypt a field

Data is sent to Stronghold base64-encoded.

  1. Encrypt a value:

    d8 stronghold write -field=ciphertext transit/encrypt/orders plaintext=$(echo -n "4509 123456" | base64)

    Store the vault:v1:... result in a table column. The v1 prefix is the key version used for encryption.

  2. Decrypt the value:

    d8 stronghold write -field=plaintext transit/decrypt/orders ciphertext="vault:v1:..." | base64 --decode

To process many records in one request, use the batch_input parameter of the encrypt, decrypt, and rewrap endpoints.

Step 4. Use envelope encryption for large data

Sending files and large objects through the API is inefficient. Instead, request a data key: Stronghold returns it in plaintext for local encryption and in encrypted form for storage.

  1. Get a data key:

    d8 stronghold write -format=json -f transit/datakey/plaintext/orders > datakey.json
    jq -r '.data.plaintext' datakey.json | base64 --decode > dek.bin
    jq -r '.data.ciphertext' datakey.json > dek.enc
  2. Encrypt the file locally and delete the plaintext key:

    openssl rand -hex 16 > report.pdf.iv
    openssl enc -aes-256-cbc -K "$(xxd -p -c 64 dek.bin)" -iv "$(cat report.pdf.iv)" -in report.pdf -out report.pdf.enc
    shred -u dek.bin

    The data key is 256 bits long and is passed to openssl as a hexadecimal AES key with the -K flag. The report.pdf.iv file contains a random initialization vector (IV). Generate a new IV for each file. Store report.pdf.enc together with dek.enc and report.pdf.iv.

  3. To decrypt, recover the data key through Transit and decrypt the file with the same IV:

    d8 stronghold write -field=plaintext transit/decrypt/orders ciphertext="$(cat dek.enc)" | base64 --decode > dek.bin
    openssl enc -d -aes-256-cbc -K "$(xxd -p -c 64 dek.bin)" -iv "$(cat report.pdf.iv)" -in report.pdf.enc -out report.pdf
    shred -u dek.bin

If the data key is needed only for storage (for example, another service will decrypt it), use transit/datakey/wrapped/orders: the response does not contain the plaintext key.

Step 5. Rotate the key

Rotate the key manually or set up automatic rotation:

  1. Create a new key version:

    d8 stronghold write -f transit/keys/orders/rotate

    New data is encrypted with the latest version; old data can still be decrypted.

  2. To rotate automatically, set a period:

    d8 stronghold write transit/keys/orders/config auto_rotate_period=720h

Step 6. Re-encrypt data (rewrap)

Rewrap re-encrypts ciphertext with the latest key version without exposing plaintext. Run it for all table records, for example as a background job with the orders-rewrap policy:

d8 stronghold write -field=ciphertext transit/rewrap/orders ciphertext="vault:v1:..."

Write the vault:v2:... result back to the column.

Step 7. Prohibit decryption with old versions

When the database no longer contains ciphertext with old versions, raise the minimum decryption version:

d8 stronghold write transit/keys/orders/config min_decryption_version=2

Versions below min_decryption_version are archived and are not used for decryption. In an emergency, you can lower the value again.

To delete old versions permanently, use transit/keys/orders/trim with the min_available_version parameter. This action is irreversible.

Verification

Check the result:

  1. View the key state:

    d8 stronghold read transit/keys/orders

    Check the latest_version, min_decryption_version, and auto_rotate_period values.

  2. Try to decrypt vault:v1:... ciphertext after step 7: the request must fail.

  3. Make sure a token with the orders-rewrap policy cannot call transit/decrypt/orders.

Cleanup

Delete the objects created in this example:

  1. Delete the policies: d8 stronghold policy delete orders-app and d8 stronghold policy delete orders-rewrap.

  2. Delete the local files datakey.json, dek.enc, report.pdf.iv, and report.pdf.enc.

  3. To delete the test key, allow deletion and delete the key:

    d8 stronghold write transit/keys/orders/config deletion_allowed=true
    d8 stronghold delete transit/keys/orders

    Data encrypted with this key becomes inaccessible.

Additional resources