HashiCorp Vault PKI Discovery

Enumerates CA and issued certificates from Vault's PKI secrets engine via token-authenticated REST — no Vault SDK required

How It Works

The Vault PKI connector uses Vault's REST API with token authentication. It performs three operations per PKI mount:

🏛️

CA Certificate

GET /v1/{mount}/ca/pem — fetches the root or intermediate CA certificate installed on this mount. Vault returns raw PEM.

📋

Serial List

LIST /v1/{mount}/certs — retrieves all issued certificate serial numbers using Vault's HTTP LIST verb.

📄

Individual Certs

GET /v1/{mount}/cert/{serial} — fetches each cert's PEM and parses full x509 details.

Mount Auto-Discovery

When no mount path is configured, the connector calls GET /v1/sys/mounts and scans every mount of type pki automatically. This means a Vault instance with pki/ and pki-int/ will have both enumerated with zero extra config.

Lab Setup

Two options — Docker (recommended, no install needed) or Homebrew. Both produce an identical Vault instance for testing.

Option A Docker (Recommended)

Vault starts pre-unsealed in dev mode. The container is destroyed when you ctrl-C — no cleanup needed.

Start Vault dev server

docker run --rm --name vault-lab \
  -p 8200:8200 \
  -e VAULT_DEV_ROOT_TOKEN_ID=devroot \
  -e VAULT_DEV_LISTEN_ADDRESS=0.0.0.0:8200 \
  hashicorp/vault:latest

Vault starts on http://127.0.0.1:8200 · root token: devroot · pre-unsealed

In a second terminal — configure PKI

export VAULT_ADDR=http://127.0.0.1:8200
export VAULT_TOKEN=devroot

# Enable PKI secrets engine
vault secrets enable pki
vault secrets tune -max-lease-ttl=87600h pki

# Generate root CA (RSA-2048)
vault write pki/root/generate/internal \
  common_name="Lab Root CA" \
  key_type=rsa key_bits=2048 \
  ttl=87600h

# Create a role so you can issue leaf certs
vault write pki/roles/lab \
  allowed_domains=lab.local \
  allow_subdomains=true \
  max_ttl=720h

# Issue a leaf cert to populate LIST /v1/pki/certs
vault write pki/issue/lab \
  common_name=server.lab.local ttl=24h

(Optional) Add an EC-P256 intermediate mount for PQC coverage testing

vault secrets enable -path=pki-int pki
vault secrets tune -max-lease-ttl=43800h pki-int

# Generate intermediate CSR
vault write pki-int/intermediate/generate/internal \
  common_name="Lab Intermediate CA" \
  key_type=ec key_bits=256 | grep csr | awk '{print $2}' \
  > /tmp/int_csr.pem

# Sign with root CA
vault write pki/root/sign-intermediate \
  csr=@/tmp/int_csr.pem \
  format=pem_bundle ttl=43800h | grep certificate | awk '{print $2}' \
  > /tmp/int_cert.pem

# Import signed cert into pki-int
vault write pki-int/intermediate/set-signed \
  certificate=@/tmp/int_cert.pem

# Role + issue a leaf so LIST has data
vault write pki-int/roles/lab allowed_domains=lab.local allow_subdomains=true max_ttl=720h
vault write pki-int/issue/lab common_name=app.lab.local ttl=24h

The scanner will auto-discover both pki and pki-int mounts without any extra config.

Configure the scanner and run

# Store Vault address and token in config.enc
./build/certscanner -config \
  -config-vault-addr http://127.0.0.1:8200 \
  -config-vault-token devroot

# Run PKI scan — auto-discovers all pki mounts
./build/certscanner -scan-pki -outputformat flatndjson \
  -output /tmp/vault_scan.ndjson

# Verify output
grep "vault://" /tmp/vault_scan.ndjson | grep -o '"source_file_path":"[^"]*"'

Option B Homebrew (macOS)

# Install Vault CLI + server
/opt/homebrew/bin/brew install vault

# Start dev server (same flags as Docker)
vault server -dev -dev-root-token-id=devroot -dev-listen-address=127.0.0.1:8200

# Then follow the same PKI setup steps above (Step 2 onward)

Dev Mode Notes

  • Dev mode stores all data in memory — data is lost when the process exits. That's intentional for lab use.
  • Dev mode uses HTTP (no TLS). Production Vault always uses HTTPS; use -insecure if your lab cert is self-signed.
  • The devroot token has full root permissions — use a scoped token in any shared environment.

Credential Chain

The scanner resolves a Vault token using this priority order. The first tier that returns a non-empty token wins.

Tier 1

config.enc stored token

Set via -config -config-vault-token <token>. Encrypted at rest using AES-256-GCM + PBKDF2. Highest precedence — use in production deployments where credentials must be centrally managed.

Tier 2

VAULT_TOKEN environment variable

Standard Vault environment variable. Works for CI/CD pipelines and Docker-injected secrets. Falls back from Tier 1 when no stored config exists.

