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