Skip to main content
Resource permissions attach an action to a Unkey Resource Name. They are the canonical authorization primitive for root keys, users, service tokens, and any other principal that needs scoped access to Unkey resources. The resource portion follows the Unkey Resource Names contract, including resource-name patterns and their matching semantics. The permission layer adds only the action suffix. Resource parsing and pattern matching, the rules deciding whether a stored resource pattern covers a requested concrete URN, live in pkg/urn. Action validation and permission evaluation live in pkg/rbac, which delegates resource comparison to pkg/urn instead of defining its own. Creation actions live on the nearest parent resource that already exists when the operation runs. For example, create_key applies to the keyspace, while read_key, update_key, and delete_key apply to concrete key resources or key-resource patterns. This avoids inventing collection URNs only for create operations. Top-level resources are the exception. Their parent is the workspace itself, which isn’t an expressible resource path in v1. For those actions, use the top-level collection wildcard as the closest expressible scope. For example, create_keyspace uses keyspaces/*, and create_role uses rbac/roles/*.

Format

A permission has a URN, a # separator, and an action.
unkey:v1:{workspace_id}:{resource_path}#{action}
For example:
unkey:v1:ws_123:keyspaces/ks_123#read_keyspace
unkey:v1:ws_123:keyspaces/ks_123#create_key
unkey:v1:ws_123:keyspaces/ks_123/keys/key_456#delete_key
unkey:v1:ws_123:projects/proj_123/apps/app_456#update_app
The action is not part of the URN. Code that evaluates permissions must parse the resource and action separately.

Actions

Actions are explicit operation names. They use lowercase words separated by underscores.
read_keyspace
create_key
delete_deployment
add_role
remove_permission
Most permissions name one explicit action. The only action wildcard is *, which is reserved for the admin:* migration escape hatch and is valid only on the global resource pattern **. There is no shorter global form: * matches exactly one path segment everywhere it appears, so only ** covers every resource in a workspace.
unkey:v1:ws_123:**#*
Do not add action wildcards to normal product permissions.

Create actions

Create actions authorize the parent resource that receives the child resource. Handlers must query the parent resource because the child ID may not exist until after the operation succeeds. For example, creating a key in a keyspace uses the keyspace resource:
unkey:v1:ws_123:keyspaces/ks_123#create_key
Creating nested deploy resources follows the same parent-resource rule:
unkey:v1:ws_123:projects/proj_123#create_app
unkey:v1:ws_123:projects/proj_123/apps/app_456#create_environment
unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789#create_deployment
unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789#create_domain
unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789#create_variable
The same parent can still hold non-create actions that target the parent itself, such as read_keyspace on keyspaces/ks_123. The action name determines which operation is authorized. Top-level create actions target the collection wildcard because there is no workspace resource path to attach the action to:
unkey:v1:ws_123:keyspaces/*#create_keyspace
unkey:v1:ws_123:rbac/roles/*#create_role
Go handlers can pass typed URN builders directly to rbac.U when the builder represents the parent resource. Use typed actions from pkg/rbac/permissions, where action structs are split by resource type:
import "github.com/unkeyed/unkey/pkg/rbac/permissions"

rbac.U(
	urn.New().Workspace(workspaceID).Keyspace(keyspaceID),
	permissions.CreateKey{},
)

rbac.U(
	urn.New().Workspace(workspaceID).Project(projectID),
	permissions.CreateApp{},
)

rbac.U(
	urn.New().Workspace(workspaceID).Project(projectID).App(appID),
	permissions.CreateEnvironment{},
)

Patterns

Stored permissions can contain resource patterns. Requested resources in audit logs and authorization checks must always use concrete URNs. v1 supports two resource-pattern operators.
OperatorMeaning
*Matches exactly one complete path segment.
/**Matches descendants below the preceding path.
The /** operator is valid only at the end of a resource path. It matches the path before /** and all descendants below that path. After a resource path uses * for an ID selector, every descendant ID selector must also use *. A permission can continue through child collection segments, but it can’t select a specific child under a wildcard parent. Valid:
unkey:v1:ws_123:projects/*/apps/*#read_app
unkey:v1:ws_123:projects/*/apps/*/environments/*/deployments/*#read_deployment
Invalid:
unkey:v1:ws_123:projects/*/apps/app_123#read_app
unkey:v1:ws_123:projects/proj_123/apps/*/environments/env_123#read_environment

Matching rules

Permission evaluation compares a requested {resource, action} tuple with the permissions assigned to the principal. A permission matches when all rules are true:
  1. The version matches.
  2. The workspace ID matches.
  3. The resource path matches exactly or by a valid resource pattern.
  4. The action matches exactly.
The matcher is segment-aware. It must split resource paths on / before matching wildcards. Rules 2 and 3, workspace and resource matching, are implemented by pkg/urn; pkg/rbac parses the action suffix and adds rule 4. For example, this permission:
unkey:v1:ws_123:keyspaces/*/keys/*#read_key
matches this request:
unkey:v1:ws_123:keyspaces/ks_123/keys/key_456#read_key
It does not match these requests:
unkey:v1:ws_123:keyspaces/ks_123#read_key
unkey:v1:ws_123:keyspaces/ks_123/keys/key_456#delete_key
unkey:v1:ws_123:keyspaces/ks_123/keys/key_456#update_key

Descendant grants

Use trailing /** when a permission must apply to any public descendant of a resource. This permission grants delete_deployment for deployments below one project:
unkey:v1:ws_123:projects/proj_123/**#delete_deployment
It matches:
unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789/deployments/d_abc#delete_deployment
It also matches the project itself:
unkey:v1:ws_123:projects/proj_123#delete_deployment
It also does not grant unrelated actions on descendants:
unkey:v1:ws_123:projects/proj_123/apps/app_456#delete_app
The action still has to match exactly.

Common permissions

These examples show how broad and narrow permissions use the same grammar.
unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789/deployments/d_abc#delete_deployment
unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789/deployments/*#delete_deployment
unkey:v1:ws_123:projects/proj_123/**#delete_deployment
unkey:v1:ws_123:keyspaces/ks_123/keys/*#read_key
unkey:v1:ws_123:rbac/roles/role_123#update_role

Audit logs

Audit logs store concrete resources and actions. They can also store the permission that authorized the action when authorization was evaluated.
{
  "actor": {
    "type": "key",
    "id": "key_root_123",
    "urn": "unkey:v1:ws_123:keyspaces/ks_123/keys/key_root_123"
  },
  "resource": {
    "urn": "unkey:v1:ws_123:projects/proj_123/apps/app_456/environments/env_789/deployments/d_abc",
    "type": "deployment"
  },
  "action": "delete_deployment",
  "authorization": {
    "permission": "unkey:v1:ws_123:projects/proj_123/**#delete_deployment",
    "matched": true
  }
}
Relationship changes can include multiple concrete resources. For example, adding a role to a key can log the key as the primary resource and the role as a target resource.
{
  "resource": {
    "urn": "unkey:v1:ws_123:keyspaces/ks_123/keys/key_456",
    "type": "key"
  },
  "targets": [
    {
      "urn": "unkey:v1:ws_123:rbac/roles/role_123",
      "type": "role"
    }
  ],
  "action": "add_role"
}

Migration from tuple permissions

Existing tuple permissions map to resource permissions by expanding the old resource type, resource ID, and action into a URN path. Legacy API permissions map through the keyspace that replaces the API.
Existing permissionResource permission
api.*.create_apiunkey:v1:{workspace_id}:keyspaces/*#create_keyspace
api.{api_id}.read_apiunkey:v1:{workspace_id}:keyspaces/{keyspace_id}#read_keyspace
api.{api_id}.create_keyunkey:v1:{workspace_id}:keyspaces/{keyspace_id}#create_key
api.{api_id}.read_keyunkey:v1:{workspace_id}:keyspaces/{keyspace_id}/keys/*#read_key
api.{api_id}.verify_keyunkey:v1:{workspace_id}:keyspaces/{keyspace_id}/keys/*#verify_key
identity.*.read_identityunkey:v1:{workspace_id}:identities/*#read_identity
ratelimit.*.delete_overrideunkey:v1:{workspace_id}:ratelimits/namespaces/*/overrides/*#delete_override
rbac.*.create_roleunkey:v1:{workspace_id}:rbac/roles/*#create_role
Migration code must preserve the authorized workspace. The old permission string does not contain a workspace ID, so the workspace has to come from the owning key, role, user, or session.

Invalid examples

These permissions are invalid.
ValueReason
unkey:v1:ws_123:keyspaces/ks_123Missing the action suffix.
unkey:v1:ws_123:keyspaces/ks_123.read_keyspaceUses the old tuple separator.
unkey:v1:ws_123:keyspaces/ks_123#*Uses an action wildcard outside the global resource pattern.
unkey:v1:ws_123:**/deployments/*#delete_deploymentUses recursive wildcard outside the trailing position.
unkey:v1:ws_123:projects/proj_123/**/deployments/*#delete_deploymentUses recursive wildcard in the middle of the path.
unkey:v1:ws_123:projects/*/apps/app_123#read_appSelects a specific child under a wildcard parent.
unkey:v1:ws_123:keyspaces/*/keys#read_keyDoes not match a catalog path shape.