Access Policies
Reusable bundles of conditions on secret reads. Time windows, IP allowlists, rate caps, co-sign requirements, and lifecycle triggers, all expressed as named policies and bound to secrets opt-in.
Access policies are named, reusable bundles of conditions on secret reads. You create a policy once inside a project, configure the conditions it should enforce, and bind secrets to it. From that moment on, any read of a bound secret has to satisfy every condition the policy carries. A read that does not is rejected, and your audit log shows which condition failed.
A secret with no policy bound to it is unaffected. Policies are opt-in.
Building constraints as named policies, instead of as individual settings on each secret, means you set a rule once and apply it to as many secrets as you need. When the rule changes, you change it in one place. When you want every secret bound to a policy retired together, one edit to the policy retires them. There is one canonical "this is how we treat production credentials" that you can audit against, instead of dozens of separate per-secret configurations that drift apart over time.
The Six Axes
A policy can carry up to six independent access conditions. Each section in the policy editor has its own toggle. A section that is toggled off is ignored even if the fields are populated, so you can stage a configuration before you turn it on.
Time Window
The read is allowed only inside a recurring weekly time window expressed in an IANA timezone.
| Field | Type |
|---|---|
| Start time | HH:mm in the chosen timezone |
| End time | HH:mm in the chosen timezone |
| Timezone | Any IANA zone (e.g. Europe/Copenhagen, America/New_York) |
| Days | One or more of mon..sun |
Windows that wrap midnight are accepted. A window of 22:00-06:00 allows reads between 22:00 and 06:00 local time in the configured zone.
A read outside the window is rejected with HTTP 403.
IP / CIDR Allowlist
The source IP of the machine's request must match at least one of the configured CIDRs.
Both IPv4 and IPv6 are supported. Single addresses (203.0.113.50) and ranges (10.0.0.0/8) are accepted. CIDRs are normalised to their canonical network form on save, so duplicates collapse and host bits below the prefix are zeroed.
A read from an IP outside every configured CIDR is rejected with HTTP 403.
This is independent from the vault-wide IP Allowlist, which gates every machine request before any secret-specific evaluation runs. Per-secret IP rules let you constrain access to one credential to one network even when the rest of the vault accepts a wider range.
Read-Rate Cap
Limits how many times a single secret can be successfully read in a sliding window.
| Field | Effect |
|---|---|
| Max per day | Maximum successful reads per UTC calendar day |
| Max per minute | Maximum successful reads per wall-clock minute |
The cap applies to each bound secret on its own. Two secrets bound to the same rate-capped policy each get the full quota: a busy client on one secret cannot exhaust the budget for the other.
A read that would exceed the configured limit is rejected with HTTP 429 (Too Many Requests).
Co-Sign Specific
Requires that another named machine in the same project has logged a successful read of this secret recently.
| Field | Type |
|---|---|
| Co-signer machine | A machine in the same project, picked by ID |
| Window | Number of seconds (1 to 86400) |
The fetcher cannot be its own co-signer. A common shape is to bind production read access to a policy that requires a "release-controller" machine to have just read the same secret, so a deploy-time read is the only one that unlocks the credential. Reads not preceded by an in-window co-sign are rejected with HTTP 403.
TTL: Time-Based Destruction
The bound secret is destroyed at a fixed wall-clock moment.
This applies to the secret, not to the policy. Every secret bound to a policy with a destruction time shares that destruction time, so one edit retires a whole bundle of credentials at once.
Both the read path and a five-minute background sweep apply the destruction. Reads that arrive after expiry are rejected with HTTP 410 (Gone). The sweep handles the case where no machine reads after expiry, so a secret bound to an expired policy cannot stay live past its expiry.
The audit entry for a destroyed secret is attributed to system:ttl-time so it is distinguishable from a manual delete.
TTL: Read-Count-Based Destruction
The bound secret is destroyed after it has been successfully read a configured number of times in total.
The read that exhausts the budget still returns the value. Subsequent reads return HTTP 410. This matches "destroy after N reads", not "after N-1". The audit entry is attributed to system:ttl-reads.
Rotate After N Reads
Triggers an immediate rotation once the bound secret has been read a configured number of times since its current value was minted.
The read that crosses the threshold succeeds. The rotation runs immediately afterwards. The count resets to zero after each rotation, so the trigger re-arms for the new value.
This axis only makes sense for secrets that have rotation infrastructure already configured: either a rotation schedule (configured from the secret's edit panel) or a managed-secret config with an active agent. Binding a secret without rotation infrastructure to a policy that carries a rotate-after-N value is rejected at bind time with HTTP 400, because the trigger would have nothing to rotate.
Binding a Secret to a Policy
A secret can be bound to at most one policy. From the policy's perspective, many secrets can be bound to it. From the secret's perspective, the binding is single-valued: this secret follows this policy, or no policy at all.
Cross-project binding is rejected. A secret can only bind to a policy in its own project. The project is the trust boundary in SikkerKey, and a binding spanning projects has no clean meaning.
Removing a binding (or deleting the policy that the binding pointed to, after every secret has been detached) reverts the secret to standard machine authentication as the full check.
Configuring Policies
Policies live on the Policies sub-page inside each project. The sub-page sits between Secrets and Machines in the project sidebar.
Creating a Policy
Click the + icon on the Policies page. The policy editor opens with five toggle-able axis sections plus a Lifecycle group covering the two TTLs and rotate-after-N.
Name the policy something the team will recognise (e.g. prod-db-policy, weekday-window, release-only). Names are unique inside a project: a second policy named prod-db-policy in the same project is rejected with HTTP 409.
Editing a Policy
Click any policy row to reopen the editor. Changes take effect on the next read of every bound secret. There is no per-binding override or pinned version: the policy is the source of truth for the rules its bound secrets follow.
If you want a different rule set for a subset of bound secrets, create a second policy and rebind those secrets to it.
Binding Secrets
Bindings are managed from the Secrets page. Each row in the secrets table has a Policy column with a dropdown listing every policy in the current project plus a No policy option. Picking a policy attaches the secret immediately. Picking No policy detaches it.
For multi-secret operations, select rows on the secrets table and use the bulk Bind to policy action in the toolbar. The selected policy applies to all selected secrets in one operation, replacing any existing bindings on those secrets.
Detaching All Bound Secrets
The policy editor in edit mode shows a Bound Secrets section listing every secret currently attached. Each row has a detach button, and the section header has a Detach all button for retiring a policy.
Deleting a policy that still has bindings is rejected with HTTP 409: detach the secrets first. This is intentional. Without the rejection, a policy delete would silently strip the constraints from every bound secret as a side effect, which is exactly the kind of one-action-many-consequences operation worth keeping explicit.
Granting Permission to Manage Policies
Creating, editing, deleting, binding, and unbinding all sit behind a project-scoped permission called policy_manage. Vault owners always have it. Team members get it only when granted on a per-project basis from the Teams page.
The permission is one bit on purpose. Splitting it into smaller pieces (read-only, attach-only) creates real gaps:
- An attach-only permission lets a partially-trusted member bind a secret to an existing over-permissive policy. The end result is the same as letting them write policies.
- A read-only permission discloses the constraint shape (IP allowlists, time windows, rate caps), which is the information someone planning around the rules would want.
Either you can shape how this project's secrets are accessed, or you cannot.
The Policies sub-page is hidden from the project sidebar for team members without the permission, and every editing and binding endpoint returns HTTP 403 server-side regardless of UI state.
See Permissions for the rest of the permission vocabulary, and the Teams page for how to grant policy_manage.
Order of Evaluation
Each read evaluates the bound policy in this order. The first failing condition rejects the request:
- TTL expiry (time-based). Returns HTTP 410 if past expiry.
- TTL exhaustion (read-count-based). Returns HTTP 410 if the total-reads budget is spent.
- Time window. Returns HTTP 403 if outside the permitted window.
- IP / CIDR allowlist. Returns HTTP 403 if the source IP is outside the configured ranges.
- Read-rate cap. Returns HTTP 429 if the per-day or per-minute count is at the configured cap.
- Co-sign. Returns HTTP 403 if no in-window co-sign read is recorded.
The HTTP status varies on purpose so machine clients can distinguish "not allowed" (403) from "rate limited" (429) from "credential is gone" (410). The response body itself is generic and never reveals which axis failed beyond what the status conveys.
Audit Trail
Every policy operation and every policy-driven outcome is logged. The full severity classification is in Audit Logging.
| Action | Severity | When it fires |
|---|---|---|
access_policy_create | medium | A policy was created |
access_policy_update | medium | A policy's fields were modified |
access_policy_delete | medium | A policy was removed (only possible with zero bindings) |
secret_policy_bind | medium | A secret was attached to a policy |
secret_policy_unbind | medium | A secret was detached from its policy |
secret_read_blocked | high | A read was rejected by the bound policy. Detail names the failing condition. |
secret_destroyed | critical | A bound secret was destroyed by a TTL trigger. system:ttl-time for time-based, system:ttl-reads for read-count-based. |
secret_rotate | info | An automatic rotation fired by a rotate-after-N trigger. The read that crossed the threshold is its own secret_read entry. |
When a team member with policy_manage performs a policy operation, the entry is written under both the actor's audit log and the vault owner's, so the owner always has visibility into changes to the constraint layer regardless of who made them.
What Policies Are Not
- Policies are not who-can-do-what controls for users. Granting permissions to team members and granting machines access to specific secrets are separate features. Policies apply on top of those, on the machine read path: they decide whether an already-authorised machine's read is allowed under the current conditions.
- Policies are not a key revocation mechanism. Detaching a secret from a policy reverts it to standard machine authentication, it does not invalidate the value or block previously-authorised machines from reading it. To stop a specific machine from reading a secret, revoke that machine's grant on it.
- Policies are not retroactive. Reads that happened before a policy was bound, or before a rule was tightened, are already in your audit log and cannot be unread.