Skip to content

Vault

DRP's Integration with HashiCorp Vault

Digital Rebar Provision (DRP) integrates with HashiCorp Vault to extend its secret management capabilities. This integration enables users to securely retrieve secrets from Vault, enhancing DRP's native secret management options.

Plugin Configuration

These steps are necessary to use the Vault plugin. Configure the Vault plugin with the following parameters:

Upgrading from prior versions

Prior versions of this plugin set AutoStart: true, which caused DRP to auto-create an unconfigured vault plugin instance at install time. That instance was immediately broken (no vault/address) and silently captured vault:// URI dispatch.

After upgrading:

  • If you have a broken vault instance with empty params, destroy it: drpcli plugins destroy vault.
  • If you have a configured vault instance, no action needed.
  • For new installs, create the instance explicitly: drpcli plugins create '{"Name":"vault","Provider":"vault","Params":{...}}'.

  • vault/address: The address of the Vault server. Ensure it follows the format http(s)://<vault-server-address>:<port>.

  • vault/auth-method: How the plugin authenticates to Vault. One of token (default), approle, or jwt. See the Authentication section below.
  • vault/token: The authentication token used to connect to Vault. Required when vault/auth-method is token. Learn more about Vault authentication tokens.
  • vault/approle-role-id: The AppRole role ID. Required when vault/auth-method is approle.
  • vault/approle-secret-id: The AppRole secret ID (secure param). Required when vault/auth-method is approle.
  • vault/approle-mount: The mount path for the AppRole auth backend in Vault. Defaults to approle (Vault's stock mount path). Only relevant when vault/auth-method is approle.
  • vault/jwt-role: The Vault role to authenticate as via JWT. Required when vault/auth-method is jwt.
  • vault/jwt: The JWT to present at Vault login (secure param). Required when vault/auth-method is jwt.
  • vault/jwt-mount: The mount path for the JWT auth backend in Vault. Defaults to jwt. Only relevant when vault/auth-method is jwt.
  • vault/token-short-lived: Indicates that the Vault token is short-lived and should be refreshed on every call. This is useful when the token is backed by a different secret store or is rotated on the plugin often. The default value is false. Only honored when vault/auth-method is token; see Authentication for the interaction with non-token methods.
  • vault/kv-version: The version of Vault KV secrets engine while running in versioned mode. If none is provided it will default to v2.
  • vault/kv-mount: The mount path for the KV secrets engine. Defaults to secret for KV v2 and kv for KV v1.
  • vault/insecure-tls: (Optional) Set to true to disable TLS verification. This is useful when using self-signed certificates. The default value is false.
  • vault/ca-cert: (Optional) The string form of a PEM format CA certificate file.

Optionally, you can configure a cache timeout, which allows DRP to temporarily store secrets in memory. This minimizes the number of requests made to the Vault server and improves performance. The duration is specified in seconds, with a default of 300 seconds.

  • vault/cache-timeout: The duration (in seconds) to cache secrets in memory.

Authentication

The plugin supports three authentication methods, dispatched by the vault/auth-method param:

Token (vault/auth-method: token, default)

The plugin uses the value of vault/token as the Vault client token. Suitable for development and for deployments where a long-lived token (root or otherwise) is acceptable.

AppRole (vault/auth-method: approle)

The plugin authenticates via Vault's AppRole auth backend, exchanging a role-id + secret-id pair for a Vault token. Suitable for production deployments where per-application credentials and least-privilege policies are required.

  • On Config(), the plugin calls auth/<vault/approle-mount>/login with the configured role-id and secret-id; the issued token becomes the in-memory token used for all subsequent operations.
  • vault/token is NOT required when vault/auth-method is approle; the plugin manages the token lifecycle itself.
  • The issued token's renewability and TTL are discovered via LookupSelf and fed into the same auto-renewal path used for static tokens (see Token Auto-Renewal).
  • If the auto-renew watcher terminates (e.g., on max_ttl, token revocation, or after grace-period exhaustion), the plugin transparently re-logs in with the cached role-id + secret-id to mint a fresh token. The re-login is bounded to 6 attempts (one immediate, then 5 retries spaced 5s, 30s, 2m, 5m, 10m apart; total budget ~17 minutes); if all attempts fail, the final error is surfaced via PluginErrors.
  • The AppRole's policy must include read access to the KV paths the plugin will fetch. A least-privilege example for secrets stored under a drp/ prefix:

    Terraform
    path "secret/data/drp/*"     { capabilities = ["read"] }
    path "secret/metadata/drp/*" { capabilities = ["read"] }
    

    The plugin does NOT require read access to the kv-mount root (secret/data without a subpath). The Config()-time validation step uses auth/token/lookup-self instead, which Vault's built-in default policy grants to every non-root token. This lets operators scope their AppRole policies to exactly the secrets the plugin needs.

JWT (vault/auth-method: jwt)

The plugin authenticates via Vault's JWT auth backend, exchanging a JWT for a Vault token. Suitable for deployments where an identity provider Vault already trusts issues short-lived JWTs that DRP can present (OIDC IDPs like Auth0/Keycloak, Kubernetes API servers that the customer's Vault is federated with, etc.).

