AI Tools for Automated Vault Policy Generation 2026
HashiCorp Vault policies are written in HCL with path-based access control. Getting the paths right — especially for KV v2 (which adds a data/ prefix) vs KV v1 — is a common source of errors. AI tools handle Vault policy generation well with the right context.
Setting Up Claude for Vault Work
Always specify the Vault version and KV engine version:
You are generating HashiCorp Vault policies for Vault 1.15+.
We use KV secrets engine v2 (kv-v2), mounted at "secret/".
KV v2 paths require "data/" prefix for reading (secret/data/...) and
"metadata/" for listing (secret/metadata/...).
Our auth methods: AppRole for services, Kubernetes auth for pods.
This prevents the most common mistake: writing secret/myapp/* when you need secret/data/myapp/* for KV v2.
Basic Application Policy
Prompt:
Write a Vault policy for an application called "orders-service".
It needs to:
- Read secrets at secret/orders-service/* (KV v2)
- List available secrets at secret/orders-service/
- Read its own dynamic database credentials at database/creds/orders-service-role
- NOT be able to write or delete any secrets
Claude output:
# Policy: orders-service
# Grants read-only access to orders-service secrets and database credentials
# KV v2: read secrets
path "secret/data/orders-service/*" {
capabilities = ["read"]
}
# KV v2: list secret names (metadata path for listing)
path "secret/metadata/orders-service/*" {
capabilities = ["list"]
}
# KV v2: list the top-level directory
path "secret/metadata/orders-service" {
capabilities = ["list"]
}
# Dynamic database credentials (read-only, Vault generates these)
path "database/creds/orders-service-role" {
capabilities = ["read"]
}
# Allow the service to renew its own token
path "auth/token/renew-self" {
capabilities = ["update"]
}
# Allow the service to look up its own token info
path "auth/token/lookup-self" {
capabilities = ["read"]
}
Claude correctly separates the data/ path (for reading secret values) from the metadata/ path (required for listing). It also adds auth/token/renew-self and auth/token/lookup-self — standard additions that most generated policies miss, causing token renewal failures.
ChatGPT often writes secret/orders-service/* without the data/ prefix, which works for KV v1 but silently fails in KV v2.
AppRole Authentication
Prompt:
Write Vault configuration to set up AppRole auth for orders-service.
The role should: use the orders-service policy, have a 24-hour token TTL,
allow token renewal up to 7 days, require secret ID to not be renewable,
and bind to CIDR 10.0.0.0/8.
Include the Terraform configuration and the initial credential bootstrap commands.
Claude output:
# Terraform: enable AppRole auth and configure role
resource "vault_auth_backend" "approle" {
type = "approle"
path = "approle"
}
resource "vault_approle_auth_backend_role" "orders_service" {
backend = vault_auth_backend.approle.path
role_name = "orders-service"
token_policies = ["orders-service"]
token_ttl = 86400 # 24 hours in seconds
token_max_ttl = 604800 # 7 days
secret_id_ttl = 86400 # Secret IDs expire after 24 hours
secret_id_num_uses = 0 # Unlimited uses (use 1 for one-time bootstrap)
token_bound_cidrs = ["10.0.0.0/8"]
secret_id_bound_cidrs = ["10.0.0.0/8"]
}
output "role_id" {
value = vault_approle_auth_backend_role.orders_service.role_id
}
# Bootstrap: retrieve Role ID (not secret, can be stored in config)
ROLE_ID=$(vault read -field=role_id auth/approle/role/orders-service/role-id)
# Generate a Secret ID (keep secret, rotate regularly)
SECRET_ID=$(vault write -field=secret_id -f auth/approle/role/orders-service/secret-id)
# Login and get a token
vault write auth/approle/login \
role_id="${ROLE_ID}" \
secret_id="${SECRET_ID}"
Kubernetes Auth Integration
Prompt:
Write Vault Kubernetes auth configuration for a pod in namespace "production",
service account "orders-service-sa". The pod should receive the orders-service policy.
Include both the Vault configuration and the Kubernetes service account manifest.
Claude output:
# Terraform: Kubernetes auth backend role
resource "vault_kubernetes_auth_backend_role" "orders_service" {
backend = "kubernetes"
role_name = "orders-service"
bound_service_account_names = ["orders-service-sa"]
bound_service_account_namespaces = ["production"]
token_policies = ["orders-service"]
token_ttl = 3600 # 1 hour (short-lived; pods re-authenticate)
token_max_ttl = 86400
}
# Kubernetes: ServiceAccount and RBAC
apiVersion: v1
kind: ServiceAccount
metadata:
name: orders-service-sa
namespace: production
annotations:
vault.hashicorp.com/role: "orders-service" # used by Vault Agent Injector
---
# Pod spec snippet with Vault Agent Injector annotations
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "orders-service"
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/orders-service-role"
vault.hashicorp.com/agent-inject-template-db-creds: |
{{- with secret "database/creds/orders-service-role" -}}
DATABASE_URL=postgresql://{{ .Data.username }}:{{ .Data.password }}@db:5432/orders
{{- end }}
Claude includes the Vault Agent Injector annotation template — the HCL-like templating that transforms Vault secret values into the format your application expects.
Policy Linting
Claude can also review existing policies for overly broad permissions:
Prompt:
Review this Vault policy for security issues:
path "secret/*" { capabilities = ["read", "list", "create", "update"] }
path "sys/*" { capabilities = ["read"] }
Claude’s response:
secret/*with create/update is overly permissive — gives write access to all secretssys/*is dangerous —sys/contains Vault’s own configuration, audit logs, and health endpoints. Read access tosys/includessys/config/uiandsys/seal-status- Recommend narrowing to specific paths:
secret/data/myapp/*and removingsys/*entirely unless specific sys paths are required
Dynamic Secrets for Databases
One of Vault’s most powerful features is generating short-lived database credentials on demand. AI tools can generate both the Vault configuration and the application integration code.
Prompt:
Write Vault Terraform configuration to set up dynamic PostgreSQL credentials.
Database: postgres.internal:5432, dbname: orders.
Create a role called "orders-service-role" that generates credentials with
SELECT, INSERT, UPDATE on all tables in the public schema.
Credentials should expire after 1 hour.
resource "vault_database_secret_backend_connection" "postgres" {
backend = "database"
name = "orders-postgres"
allowed_roles = ["orders-service-role"]
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@postgres.internal:5432/orders"
max_open_connections = 5
max_connection_lifetime = "300s"
}
root_rotation_statements = [
"ALTER USER '{{name}}' WITH PASSWORD '{{password}}';"
]
}
resource "vault_database_secret_backend_role" "orders_service" {
backend = "database"
name = "orders-service-role"
db_name = vault_database_secret_backend_connection.postgres.name
default_ttl = "3600" # 1 hour
max_ttl = "86400" # 24 hours max renewal
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";",
"ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE ON TABLES TO \"{{name}}\";"
]
revocation_statements = [
"REVOKE ALL PRIVILEGES ON ALL TABLES IN SCHEMA public FROM \"{{name}}\";",
"DROP ROLE IF EXISTS \"{{name}}\";"
]
}
The ALTER DEFAULT PRIVILEGES statement ensures the dynamic user can access tables created after the role was configured — a detail that Claude adds proactively. Without it, dynamic users lose access to new tables without a policy change.
Secret Versioning and Rollback
KV v2 stores multiple versions of each secret, and AI can help generate the management scripts:
Prompt:
Write a bash script to:
1. Write a new version of secret/data/orders-service/config
2. Verify the write succeeded and show the version number
3. Roll back to the previous version if the application health check fails
4. Archive (soft-delete) old versions beyond the last 5
#!/usr/bin/env bash
set -euo pipefail
SECRET_PATH="secret/data/orders-service/config"
METADATA_PATH="secret/metadata/orders-service/config"
HEALTH_URL="http://orders-service:8080/health"
# Write new version
echo "Writing new secret version..."
NEW_VERSION=$(vault kv put -mount=secret orders-service/config \
DB_HOST="postgres.internal" \
DB_PORT="5432" \
FEATURE_FLAG="enabled" \
-format=json | jq -r '.data.version')
echo "Wrote version: ${NEW_VERSION}"
# Wait for application to pick up new secret
sleep 5
# Health check
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" "${HEALTH_URL}")
if [ "${HTTP_CODE}" != "200" ]; then
echo "Health check failed (HTTP ${HTTP_CODE}). Rolling back to version $((NEW_VERSION - 1))..."
vault kv rollback -mount=secret -version=$((NEW_VERSION - 1)) orders-service/config
echo "Rollback complete."
exit 1
fi
echo "Health check passed. Deployment successful."
# Clean up old versions — keep last 5
CURRENT_VERSION=$(vault kv metadata get -mount=secret -format=json orders-service/config \
| jq -r '.data.current_version')
if [ "${CURRENT_VERSION}" -gt 5 ]; then
VERSIONS_TO_DELETE=$(seq 1 $((CURRENT_VERSION - 5)) | tr '\n' ',' | sed 's/,$//')
echo "Archiving old versions: ${VERSIONS_TO_DELETE}"
vault kv delete -mount=secret -versions="${VERSIONS_TO_DELETE}" orders-service/config
fi
Claude uses vault kv rollback (available in Vault 1.9+) rather than a manual version re-write, which is the correct approach — rollback promotes an existing version as the new HEAD without creating ambiguous state.
Audit Log Analysis
Prompt:
Write a jq command to analyze Vault audit logs and find the top 10 secrets
being accessed most frequently, showing path and request count.
# Vault audit log is JSONL — one JSON object per line
cat /var/log/vault/audit.log | \
jq -r 'select(.type == "request") | .request.path' | \
sort | uniq -c | sort -rn | head -10
# Filter to only successful reads in the last hour
cat /var/log/vault/audit.log | \
jq -r --arg cutoff "$(date -u -d '1 hour ago' +%Y-%m-%dT%H:%M:%SZ)" \
'select(.type == "response" and .time >= $cutoff and .response.auth != null)
| .request.path' | \
sort | uniq -c | sort -rn | head -10
AI is particularly useful for Vault audit log queries because the JSONL format and nested structure make ad-hoc jq expressions tedious to write correctly. Claude handles the nested field selection and date filtering without needing multiple iterations.
Related Reading
- AI-Powered Feature Flag Management Tools
- AI-Powered Observability Configuration Tools
- Best AI Tools for Writing Systemd Units
- AI for Automated Regression Test Generation from Bug
Related Articles
Database Role Provisioning
Prompt:
Write Vault configuration for dynamic database credentials.
- Postgres database at db.prod.internal:5432
- Create read-only role: "orders-service-ro", grants SELECT
- Create write role: "orders-service-rw", grants SELECT, INSERT, UPDATE, DELETE
- Role TTL: 1 hour (credentials rotate hourly)
- Max TTL: 24 hours
Include the Terraform configuration and policy.
Claude output:
# Enable database secrets engine
resource "vault_mount" "database" {
path = "database"
type = "database"
}
# Configure Postgres connection
resource "vault_database_connection" "postgres" {
backend = vault_mount.database.path
name = "postgres"
allowed_roles = ["orders-service-ro", "orders-service-rw"]
postgresql {
connection_url = "postgresql://{{username}}:{{password}}@db.prod.internal:5432/orders"
username = "vault_admin"
password = var.postgres_admin_password
verify_connection = true
disable_verify_connection = false
}
}
# Read-only role
resource "vault_database_secret_backend_role" "orders_ro" {
backend = vault_mount.database.path
name = "orders-service-ro"
db_name = vault_database_connection.postgres.name
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT CONNECT ON DATABASE orders TO \"{{name}}\";",
"GRANT USAGE ON SCHEMA public TO \"{{name}}\";",
"GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
]
default_ttl = 3600 # 1 hour
max_ttl = 86400 # 24 hours
}
# Read-write role
resource "vault_database_secret_backend_role" "orders_rw" {
backend = vault_mount.database.path
name = "orders-service-rw"
db_name = vault_database_connection.postgres.name
creation_statements = [
"CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';",
"GRANT CONNECT ON DATABASE orders TO \"{{name}}\";",
"GRANT USAGE ON SCHEMA public TO \"{{name}}\";",
"GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"
]
default_ttl = 3600
max_ttl = 86400
}
Policy for services to use database credentials:
# Policy: orders-service-db-access
path "database/creds/orders-service-ro" {
capabilities = ["read"]
}
path "database/creds/orders-service-rw" {
capabilities = ["read"]
}
path "auth/token/renew-self" {
capabilities = ["update"]
}
Claude correctly uses {{username}}, {{password}}, and {{expiration}} placeholders — Vault substitutes these when creating roles.
SSH Certificate Signing (Host Access)
Prompt:
Generate Vault SSH CA configuration for signing user certificates.
- SSH CA keypair stored in Vault
- Sign certificates valid for 8 hours
- Allow access to specific hosts via CIDR
- Policy for developers to request signed certificates
Claude output:
# Enable SSH secrets engine
resource "vault_mount" "ssh" {
path = "ssh"
type = "ssh"
}
# Configure SSH CA
resource "vault_ssh_secret_backend_ca" "ca" {
backend = vault_mount.ssh.path
# Vault generates the CA keypair; store pub key in authorized_keys on servers
generate_signing_key = true
}
# SSH role for user certificates (8-hour TTL)
resource "vault_ssh_secret_backend_role" "user_cert" {
backend = vault_mount.ssh.path
name = "user-cert"
ttl = 28800 # 8 hours
max_ttl = 28800
allowed_user_key_config = "dictated"
allow_user_certificates = true
# Signed certificates can access these principals
allowed_users = "ubuntu,ec2-user,ubuntu"
# Optional: restrict to specific hosts by CIDR or hostname pattern
allowed_domains = "prod-*.internal,staging-*.internal"
}
# Output CA public key to distribute to servers
output "ssh_ca_public_key" {
value = vault_ssh_secret_backend_ca.ca.public_key
sensitive = true
}
Policy for developers:
path "ssh/sign/user-cert" {
capabilities = ["create", "update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
Usage:
# 1. Request signed certificate from Vault
vault write -field=signed_certificate ssh/sign/user-cert \
username=ubuntu \
public_key=@~/.ssh/id_rsa.pub > ~/.ssh/id_rsa-cert.pub
# 2. SSH using certificate (no password)
ssh -i ~/.ssh/id_rsa -i ~/.ssh/id_rsa-cert.pub ubuntu@prod-web-01.internal
# 3. SSH servers validate certificate against CA public key
# (installed in /etc/ssh/trusted-user-ca-keys.pub)
Tool Comparison
| Feature | Claude | ChatGPT |
|---|---|---|
| KV v2 path prefixes | Always correct (data/, metadata/) | Often forgets data/ |
| AppRole config | Includes CIDR binding, TTLs | Minimal, missing options |
| Kubernetes auth | Includes Agent Injector templates | Omits Injector pattern |
| Database dynamic roles | Correct placeholder syntax | Sometimes wrong |
| SSH CA setup | Complete (CA key, roles, policy) | Missing steps |
| Token renewal paths | Includes auth/token/* automatically | Omitted |
| Policy review feedback | Identifies overly broad paths | Generic feedback |
| Terraform + HCL | Both correct | HCL formatting issues |
Related Reading
- AI-Powered Feature Flag Management Tools
- AI-Powered Observability Configuration Tools
- Best AI Tools for Writing Systemd Units
- AI Tools for Automated Secrets Rotation and Vault Management
- AI Tools for Automated Test Data Generation 2026
- AI Policy Management Tools Enterprise Compliance
- How to Configure AI Coding Tools to Exclude Secrets and Env
- AI Tools for Automated Changelog Generation 2026
Built by theluckystrike — More at zovo.one