HashiCorp Vault module
Writes certificate + key + chain into a Vault KV secret path (KV v1 or v2, selected per target). Rollback writes a previous retained certificate version back through the same deploy path (a new version on KV v2, an in-place overwrite on KV v1). Supports Token and AppRole authentication.
Overview
- Transport: Vault HTTP API (
X-Vault-Token, optionalX-Vault-Namespace). - Auth: Token (paste a Vault
token) or AppRole (role_id + secret_id). - Target: KV secret engine — KV v1 or KV v2, selected per target via the
engine_typefield (kv1|kv2, defaultkv2). The secret path is a literal string (no templating) — leave it empty for a per-certificate auto path, or override it per distribution. - Payload:
{"certificate": ..., "private_key": ..., "chain": ...}plus four reserved metadata keys (_certautopilot_updated_at,_certautopilot_common_name,_certautopilot_expires_at,_certautopilot_fingerprint). The private key is always written; scope which secrets the token may read via the Vault policy. Field names customisable. - Rollback: supported — a rollback re-runs the deploy with the previous retained certificate version (Rollback). On KV v2 that is a new KV version; on KV v1 the secret is overwritten in place (KV v1 has no versions).
- Namespace: Enterprise Vault namespace is supported via the
X-Vault-Namespaceheader.
Prerequisites
- Vault cluster reachable from the backend on its API port (typically 8200 HTTPS).
- A KV secret engine mounted (e.g. at
secret/) — KV v1 or KV v2. - A policy granting
create,updateandreadon the target path pattern. Auto-generated paths live under thecertautopilot/directory, so a policy coveringsecret/data/certautopilot/*works out of the box. The deploy never reads or deletesmetadata/; the only other write is the optional soft-delete of an old path after you changesecret_path, which Vault authorizes asupdateon<mount>/delete/<path>:
# REQUIRED: deploy, rollback, dry run and the health check's read probe
path "secret/data/certautopilot/*" {
capabilities = ["create", "update", "read"]
}
# OPTIONAL: soft-delete of the OLD path after secret_path / mount changes
path "secret/delete/certautopilot/*" {
capabilities = ["update"]
}
# OPTIONAL: mount probe in health check / dry run (a 403 is tolerated)
path "sys/mounts/secret/tune" {
capabilities = ["read"]
}
# OPTIONAL: live pickers only — the mount list and the mount's LIST calls
path "sys/mounts" {
capabilities = ["read"]
}
path "secret/metadata/*" {
capabilities = ["list"]
}
For a KV v1 mount drop the data/, delete/ and metadata/ segments: the secret is read and written at <mount>/<secret_path> and listed at <mount>/*; the rename cleanup never deletes on KV v1. No delete, patch or sudo capability is used anywhere.
Create the module credential
- Settings → Distribution → Credentials → New → HashiCorp Vault.
- Pick the auth method:
- Token: paste a Vault
token. CertAutoPilot sends it as-is and never renews it — create it with a TTL that outlives the renewals you expect (vault token create -policy=certautopilot -ttl=8760h -orphan). - AppRole: paste
role_id+secret_id. Every operation (deploy, dry run, health check, each picker listing) logs in once per target atauth/approle/login, so asecret_id_num_useslimit is consumed by all of them.
- Token: paste a Vault
- Save.
Create a Vault target
- Settings → Distribution → Targets → New. Module: HashiCorp Vault.
- Fields:
- Address —
https://vault.example.com:8200. - Namespace — optional; required for Vault Enterprise namespaces.
- KV mount path —
secret(default). - Engine type —
kv2(default) orkv1. - Secret path — optional. Empty = auto-generated per certificate as
certautopilot/cap-<token>-<domain>(token: 8-char hash derived from the certificate, stable across renewals), so one target can serve many certificates. Can also be overridden per distribution from the certificate's Distributions → Overrides drawer. - Field names — customise which JSON keys hold the leaf certificate, private key and chain (defaults
certificate,private_key,chain). There is no fullchain key —cert_keyholds the leaf only. The four reserved_certautopilot_*metadata keys are managed automatically and cannot be renamed. - TLS CA cert — PEM for a private-CA-signed Vault endpoint.
- TLS skip verify — for bring-up only.
- HTTP timeout — default 30 s.
- Credential.
- Address —
- Save → health check (see below).
Health check
The health check (module health, or the target's Test connection) runs three steps per target:
- Authenticate — AppRole: a real login. Token: no call is made (the token is used as-is).
- Mount probe (optional) —
GET sys/mounts/<mount>/tune. 200 →mount: ok; 403 → tolerated and reported as a warning; 404 → unhealthy (mount <mount> not found). - Read probe (decides) — a
GETof the secret path the deploy needs, with the same token: the target'ssecret_pathwhen one is set, otherwisecertautopilot/__healthcheck__(a name under the auto-path directory that is never written; a per-distribution override path is not probed). 200 or 404 → healthy (read: ok …), 403 → unhealthy (read probe: token rejected or policy lacks read on /v1/<mount>/data/<path> (HTTP 403)— an invalid or expired token, or a policy withoutreadthere); any other status or a network error → unhealthy.
So an expired static token, or a token whose policy does not cover the deploy path, fails the health check. A green check still does not prove create/update: only a deploy (or a dry run's planned write) exercises those.
Choosing from Vault (live pickers)
The target form and the per-certificate override drawer can read Vault and offer what is there instead of asking you to type it:
- KV mount path (target form only) — the KV secrets engines from
sys/mountsof the target's namespace (mounts of child namespaces are not listed — set the child namespace on the target), each with its KV version. Picking a mount sets the Engine type to match; a mount whose version differs from the current engine type is flagged in the list, and a secret-path listing against a mismatched or missing mount says so instead of looking empty. - Secret path (target form and per-target override rows) — the entries
under the typed path's folder in the target's mount (
LISTonly — no secret is ever read): folders (pick one to list what is inside;.. (up)goes back) and existing secrets (the deploy overwrites the certificate keys of the one you pick). A folder is not a secret path — the form refuses to save one; pick or type a name inside it. The Default (all targets) row of the override drawer stays manual-entry.
The credential needs read on sys/mounts for the mount picker and list on
<mount>/metadata plus <mount>/metadata/* (KV v2) or <mount> plus
<mount>/* (KV v1) for the path picker — the root listing is the first thing
it asks for; without them the picker reports the permission problem and you
type the value instead.
Listings are capped at 500 entries and nothing is read until you open a list.
Listing through a saved target is a project operator action; browsing
from an unsaved form, or after changing the address, namespace, TLS settings,
mount or engine type on an existing target, needs project admin (or save
first).
Execution flow
- Authenticate. Token credential: the token is used as-is, no call. AppRole:
POST auth/approle/loginand use the returned token for this target only. A failed login →VAULT_AUTH_FAILED. - Resolve the secret path (per-distribution override for this target → override for all targets → target
secret_path→ autocertautopilot/cap-<token>-<domain>). - Read the current secret —
GET /v1/<mount>/data/<path>(KV v2) orGET /v1/<mount>/<path>(KV v1). 404 = does not exist yet. A 403 (invalid/expired token, or noread) →VAULT_AUTH_FAILED(not retried); any other failure →VAULT_READ_FAILED(retried). Nothing is written after a refused read. - Skip when already current — if the stored
_certautopilot_fingerprintequals the fingerprint of the certificate being deployed, the write is skipped (success, not changed). - Merge and write — every existing key is kept, the certificate/key (and chain, only when the certificate has one) and the four metadata keys are set, and the whole map is written with one
PUT({"data": {...}}on KV v2, the bare map on KV v1; no check-and-set). On KV v2 Vault creates a new version. A 403 →VAULT_AUTH_FAILED(not retried); any other failure →VAULT_WRITE_FAILED(retried). - Clean up a renamed path (see the warning below).
After the new path is written, the module soft-deletes the certificate at the previous path — but only on KV v2, and only when that path still holds exactly the fingerprint CertAutoPilot last wrote there. On KV v1 (where deletion is irreversible) or when the origin cannot be confirmed, it logs a warning and leaves the secret alone.
Vault cannot tell you who reads a path, so anything still pointed at the old location breaks at that moment. Recover with vault kv undelete. The cleanup needs update on <mount>/delete/<path> in the policy; without it the delete simply fails as a warning.
Dry run
A dry run authenticates, probes the mount and reads the secret exactly as a deploy would, then reports one planned action: create (path empty), noop (fingerprint matches — the deploy will skip) or update. A failed read marks the target unreachable with the same code the deploy would stop with (VAULT_AUTH_FAILED: read failed: … on 403, VAULT_READ_FAILED: read failed: … otherwise) — the dry run never suggests the write "may still succeed", because a deploy never writes after a refused read. If the path changed since the certificate was last written, a warning says the old secret will be removed.
Rollback
Supported via previous-version re-deploy: a rollback runs the flow above with a previous retained certificate version. Its fingerprint differs from the stored one, so it is written — as a new KV version on KV v2 (the version counter never rewinds and metadata/ is not touched), or as an in-place overwrite on KV v1, which has no versions. Eligibility, the version picker, and auto-rollback: Rollback.
Security notes
- The private key is always written to Vault as part of the payload — if that matters to your threat model, scope the backend's Vault policy tightly (and audit reads) so only the intended consumers can retrieve the secret.
- Vault's audit log captures every write; pair it with CertAutoPilot's audit trail for a full chain of custody.
- The backend's Vault token is scoped to the policy above; it cannot read secrets outside that path pattern. Vault error bodies that reach the job log are truncated and have long base64/hex runs redacted.
Troubleshooting
VAULT_AUTH_FAILED / "permission denied" (HTTP 403)
With a token credential this is the token itself (expired, revoked) or a policy missing one of create/update/read on the path — or a path pattern that does not match. The health check catches the read half (it reads the deploy path, or certautopilot/__healthcheck__ for an auto-path target); create/update only surface on a deploy. Test with vault token capabilities <token> secret/data/certautopilot/cap-1a2b3c4d-example-com. Not retried.
"namespace not found"
Either typo, or you hit a non-Enterprise Vault — namespaces are an Enterprise-only feature.
Engine type does not match the mount
The module does not detect the mount's KV version — it builds paths from the target's Engine type. A kv2 target pointed at a KV v1 mount writes to <mount>/data/<path> as an ordinary KV v1 secret named data/<path> and reports success, so consumers reading <mount>/<path> see nothing; a kv1 target on a KV v2 mount sees an empty read and then fails the write with VAULT_WRITE_FAILED (Vault rejects the unversioned path). Set engine_type to match the mount (the mount picker does this for you) — both versions are supported, including for rollback. No migration is needed.
See also
Behaviours worth knowing
- The write is a merge, not a replace. Unrelated keys already at the secret path are preserved, and
chainis left untouched when the certificate has no chain. - No check-and-set. The module does not send
options.cas, so a concurrent writer to the same path is silently overwritten. - Renaming a key name leaves the old key behind. Change
cert_keyand the previous key keeps holding the old certificate forever — remove it yourself. - KV v1 has no versions. Rollback overwrites the secret in place; only KV v2 creates a new version.
- Cleanup needs
update, notdelete. The soft-delete call is a POST to<mount>/delete/<path>, which Vault authorizes under theupdatecapability. - A 403 is never retried. On the read or the write it is classified
VAULT_AUTH_FAILED; only network errors and 5xx answers get the retryableVAULT_READ_FAILED/VAULT_WRITE_FAILED.