Tier 3

~/.vault-token file

Written automatically by vault login. Works for developer workstations where the operator has already authenticated interactively.

Tier 4

Error — scan skipped

Vault PKI requires authentication. There is no anonymous access. The connector logs a message and skips Vault scanning without aborting the rest of the scan.

Configuration Flags

Flag Stored Description
-config-vault-addr✓ config.encVault server address. Falls back to VAULT_ADDR env var, then http://127.0.0.1:8200.
-config-vault-token✓ config.encVault token for PKI API access. Falls back to VAULT_TOKEN env / ~/.vault-token.
-config-vault-mount✓ config.encComma-separated PKI mount path(s) to scan (e.g. pki,pki-int). Leave empty to auto-discover all pki mounts via /v1/sys/mounts.
-config-vault-namespace✓ config.encVault Enterprise namespace (X-Vault-Namespace header). Leave empty for Community/OSS.
-insecureruntime onlySkip TLS certificate verification. Required when Vault uses a self-signed certificate.

Quick Start Examples

Local dev server (lab)

# Store config once
./build/certscanner -config \
  -config-vault-addr http://127.0.0.1:8200 \
  -config-vault-token devroot

# Scan (auto-discovers all pki mounts)
./build/certscanner -scan-pki -outputformat flatndjson -output vault_certs.ndjson

Production Vault (HTTPS, specific mounts)

./build/certscanner -config \
  -config-vault-addr https://vault.corp.com:8200 \
  -config-vault-token s.AbCdEfGhIjKlMnOp \
  -config-vault-mount pki,pki-intermediate,pki-iot

./build/certscanner -scan-pki -outputformat flatndjson -output vault_certs.ndjson

Self-signed Vault TLS

./build/certscanner -scan-pki -insecure -outputformat flatndjson -output vault_certs.ndjson

Via environment variable (CI/CD)

export VAULT_ADDR=https://vault.corp.com:8200
export VAULT_TOKEN=s.AbCdEfGhIjKlMnOp
./build/certscanner -scan-pki -outputformat flatndjson -output vault_certs.ndjson

Multi-Mount Auto-Discovery

When -config-vault-mount is not set, the connector calls GET /v1/sys/mounts and scans every mount whose type field is "pki". This is the recommended configuration for environments with multiple PKI hierarchies.

Common multi-mount layouts
pki/
Root CA — auto-discovered and scanned
pki-intermediate/
Intermediate CA — auto-discovered and scanned
pki-iot/
IoT device PKI mount — auto-discovered and scanned
secret/
KV secrets engine — type is "kv", skipped automatically
aws/
AWS secrets engine — type is "aws", skipped automatically

Vault Enterprise — Namespaces

Vault Enterprise supports multi-tenancy via namespaces. Set the namespace using -config-vault-namespace and the scanner will include the X-Vault-Namespace header on every API request.

# Enterprise: store namespace alongside addr/token
./build/certscanner -config \
  -config-vault-addr https://vault.enterprise.com:8200 \
  -config-vault-token s.TokenHere \
  -config-vault-namespace "infosec/pki"

# Scans /v1/infosec/pki/{mount}/... automatically
./build/certscanner -scan-pki -outputformat flatndjson -output vault_ent.ndjson

Vault Community Edition (OSS) does not have namespaces. Leave -config-vault-namespace empty — the header is omitted when the namespace is not set.

Output Fields

Vault PKI CA certificates appear as pki_ca_discovered events; issued leaf certificates appear as pki_issued_cert_discovered events. Both have event.dataset: pki_certificate, which routes them to the tychon-pqc-certificates Elasticsearch index.

PKI platform metadata (Vault address, mount name) lives in tychon.pki.*. Certificate cryptographic data follows the standard ECS namespace (x509.*, certificate.*).