The plugin is a JWT consumer, not a JWT minter. Whatever identity source you use is responsible for producing the JWT and updating vault/jwt when rotation is needed; the plugin presents whatever is currently in vault/jwt at each login.

Lifecycle

Text Only
  [your rotation source]     writes JWT ->     vault/jwt
                                                   |
                                  loginJWT reads at each call
                                                   |
                                                   v
                                       Plugin   login ->  Vault
                                                   |
                                                   v
                                            Vault token
                                       (cached + auto-renewed
                                         via LifetimeWatcher)
                                                   |
                                                   v
                                  Used for actual secret fetches
                                    until renewal can no longer
                                     keep it alive, then loop

A login happens once at Config() and again whenever the auto-renewed Vault token can no longer be kept alive (typically at token_max_ttl). The JWT is consumed at those login moments only; secret fetches in between use the cached Vault token. So the operator's JWT rotation cadence matches the JWT's TTL (set by your identity source), not the Vault token's TTL.

  • On every login call, the plugin reads the current vault/jwt value from this plugin instance's params via the DRP API, then calls auth/<vault/jwt-mount>/login with {"role": <vault/jwt-role>, "jwt": <current vault/jwt>}. This re-read happens on every login attempt, so an external rotator's update to vault/jwt is observed at the next natural re-login without needing a plugin reconfig.
  • vault/token is NOT required when vault/auth-method is jwt; the plugin manages the Vault token's lifecycle itself.
  • vault/jwt must be set at Config() time even though it is read fresh at each login; the Config()-time check makes a misconfigured plugin fail fast rather than at the first secret fetch.
  • Like AppRole, the issued Vault token is auto-renewed; when the watcher terminates the plugin re-logs in with the same bounded retry budget (6 attempts: one immediate, then 5 retries spaced 5s, 30s, 2m, 5m, 10m apart; total budget ~17 minutes). Each retry re-reads vault/jwt, so if rotation completes mid-failure the plugin recovers automatically.
  • Vault's "Kubernetes" auth method shares the role + jwt login payload with the generic JWT method; set vault/jwt-mount to your Kubernetes auth mount (typically kubernetes) to use it. This is the natural fit when your JWT source is a Kubernetes cluster.

Where does the JWT come from?

The plugin does not care; it just consumes vault/jwt. Realistic sources:

Source How vault/jwt gets populated
Corporate OIDC IDP that Vault is federated with The IDP's tooling pushes a fresh JWT into vault/jwt on its rotation cadence (via drpcli plugins set or the DRP API)
A Kubernetes cluster Vault trusts A scheduled job (CronJob inside the cluster, or a DRP task on a worker with cluster credentials) calls oc create token <sa> --audience=vault and pushes the result into vault/jwt
A long-lived static JWT signed by something Vault trusts Operator pastes it once; rotated infrequently (annually or on policy change)

Bootstrap is whichever of the above your environment already has. The plugin's contract starts after a JWT exists somewhere; producing the first one is the operator's problem, not the plugin's.

Role configuration

Least-privilege role configuration example for a Kubernetes service account named drp-vault in the drp namespace, presenting tokens with audience vault:

Bash
vault write auth/jwt/role/drp-vault \
    role_type=jwt \
    bound_audiences=vault \
    bound_subject=system:serviceaccount:drp:drp-vault \
    user_claim=sub \
    token_policies=drp-vault \
    token_ttl=1h \
    token_max_ttl=4h

