misleading abstraction, twice over
A Policy Attached to Nothing: Two Silent Vault Failures Hiding Behind One Error
TL;DR: An Ansible lookup for AdGuard's admin credentials failed with a precise,
accurate Forbidden: Permission Denied error. The AppRole's policy list already
named the exact policy the error complained about missing. Both of those things
were true at once because Vault lets you attach a policy name to a role before the
policy itself exists — and a second, unrelated typo in the same list had been
silently doing the same thing to the built-in default policy for who knows how
long.
The symptom
Error was a <class 'ansible.errors.AnsibleError'>, original message: Forbidden:
Permission Denied to path ['dns/adguard-admin'].
Reasonable next step: check whether the dns-reader policy — written specifically
to grant read access to that exact path — was actually attached to the ansible
AppRole.
vault read auth/approle/role/ansible
policies [authentik dns-reader gitlab-reader grafana k3s-reader policies=default]
dns-reader was right there in the list.
Diagnosis
Checking the policy itself, rather than trusting that its presence in the list meant it existed:
vault policy read dns-reader
No policy named: dns-reader
Vault's AppRole policies field is just a list of strings — attaching a name to a
role doesn't require that name to correspond to a real policy at the time it's
attached, or ever. A reference to nothing contributes zero permissions, silently,
with no error at attach time. The role had been carrying a reference to a policy
that was never actually created — likely because the step to attach it and the
step to create it were done in the wrong order, and nothing caught the gap since
neither half complained on its own.
A second bug found only by reading the same output carefully
Look again at the full policy list: ...grafana k3s-reader policies=default.
That last entry isn't a fourth real policy — it's the literal fragment
policies=default, meaning a command intended to end ...,default was instead
typed as ...,policies=default at some point, and Vault split the string exactly
on the commas it was given. The same silent-failure mechanism as dns-reader
applies here too: a policy name that resolves to nothing grants nothing. This
means the actual built-in default policy — which every token normally gets
automatically — had been quietly missing from this AppRole's grants for an
unknown length of time, discovered only as a side effect of investigating a
completely different missing policy.
Fix
Both problems share one fix, since vault write auth/approle/role/ansible
policies=... replaces the entire list rather than appending to it — meaning the
real current list had to be captured correctly before rewriting anything, not
assumed from memory of what should be there:
cat > dns-reader.hcl << 'EOF'
path "secret/data/dns/*" {
capabilities = ["read"]
}
EOF
vault policy write dns-reader dns-reader.hcl
vault write auth/approle/role/ansible policies=default,authentik,dns-reader,gitlab-reader,grafana,k3s-reader
Followed immediately by a direct re-read to confirm the correction actually landed clean, rather than trusting the write command's own success message:
vault read auth/approle/role/ansible
What this demonstrates
Two independent bugs, both invisible from the same vantage point, both surfaced only because the actual policy list was read character by character rather than skimmed for "does the name I'm looking for appear somewhere in here." Vault's design choice to allow a dangling policy reference without complaint is reasonable on its own terms — policies and roles are managed separately, and enforcing existence at attach time would create its own ordering headaches — but it means a role's policy list can look completely correct while granting a fraction of what it claims to. The second bug is the sharper lesson: it was never being actively investigated at all, and would likely still be sitting there unnoticed if the first one hadn't forced a character-by-character read of the exact same line.