Field Example Value Notes
event.actionpki_ca_discoveredCA cert (vault://.../ca). Issued certs use pki_issued_cert_discovered.
event.datasetpki_certificateRoutes to tychon-pqc-certificates Elasticsearch index.
certificate.source_file_pathvault://127.0.0.1:8200/pki/cert/48-a3-...Issued cert. Format: vault://{addr}/{mount}/cert/{serial}.
certificate.source_file_pathvault://127.0.0.1:8200/pki/caCA cert. Format: vault://{addr}/{mount}/ca.
tychon.pki.platform.typevault_pkiPlatform identifier — consistent across Kibana / Splunk.
tychon.pki.platform.host127.0.0.1:8200Vault server address extracted from the source path.
tychon.pki.platform.source_protohttpsAlways https (Vault REST API).
tychon.pki.ca.namepki-intVault mount path (e.g. pki, pki-int, pki-iot).
x509.not_before2025-01-01T00:00:00ZCertificate valid-from (RFC 3339).
x509.not_after2026-01-01T00:00:00ZCertificate expiration (RFC 3339).
certificate.not_before2025-01-01T00:00:00ZAlias of x509.not_before.
certificate.not_after2026-01-01T00:00:00ZAlias of x509.not_after.
omb.sig_tierCLASSICAL / MODERN / LEGACY / PQC READYOMB M-23-02 signature tier.
omb.vulnerability_statusVulnerable / Not VulnerableDerived from PQC assessment.
omb.dsa_algorithmsRSAKey algorithm family from the cert's own public key.
omb.dsa_parametersRSA: 2048Algorithm and key size.
certificate.pqc_vulnerabletrue / falsePQC vulnerability flag.
certificate.sha256_fingerprintf808...Dedup key — same cert across mounts or scanners appears once.

Minimum Vault Policy

Create a scoped policy instead of using the root token in production. The scanner requires only read and list capabilities on PKI paths — no write, delete, create, update, patch, or sudo access is needed or should be granted.

Capabilities Granted

  • read — fetch CA cert, individual certs, mounts
  • list — enumerate issued certificate serials

Capabilities Never Granted

  • create — issue new certificates
  • update — modify PKI config or roles
  • delete — revoke or purge certificates
  • patch — partial updates
  • sudo — elevated Vault operations

Known Mount Names

Use explicit mount paths when your PKI mounts have fixed, known names. Add one block per mount.

# tychon-pki-read.hcl  (known mount names: pki, pki-int)

# Read CA certificate (raw PEM endpoint)
path "pki/ca/pem"     { capabilities = ["read"] }
path "pki-int/ca/pem" { capabilities = ["read"] }

# List issued certificate serials
path "pki/certs"     { capabilities = ["list"] }
path "pki-int/certs" { capabilities = ["list"] }

# Read individual certificates by serial
path "pki/cert/*"     { capabilities = ["read"] }
path "pki-int/cert/*" { capabilities = ["read"] }

# Required for auto-discovery (omit if using -config-vault-mount explicitly)
path "sys/mounts" { capabilities = ["read"] }

Dynamic / Unknown Mount Names

If your environment uses programmatically created PKI mounts with unpredictable names, use the + single-level wildcard. This covers all direct child mounts without granting access to other secret engines.

# tychon-pki-read.hcl  (wildcard variant — covers any top-level PKI mount name)

path "+/ca/pem"   { capabilities = ["read"] }
path "+/certs"    { capabilities = ["list"] }
path "+/cert/*"   { capabilities = ["read"] }
path "sys/mounts" { capabilities = ["read"] }
Wildcard scope: The + glob matches exactly one path segment (e.g. pki/ca/pem, pki-int/ca/pem) but not nested sub-mounts (team-a/pki/ca/pem). In environments with namespace isolation, add namespace-prefixed paths or apply the policy within the correct namespace scope.

Apply the Policy and Create a Token

# Write the policy
vault policy write tychon-pki-read tychon-pki-read.hcl

# Create a standard token (TTL = 1 year)
vault token create \
  -policy=tychon-pki-read \
  -display-name="tychon-scanner" \
  -ttl=8760h

# Store the token in config.enc (never on the CLI or in env vars in production)
./build/certscanner -config -config-vault-addr https://vault.corp.example.com:8200 \
                            -config-vault-token s.YourTokenHere

Token Renewal (Scheduled Scans)

For recurring scans, create a periodic token (no hard expiry) with an explicit max TTL and renew it on each scan run. Periodic tokens require the sudo capability on auth/token/create to create, but the resulting token itself has no elevated rights.

# Create a periodic token — max-ttl caps total lifetime even with renewals
vault token create \
  -policy=tychon-pki-read \
  -display-name="tychon-scanner" \
  -period=720h \
  -explicit-max-ttl=8760h

# Renew before each scan (add to your scheduling wrapper script)
vault token renew -increment=720h

Troubleshooting

no Vault token found

None of the three token sources had a value. Fix one of:

# Option A — store in config.enc
./build/certscanner -config -config-vault-token s.YourToken

# Option B — environment variable
export VAULT_TOKEN=s.YourToken

# Option C — interactive login (writes ~/.vault-token)
export VAULT_ADDR=http://127.0.0.1:8200
vault login devroot

no pki-type mounts found

The PKI secrets engine is not enabled. Enable it:

vault secrets enable pki
# Then generate or import a root CA before scanning

HTTP 403 on /v1/pki/ca/pem or /v1/pki/certs

Token exists but lacks permission. Apply the minimum policy above, or use the root token for testing.

no PEM block in CA response

The PKI mount has no root CA installed. Run vault write pki/root/generate/internal ... or import an existing CA with vault write pki/config/ca.

TLS handshake error / certificate signed by unknown authority

Vault is using a self-signed TLS cert. Add -insecure to skip verification for testing, or add Vault's CA cert to the OS trust store for production:

./build/certscanner -scan-pki -insecure -outputformat flatndjson -output out.ndjson

Vault PKI scan complete: no certificates discovered

The mount exists and the token has access, but no certs were issued yet. Issue at least one cert with vault write pki/issue/<role> common_name=test.lab.local to verify the scanner picks it up.