The drp-vault policy itself only needs the capabilities the plugin actually exercises against your KV paths (read for fetches, plus write/create if DRP is the source of truth for any secret it stores, such as a cluster's kubeconfig during install).

Compatibility with vault/token-short-lived

vault/token-short-lived: true is incompatible with any non-token auth method (approle, jwt). Short-lived asks the plugin to re-read vault/token from DRP params on every fetch (the customer's external rotator owns the token lifecycle), while AppRole and JWT ask the plugin to manage the token lifecycle itself via login + renewal + re-login. If both are configured, the plugin logs a warning and surfaces a PluginErrors entry describing the conflict, then runs as the configured non-token method only (short-lived is silently disabled in memory).

Token Auto-Renewal

The plugin uses the Vault Go SDK's LifetimeWatcher to keep renewable tokens alive in the background. On Config(), the plugin calls Vault's LookupSelf on the configured token to determine its renewability and TTL:

  • Renewable tokens with a positive TTL are renewed automatically at Vault-driven cadence (roughly half the TTL with jitter) up to their max_ttl.
  • Non-renewable tokens (e.g., the root token) skip auto-renewal with an info-level log entry.
  • If renewal terminates because the token can no longer be kept alive, the plugin writes a descriptive entry to its PluginErrors field so the failure is visible to operators via drpcli plugins show. The classification is determined by a post-mortem LookupSelf and falls into one of:
    • renewal call failed: ... — the renewal call returned an error directly (requires a non-default RenewBehavior).
    • token unreadable post-renewal: ... — the token is gone or rejected; typical aftermath of revocation.
    • max_ttl reached or token expired — Vault still knows the token but its TTL is zero or negative.
    • grace period exhausted with <ttl> TTL remaining — the watcher gave up while the token was still notionally valid, almost always because transient renewal errors ate the grace window.

Auto-renewal is compatible with vault/token-short-lived. The two mechanisms are orthogonal: auto-renewal extends the in-memory token through its max_ttl; short-lived re-reads the vault/token param on every fetch so an external rotator can swap in a fresh token across max_ttl boundaries. Both flags may be set simultaneously.

Operational Configuration

Beyond configuring the plugin, you need to set up a lookupUri to specify the location of the secret in Vault:

  • decrypt/lookup-uri:
    • Specifies where the secret is stored

      • Format: <plugin-name>://<key-to-lookup>?path=<path-to-secret>
      • In this case, we are using the vault plugin. Let's assume we have a secret at path hello with key foo:

        Bash
        $ vault kv get secret/hello
          == Secret Path ==
          secret/data/hello
        
          ======= Metadata =======
          Key                Value
          ---                -----
          created_time       *****
          custom_metadata    *****
          deletion_time      *****
          destroyed          false
          version            4
        
          === Data ===
          Key    Value
          ---    -----
          foo    bar
        
      • Example URI will be: vault://foo?path=hello

The URI can also have parameter expansions.

Path Formatting Based on Vault KV Secrets Engine Version

  • For KV Version 1:

    • Use the full path, e.g., for kv/my-secret, the path is kv/my-secret.
      • Details about KV version 1: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v1
  • For KV Version 2:

    • Exclude /secret from the path. For a secret stored at /secret/foo/creds, the path should be /foo/creds.
      • Details about KV version 2: https://developer.hashicorp.com/vault/docs/secrets/kv/kv-v2

Unsure which version your Vault secrets engine is using? Run vault secrets list -detailed to find out. Look for the Options column. - If it shows version: "2", it's KV version 2. - If the Options column is empty or does not mention a version, it's likely KV version 1.

Here is an example output:

Text Only
Path      Plugin    Accessor    Options          Description
----      ------    --------    -------          -----------
secret/   kv        kv_abcde    map[version:2]   key/value secret storage

Usage

Assuming that a secret named foo exists at the hello path in Vault. Check Vault Docs. Also assuming that a machine named vault-test exists in DRP.

Set the secret:

Bash
drpcli machines set Name:vault-test param ipmi/password to '{ "LookupUri": "vault://foo?path=hello" }'

Retrieve the secret:

Bash
drpcli machines get Name:vault-test param ipmi/password --decode