---
title: "HashiCorp Vault Transit Secrets Engine"
description: "The issuer API can leverage HashiCorp Vault's Transit Secrets Engine to hook into the \"cryptography as a service\" or \"encryption as a service\" features provided by HashiCorp Vault. With that, the issuer API can sign and…"
stack: "Community Stack (open source) — version 0.21.0"
stack_version: "0.21.0"
stack_comparison: https://docs.walt.id/community-vs-enterprise.md
canonical_url: https://docs.walt.id/community-stack/issuer/key-management/hashicorp-vault
generated: 2026-07-20
---
# HashiCorp Vault Transit Secrets Engine

Video: https://youtu.be/htSvJ-rIHUk

The issuer API can leverage HashiCorp
Vault's [Transit Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/transit)
to hook into the "cryptography as a service" or "encryption as a service" features provided
by [HashiCorp Vault](https://developer.hashicorp.com/vault). With
that, the issuer API can sign and verify credentials whilst keeping the secrets (signing keys) in an external
secure environment.

If you are new to HashiCorp Vault, make sure to check out the
guides [here](https://developer.hashicorp.com/vault/tutorials/getting-started/getting-started-intro).
The rest of this document assumes you already know how Vault works and have a transit secrets engine setup and
running.

## Setting Up Authentication Method

Once you have the HashiCorp Server up and running and enabled the transit secrets engine mentioned above, we need to
either enable AppRole Authentication or User/Pass Authentication to use HashiCorp Vault with the walt.id issuer API.

### Setup Policy
Before enabling and configuring authentication methods, a policy must be created to define the permissions and actions
that authenticated users or roles are allowed to perform.

**Create The Policy File**

```bash
echo 'path "transit/*" {
  capabilities = ["create", "update", "read", "delete", "list"]
}' > transit-policy.hcl

```

**Check the Client Configuration**

If you are configuring vault locally make sure that the client is configured to 
communicate via HTTP. This can be achieved by setting the `VAULT_ADDR` environment 
variable.

```bash
export VAULT_ADDR=https://127.0.0.1:8200
```

**Upload the Policy**
```bash
vault policy write transit-policy transit-policy.hcl
# Expected result:
# Success! Uploaded policy: transit-policy
```

### Setup Auth Method

**Option: AppRole Auth**

To enable AppRole auth you have two options, the Web UI of HashiCorp Vault or the CLI:

**Option: Web UI**

1.	Navigate to `Access`.
2. Choose `Authentication Methods`.
3.	Click on `Enable new method`.
4.	Select `Generic / AppRole`.
5.	Click `Enable method`.

_Please note: You might want to disable lockout: select the created approle method,
press "Configure", scroll to "User lockout configuration" and select "Disable lockout for this mount"._

**Option: CLI**

In the CLI enter:

```bash
vault auth enable approle
```

_Please note: You might want to disable lockout: Use the `disable_lockout` parameter._

### Configure AppRole

**Add a Role**

```bash
vault write auth/approle/role/my-role token_type=batch
# Additional parameters can include:
# secret_id_ttl, token_num_uses, token_ttl, token_max_ttl, secret_id_num_uses
```

**Show Role ID**
```bash
vault read auth/approle/role/my-role/role-id
# Save the value of role_id for later use.
```

**Issue a Secret ID**

```bash
vault write -f auth/approle/role/my-role/secret-id
# Save the value of secret_id for later use.
```

**Associate Policy with AppRole**
```bash
vault write auth/approle/role/my-role token_policies="transit-policy"
# Expected result:
# Success! Data written to: auth/approle/role/my-role
```

If you already have a token policy with the AppRole:
```bash
vault write auth/approle/role/my-role token_policies="existing-policy,transit-policy"
```

**Option: User/Pass Auth**

**Option: CLI**

**Enable Userpass**

```bash
vault auth enable userpass
# Alternatively with a custom path
vault auth enable -path=<path> userpass
# Expected result:
# Success! Enabled userpass auth method at: userpass/
```

**Add User**

```bash
vault write auth/<userpass:path>/users/myuser password=mypassword policies=mypolicy
# Creates user "myuser", password "mypassword", associated with "mypolicy".
# Example policy: transit-policy (see above).
```

## Key Creation

To create the key you can use the onboard endpoint provided by the issuer API or the CLI, UI or the API from
Vault's [Transit Secrets Engine](https://developer.hashicorp.com/vault/docs/secrets/transit).

The issuer API accepts walt.id `keyType` values and maps them to the matching HashiCorp Vault Transit key type before creating or resolving the key.

| walt.id `keyType` | Vault Transit key type |
|---|---|
| `Ed25519` | `ed25519` |
| `secp256r1` | `ecdsa-p256` |
| `secp384r1` | `ecdsa-p384` |
| `secp521r1` | `ecdsa-p521` |
| `RSA` | `rsa-2048` |
| `RSA3072` | `rsa-3072` |
| `RSA4096` | `rsa-4096` |

`secp256k1` is not supported by the TSE integration. See the HashiCorp Vault Transit Secrets Engine key types [full list](https://developer.hashicorp.com/vault/docs/secrets/transit#key-types) for the provider-side names.

### Creation via Issuer API

**Option: CURL**

**Endpoint:** `/onboard/issuer` | [API Reference](https://issuer.demo.walt.id/swagger/index.html#/Issuer%20onboarding/post_onboard_issuer)

**Example Request**

**Option: AppRole Auth**

```bash
curl -X 'POST' \
  'https://issuer.demo.walt.id/onboard/issuer' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "roleId": "my_role",
        "secretId": "my_secret"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}'
```

**Body**

```json
{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "roleId": "my_role",
        "secretId": "my_secret"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}

```

**Body Parameters**

- `key`
    - `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `TSE` (managed by
      HashiCorp Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
    - `keyType`: _String_ - the algorithm used to generate the key. For Vault TSE, use one of `Ed25519`, `secp256r1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`.
    - `config`
        - `server`: _URL_ - The endpoint of your Vault instance. Following the structure of
          `https://<yourHost>/v1/<pathOfTransitEngine>`, e.g. "https://vault.walt.id/v1/transit". By default the transit
          engine will live at /transit.
        -  `auth`:
            - **AppRole Auth:**
                - `roleId`: _String_ - The Role ID for AppRole authentication.
                - `secretId`: _String_ - The Secret ID for AppRole authentication.
            - **User/Pass Auth:**
                - `username`: _String_ - The username for user/pass authentication.
                - `password`: _String_ - The password for user/pass authentication.
            - **AccessToken Auth (Deprecated):**
                - `accessKey`: _String_ - The access token for token-based authentication.
        -  `namespace`: _String_ (optional) - The namespace within the Vault instance.
- `did`:
    - `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.

**Option: User/Pass Auth**

```bash
curl -X 'POST' \
  'https://issuer.demo.walt.id/onboard/issuer' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "username": "foo",
        "password": "bar"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}'
```

**Body**

```json
{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "username": "foo",
        "password": "bar"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}
```

**Body Parameters**

- `key`
    - `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `TSE` (managed by
      HashiCorp Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
    - `keyType`: _String_ - the algorithm used to generate the key. For Vault TSE, use one of `Ed25519`, `secp256r1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`.
    - `config`
        - `server`: _URL_ - The endpoint of your Vault instance. Following the structure of
          `https://<yourHost>/v1/<pathOfTransitEngine>`, e.g. "https://vault.walt.id/v1/transit". By default the transit
          engine will live at /transit.
        -  `auth`:
            - **AppRole Auth:**
                - `roleId`: _String_ - The Role ID for AppRole authentication.
                - `secretId`: _String_ - The Secret ID for AppRole authentication.
            - **User/Pass Auth:**
                - `username`: _String_ - The username for user/pass authentication.
                - `password`: _String_ - The password for user/pass authentication.
            - **AccessToken Auth (Deprecated):**
                - `accessKey`: _String_ - The access token for token-based authentication.
        -  `namespace`: _String_ (optional) - The namespace within the Vault instance.
- `did`:
    - `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.

**Option: AccessToken Auth (Deprecated)**

```bash
curl -X 'POST' \
  'https://issuer.demo.walt.id/onboard/issuer' \
  -H 'accept: application/json' \
  -H 'Content-Type: application/json' \
  -d '{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "accessKey": "123456"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}'
```

**Body**

```json
{
  "key": {
    "backend": "tse",
    "keyType": "Ed25519",
    "config": {
      "server": "http://127.0.0.1:8200/v1/transit",
      "auth": {
        "accessKey": "123456"
      },
      "namespace": "optional"
    }
  },
  "did": {
    "method": "key"
  }
}
```

**Body Parameters**

- `key`
    - `backend`: _String_ - Specifies the storage type of key. It can be `jwk` (managed by you), `TSE` (managed by
      HashiCorp Vault) and others. Learn more about different types [here](https://docs.walt.id/community-stack/issuer/key-management/overview.md).
    - `keyType`: _String_ - the algorithm used to generate the key. For Vault TSE, use one of `Ed25519`, `secp256r1`, `secp384r1`, `secp521r1`, `RSA`, `RSA3072`, or `RSA4096`.
    - `config`
        - `server`: _URL_ - The endpoint of your Vault instance. Following the structure of
          `https://<yourHost>/v1/<pathOfTransitEngine>`, e.g. "https://vault.walt.id/v1/transit". By default the transit
          engine will live at /transit.
      -  `auth`:
         - **AppRole Auth:**
            - `roleId`: _String_ - The Role ID for AppRole authentication.
            - `secretId`: _String_ - The Secret ID for AppRole authentication.
         - **User/Pass Auth:**
           - `username`: _String_ - The username for user/pass authentication.
           - `password`: _String_ - The password for user/pass authentication.
         - **AccessToken Auth (Deprecated):**
           - `accessKey`: _String_ - The access token for token-based authentication.
      -  `namespace`: _String_ (optional) - The namespace within the Vault instance.
- `did`:
    - `method`: _String_ - Specifies the DID method. It can be key, jwk, web, cheqd.

---

**Example Response**

The onboard/issuer endpoint will return an object containing both the generated key in JWK format and the related DID.

```json
{
  "issuerKey": {
    "type": "tse",
    "server": "http://127.0.0.1:8200/v1/transit",
    "auth": {
      "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
      "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
    },
    "id": "k732628061",
    "_publicKey": [
      86,
      -55,
      122,
      -77,
      109,
      -60,
      61,
      -40,
      -122,
      -57,
      -101,
      -53,
      69,
      -125,
      12,
      92,
      -9,
      89,
      61,
      -78,
      116,
      -76,
      111,
      -58,
      -63,
      -122,
      -26,
      79,
      -110,
      -32,
      -73,
      -98
    ],
    "_keyType": "Ed25519"
  },
  "issuerDid": "did:key:z6MkkJ3JAhCpSGe5QD9UQ2WGzna7kcswa9ahMC2o3Dd1KRXb"
}
```

**Properties**

- `type`: _String_ - the type of key can be either "tse" when using HashiCorp Vault or "jwk" when providing the key in
  full as JWK.
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure
  of `https://<yourHost>/v1/<pathOfTransitEngine>`, e.g. "https://vault.walt.id/v1/transit". By default
  the transit engine will live at `/transit`.
-  `auth`:
    - **AppRole Auth:**
       - `roleId`: _String_ - The Role ID for AppRole authentication.
       - `secretId`: _String_ - The Secret ID for AppRole authentication.
    - **User/Pass Auth:**
       - `username`: _String_ - The username for user/pass authentication.
       - `password`: _String_ - The password for user/pass authentication.
    - **AccessToken Auth (Deprecated):**
       - `accessKey`: _String_ - The access token for token-based authentication.
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
  resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
  and reducing network requests.

## Key Usage

Once you have successfully created a key that is one of the supported types listed above, you can use it in
sign and issue operations offered by the issuer API.

If you've already had a look at
our `/sign`, `/issue`, `/batchIssue`
endpoints, you have seen that they all follow a similar request body structure, where the key that should be
used for signing credentials is provided via the `issuerKey` property. Now instead of providing the key as JWK, we
provide
a reference to a key stored in Vault with the required parameters and access credentials.

Below you can see an example of
`issuerKey` object referencing a key stored in Vault.

**Option: Example IssuerKey Object**

```json 
{
  "type": "tse",
  "server": "https://vault.walt.id/v1/transit",
  "auth": {
    "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
    "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
  },
  "id": "k1474327600",
  "_publicKey": [
    -17,
    83,
    1,
    26,
    9,
    -104,
    -64,
    -50,
    48,
    92,
    -112,
    -76,
    7,
    90,
    -40,
    -100,
    72,
    106,
    -51,
    -58,
    64,
    -26,
    73,
    121,
    26,
    -118,
    39,
    -8,
    -25,
    -34,
    82,
    37
  ],
  "_keyType": "Ed25519"
}
```

**Properties**

- `type`: _String_ - the type of key can be either "tse" when using HashiCorp Vault or "jwk" when providing the key in
  full as JWK.
- `server`: _URL_ - The endpoint of your Vault instance. Following the structure
  of `https://<yourHost>/v1/<pathOfTransitEngine>`, e.g. "https://vault.walt.id/v1/transit". By default
  the transit engine will live at `/transit`.
-  `auth`:
    - **AppRole Auth:**
        - `roleId`: _String_ - The Role ID for AppRole authentication.
        - `secretId`: _String_ - The Secret ID for AppRole authentication.
    - **User/Pass Auth:**
        - `username`: _String_ - The username for user/pass authentication.
        - `password`: _String_ - The password for user/pass authentication.
    - **AccessToken Auth (Deprecated):**
        - `accessKey`: _String_ - The access token for token-based authentication.
- `id`: _String_ - the ID of the key in the Transit Engine.
- `_publicKey` (optional): _Array_ - The public key can be fetched by the issuer API or directly provided, saving
  resources and reducing network requests.
- `_keyType` (optional): _String_ - The key type can be fetched by the issuer API or directly provided, saving resources
  and reducing network requests.

## Example Issuance Request

Below you can see example issuance request
to [`jwt/issue`](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue) using
a key created in the Vault Transit Secrets Engine to
sign the credential.

**Option: CURL**

[Api Reference](https://issuer.demo.walt.id/swagger/index.html#/Credential%20Issuance/post_openid4vc_jwt_issue)

```bash
curl -X 'POST' \
  'https://issuer.demo.walt.id/openid4vc/jwt/issue' \
  -H 'accept: text/plain' \
  -H 'statusCallbackUri: https://example.com/$id' \
  -H 'Content-Type: application/json' \
  -d '{
  "issuerKey": {
    "type": "tse",
    "server": "https://vault.walt.id/v1/transit",
    "auth": {
      "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
      "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
    },
    "id": "k-1481938705"
  },
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialData": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1",
      "https://purl.imsglobal.org/spec/ob/v3p0/context.json"
    ],
    "id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
    "type": [
      "VerifiableCredential",
      "OpenBadgeCredential"
    ],
    "name": "JFF x vc-edu PlugFest 3 Interoperability",
    ... 
  },
  "mapping": {
    "id": "<uuid>",
     ... 
  }
}'
```
**Header Parameters**

- `statusCallbackUri`: _URL_ - Receive updates on the created issuance process, e.g. when a credential was successfully
  claimed. The parameter expects a URL which can accept a JSON POST request. The URL can also hold a `$id`, which will
  be
  replaced by the issuance session id. For example: `https://myurl.com/$id`, `https://myurl.com`
  or `https://myurl.com/test/$id`
  <br/>
  <br />
  <details><summary>Expand To Learn More</summary>

  <br /> 

  **Body**

  The data send to the provided URL will contain a JSON body:
    - `id` : _String_ - the issuance session id
    - `type`: _String_ - the event type
    - `data`: _JsonObject_ - the data for the event

  **Event Types**

  Possible events (event types) and their data are:
    - `resolved_credential_offer` with the credential offer as JSON (in our Web Wallet: called when the issuance offer is
      entered into the wallet, but not processing / accepted yet)
    - `requested_token` with the issuance request for the token as json object (called for the token required to receive the
      credentials)

  Credential issuance (called for every credential that's issued (= requested from wallet))
    - `jwt_issue` with `jwt` being the issued jwt
    - `sdjwt_issue` with `sdjwt` being the issued sdjwt
    - `batch_jwt_issue` with `jwt` being the issued jwt
    - `batch_sdjwt_issue` with `sdjwt` being the issued sdjwt
    - `generated_mdoc` with `mdoc` being the CBOR (HEX) of the signed mdoc

  To allow for secure business logic flows, if a callback URL is set, and it cannot be reached, the issuance will not
  commence further (after that point). If no callback URL is set, the issuance logic does not change in any way.

  </details>

**Body**

As you can see for the property `issuerKey` we only provided the required parameters of the TSE Key Reference Object
described above and left out `_publicKey` and `_keyType`

```json
{
  "issuerKey": {
    "type": "tse",
    "server": "https://vault.walt.id/v1/transit",
    "auth": {
      "roleId": "b50427bd-5070-0bec-3a00-dfe470ba349d",
      "secretId": "22ffc0f4-c308-e6d6-f5a4-246cd89bc17c"
    },
    "id": "k-1481938705"
  },
  "issuerDid": "did:key:z6MkjoRhq1jSNJdLiruSXrFFxagqrztZaXHqHGUTKJbcNywp",
  "credentialData": {
    "@context": [
      "https://www.w3.org/2018/credentials/v1",
      "https://purl.imsglobal.org/spec/ob/v3p0/context.json"
    ],
    "id": "urn:uuid:THIS WILL BE REPLACED WITH DYNAMIC DATA FUNCTION (see below)",
    "type": [
      "VerifiableCredential",
      "OpenBadgeCredential"
    ],
    "name": "JFF x vc-edu PlugFest 3 Interoperability",
    ...
  },
  "mapping": {
    "id": "<uuid>",
    ...
  },
  "standardVersion": "DRAFT13"
}
```
