HashiCorp Vault — HCL Policy Syntax

2 minute read

Vault policies are written in HCL (HashiCorp Configuration Language). Each policy is a set of path rules that grant or deny capabilities.


Basic Structure

# policy-name.hcl

path "secret/data/myapp/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

path "secret/metadata/myapp/*" {
  capabilities = ["list", "read"]
}

Anatomy:

  • path — The Vault API path to match
  • capabilities — The operations allowed on that path
  • * — Glob wildcard (matches any characters)

Path Matching Rules

Exact Match

# Matches ONLY secret/data/myapp/config — nothing else
path "secret/data/myapp/config" {
  capabilities = ["read"]
}

Glob Wildcard (*)

# Matches anything under secret/data/myapp/
# e.g., secret/data/myapp/config, secret/data/myapp/db
path "secret/data/myapp/*" {
  capabilities = ["read", "list"]
}

Segment Wildcard (+)

# Matches ONE path segment
# e.g., secret/data/team-a/config, secret/data/team-b/config
# Does NOT match secret/data/team-a/sub/config
path "secret/data/+/config" {
  capabilities = ["read"]
}

Root Path

# Matches the path itself (empty after prefix)
# e.g., secret/data/myapp (no trailing path)
path "secret/data/myapp" {
  capabilities = ["read"]
}

Path Priority

When multiple paths match, Vault uses the most specific match:

# Broad rule
path "secret/data/*" {
  capabilities = ["read"]
}

# More specific rule (wins for this exact path)
path "secret/data/admin/config" {
  capabilities = ["deny"]
}

Priority order (highest to lowest):

  1. Exact path match (no wildcards)
  2. Longest prefix with + segments
  3. Longest prefix with * glob
  4. Shorter prefixes

Capabilities

path "secret/data/myapp/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}
Capability HTTP Verb Description
create POST/PUT Create new data
read GET Read data
update POST/PUT Update existing data
delete DELETE Delete data
list LIST List keys
sudo — Access sudo-protected paths
deny — Explicitly deny (overrides everything)

Important KV v2 Paths

KV v2 has a different API structure than KV v1. Policies must account for the /data/, /metadata/, and /delete/ prefixes:

# Read and write secrets
path "secret/data/myapp/*" {
  capabilities = ["create", "read", "update"]
}

# List secret keys
path "secret/metadata/myapp/*" {
  capabilities = ["list", "read"]
}

# Delete secret versions
path "secret/delete/myapp/*" {
  capabilities = ["update"]
}

# Undelete (restore) versions
path "secret/undelete/myapp/*" {
  capabilities = ["update"]
}

# Permanently destroy versions
path "secret/destroy/myapp/*" {
  capabilities = ["update"]
}

Multiple Paths in One Policy

# dev-team policy
# Read app secrets
path "secret/data/dev/*" {
  capabilities = ["create", "read", "update", "delete", "list"]
}

path "secret/metadata/dev/*" {
  capabilities = ["list", "read"]
}

# Read shared secrets (read-only)
path "secret/data/shared/*" {
  capabilities = ["read", "list"]
}

path "secret/metadata/shared/*" {
  capabilities = ["list", "read"]
}

# Access transit encryption
path "transit/encrypt/dev-key" {
  capabilities = ["update"]
}

path "transit/decrypt/dev-key" {
  capabilities = ["update"]
}

Quick Reference

Syntax Meaning
path "exact/path" Exact match
path "prefix/*" Glob — any characters after prefix
path "prefix/+/suffix" Segment — matches one path segment
capabilities = ["read"] Allow read
capabilities = ["deny"] Explicit deny (overrides all)

Next: Capabilities Deep Dive