Access policies
Path-pattern permission grants that scope a service account's access — actions, wildcards, and evaluation.
A policy grants a service account a set of actions on a set of paths described by a pattern. A machine can do something only if at least one of its policies allows it.
Actions#
read · write · delete · list · rotate — or * for all. Each maps to a data-plane
operation:
| Action | Operation |
|---|---|
read |
GET a secret value / versions |
write |
PUT (create or new version) |
delete |
soft-delete a secret (and restore it) |
list |
list a namespace's secrets |
rotate |
re-key a secret: same value, fresh data key, new version |
write ⊇ rotate is the only containment between actions. Everything else is independent, so a
permission granting read, write, list cannot delete a secret no matter how well its pattern
matches the path — and the server checks the action before it looks at the pattern. That is why
pulsvault delete, undelete and mv can be refused on a key that writes happily, and why no starter
permission except <project>-manage-prod grants delete.
A rotation never changes a value — the server re-encrypts what is already stored, so the plaintext
is never sent anywhere. A job that only needs to re-key credentials on a schedule can hold rotate
alone and be unable to write a new value or read the current one.
write also satisfies rotate, since anyone who can write could rotate by writing the value back.
The reverse does not hold.
Path patterns#
Patterns match against the full canonical path, whose four slots are the project, the environment, the namespace and the secret:
*matches exactly one segment.**matches zero or more segments.- No leading slash; segments use
a–z, digits,_,.,-.
A permission may hold several paths. Separate them with commas — spaces around the comma are fine:
acme-web/dev/web/**, acme-web/local/web/**"dev and local, nothing else" is one intent, so it is one permission rather than two.
The project slug is the first segment of every path, so a pattern that begins with an
environment matches nothing. prod/** is not "everything in prod" — it would only match a project
literally named prod. Write */prod/** (every project) or acme-web/prod/** (one project).
| Pattern | Matches |
|---|---|
acme-web/prod/** |
everything in that project's prod environment |
*/prod/** |
prod in every project the account can reach |
acme-web/*/product/web/** |
the web product in every environment of one project |
acme-web/prod/product/web/db/* |
direct children of db in web prod |
<own-project>/dev/** |
dev in whichever project the machine belongs to — see below |
acme-web/** is refused, not accepted as "everything in the project": ** would swallow the
environment slot, so the grant would silently include production. Name the environment
(acme-web/prod/**), or tick Allow across every environment to get acme-web/*/** deliberately.
One permission, every project#
A literal pattern names exactly one project, and the only literal that spans them all — */dev/** —
grants every project, which is rarely what anyone wants. Writing {project} in the first slot
instead resolves to the machine's own project when the permission is evaluated:
{project}/dev/**One definition is therefore correct in every project and still cannot reach across them: the substitution can only ever produce the holder's own project. On a machine with no project it resolves to nothing and grants nothing, rather than quietly becoming a wildcard.
Elsewhere in these docs {project}/{env}/{namespace}/{secret} is notation for the four slots of a
path. In a permission pattern, {project} typed literally in the first slot is a feature with the
meaning above. It is valid in the first slot only.
Permissions without a path#
A permission may name an intent and leave the path out entirely — read-only = read + list, no
path. That is a template. It grants nothing on its own; when you attach it to a machine you
choose the environments and the path it takes on there, and each machine gets its own concrete grant.
Use one when the same intent applies at different paths on different machines. Use an ordinary
permission when the path is always the same.
| shared/*/** | read-only common secrets — see The Shared project |
Effect#
All policies are ALLOW at this phase (there is no DENY). Access is granted if any attached
policy allows the requested action on the requested path; otherwise it's denied (403). As
defense-in-depth, a policy can never grant access outside the service account's own space
(organization or owner).
Testing a pattern#
The policy editor includes a live tester — type a candidate path (e.g.
acme-web/prod/product/web/db/password) and see instantly whether the pattern would grant it. This
runs entirely in your browser.
Grant the narrowest pattern and action set a pipeline needs. A deploy job that only reads two
secrets should get read on those paths — not * on acme-web/prod/**.
Related#
- Machine identity permissions — attaching these to a machine
- Paths, plans & roles
When a path moves#
A permission points at a path, and paths get renamed. Nothing about a renamed namespace makes the permissions that named it illegal — they stay attached and keep evaluating, they just stop matching anything. Pipelines start failing on a path they are silently no longer granted, and the permission looks fine on every screen that shows it.
Two things close that gap.
At rename time. Renaming a namespace first asks what it would orphan. If any permission names the path being moved, you see which ones and which machines hold them, before anything is committed:
2 permissions will stop matching after this rename.
read-web ci-runner, deployer
acme-web/dev/web/** → acme-web/dev/frontend/**
[ Cancel ] [ Rename only ] [ Rename and update 2 ]Only permissions that name the moving path are listed. A broader one like acme-web/dev/**
covers the path before and after, so it is not affected and is not shown.
Updating is always a choice. A rewrite changes what a machine can reach, so it is never applied on your behalf — Rename only leaves the permissions exactly as they were.
Afterwards. The Health tab on the Permission library lists every permission that has stopped pointing at anything, worst first:
| Status | Meaning |
|---|---|
| Healthy | reaches at least one existing secret |
| Broken | it was granting access recently and now matches nothing — usually a rename |
| Never matched | it has never matched anything, which normally means a typo in the path |
| Impossible | it names an environment or project that does not exist, so it can never match |
| Nothing there yet | the namespace exists but has no secrets — fine, not an error |
Broken and Never matched both reach zero secrets today. They are separated because the fixes differ: one path used to work and moved, the other never existed. Rows also show denial counts from the last 24 hours, so a broken permission and the failures it is causing appear together rather than on two different screens.
A permission that matches nothing is a warning, never a blocked save. Pipelines routinely create the secret after the permission that will read it.
Where a permission is created#
Which screen you use decides the permission's scope — there is no scope switch on the form.
| Where | Project slot | |
|---|---|---|
| Belongs to one project | that project's Permissions screen | fixed to the project, not editable |
| Shared across projects | DevOps → Permissions | your choice, defaulting to the holder's own project |
Most permissions belong to one project. Create one in the DevOps area only when the same grant is meant for machines in several projects.
A shared permission appears on every project's Permissions screen as Inherited, read-only, with a link back to the DevOps area — it is held by machines everywhere, so changing it from inside one project would be changing it for all the others without saying so. Only an organization administrator can create or change one.
If a shared permission's path names a specific project, it is listed everywhere but can only ever be granted in that one — other projects mark it not usable here. That is usually a sign it should have been created inside that project instead.
Requiring approval for cross-environment permissions#
A cross-environment permission spans dev, staging and production at once — the widest grant this system issues. DevOps → Permissions carries a space-level switch that restricts creating one to organization administrators.
Turning it on gates new permissions only; it never revokes what is already granted, and the screen tells you how many cross-environment permissions already exist so the decision is made with that number in view rather than after discovering it.