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
-insecureif your lab cert is self-signed. - The
devroottoken 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.
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.
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.
~/.vault-token file
Written automatically by vault login. Works for developer workstations where the operator has already authenticated interactively.
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.enc | Vault server address. Falls back to VAULT_ADDR env var, then http://127.0.0.1:8200. |
| -config-vault-token | ✓ config.enc | Vault token for PKI API access. Falls back to VAULT_TOKEN env / ~/.vault-token. |
| -config-vault-mount | ✓ config.enc | Comma-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.enc | Vault Enterprise namespace (X-Vault-Namespace header). Leave empty for Community/OSS. |
| -insecure | runtime only | Skip 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.
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.action | pki_ca_discovered | CA cert (vault://.../ca). Issued certs use pki_issued_cert_discovered. |
| event.dataset | pki_certificate | Routes to tychon-pqc-certificates Elasticsearch index. |
| certificate.source_file_path | vault://127.0.0.1:8200/pki/cert/48-a3-... | Issued cert. Format: vault://{addr}/{mount}/cert/{serial}. |
| certificate.source_file_path | vault://127.0.0.1:8200/pki/ca | CA cert. Format: vault://{addr}/{mount}/ca. |
| tychon.pki.platform.type | vault_pki | Platform identifier — consistent across Kibana / Splunk. |
| tychon.pki.platform.host | 127.0.0.1:8200 | Vault server address extracted from the source path. |
| tychon.pki.platform.source_proto | https | Always https (Vault REST API). |
| tychon.pki.ca.name | pki-int | Vault mount path (e.g. pki, pki-int, pki-iot). |
| x509.not_before | 2025-01-01T00:00:00Z | Certificate valid-from (RFC 3339). |
| x509.not_after | 2026-01-01T00:00:00Z | Certificate expiration (RFC 3339). |
| certificate.not_before | 2025-01-01T00:00:00Z | Alias of x509.not_before. |
| certificate.not_after | 2026-01-01T00:00:00Z | Alias of x509.not_after. |
| omb.sig_tier | CLASSICAL / MODERN / LEGACY / PQC READY | OMB M-23-02 signature tier. |
| omb.vulnerability_status | Vulnerable / Not Vulnerable | Derived from PQC assessment. |
| omb.dsa_algorithms | RSA | Key algorithm family from the cert's own public key. |
| omb.dsa_parameters | RSA: 2048 | Algorithm and key size. |
| certificate.pqc_vulnerable | true / false | PQC vulnerability flag. |
| certificate.sha256_fingerprint | f808... | 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"] }
+ 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.