Permits
Request, review, and manage Kubernetes resources and permissions through reusable templates.
Capsule’s Resource Permit API provides approved access to Kubernetes resources.
A user creates a namespaced ResourcePermit, Capsule renders the referenced template,
an authorized reviewer or the template’s automatic approval policy approves it, and
Capsule manages the resulting resources for the approved lifetime.
Resource Permits can create any discoverable Kubernetes type, including cluster-scoped
objects. Typical uses include temporary Role and RoleBinding objects for
break-the-glass access, access to a custom resource, or a managed service provisioned
for a tenant. An effective duration gives the permit an expiry time; without one,
it stays active until explicitly expired. Each resource group’s deletion policy
determines whether its resources are removed or retained when the permit ends.
API objects
The feature uses three APIs in capsule.clastix.io/v1beta2:
| Kind | Short name | Scope | Purpose |
|---|
ResourcePermit | rp | Namespace | A user’s request, parameters, reason, schedule, review, and rendered-resource status |
ResourcePermitTemplate | rpt | Namespace | A template available to requests in the same namespace |
GlobalResourcePermitTemplate | grpt | Cluster | A reusable template optionally restricted to selected namespaces |
A request explicitly identifies the kind and name of its template:
template:
kind: GlobalResourcePermitTemplate
name: temporary-widget-editor
Admission rejects missing templates, local templates from another namespace and
global templates that do not select the request namespace. The reference cannot be
changed after the request is created.
Lifecycle
flowchart LR
create[Create] --> prepare[Created: prepare and dry-run]
prepare -->|Manual approval| review[Requested: review]
prepare -->|Automatic approval| approved
prepare -->|Dry-run error| failed[Failed]
failed -->|Retry| retrying[Retrying]
retrying -->|Preflight recovered| review
retrying -->|Activation recovered| approved
retrying -->|Error| failed
review -->|Approve| approved[Approved: wait for start time]
approved --> active[Active]
approved -->|Activation error| failed
review -->|Deny| denied[Denied]
review -->|Cancel| deleted[Deleted]
active --> expired[Expired]
expired --> deleted
Pending is reserved for a request held by a system workflow. Automatic approval
skips manual review; rendering errors remain visible in Created, while dry-run or
activation failures enter the retryable Failed phase. The complete paths are shown in
Resource Permits.
When a request becomes active, Capsule uses server-side apply to manage the exact
manifests stored in the request status. On expiry it removes or orphans those
resources according to their policies. A finalizer provides cascading cleanup even
for cluster-scoped targets, for which a namespaced request cannot be an owner.
Minimal example
The following global template grants a named user read-only Pod access in the
namespace where the request is created. Save it as template.yaml.
Use a Capsule build that includes these CRDs and configure
request and review RBAC. The request
namespace must already exist. The template’s execution identity needs permission to
manage Roles and RoleBindings there and to grant the listed Pod permissions.
apiVersion: capsule.clastix.io/v1beta2
kind: GlobalResourcePermitTemplate
metadata:
name: temporary-pod-reader
spec:
defaultDuration: 30m
maxDuration: 2h
resources:
- targets:
- apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: '{{ .request.name }}-pod-reader'
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: '{{ .request.name }}-pod-reader'
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: '{{ .request.name }}-pod-reader'
subjects:
- kind: User
apiGroup: rbac.authorization.k8s.io
name: '{{ .user }}'
paramSchema:
type: object
required: [user]
additionalProperties: false
properties:
user:
type: string
minLength: 1
A user can then request the permit. Save this as request.yaml:
apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePermit
metadata:
name: investigate-pods
namespace: solar-uat
spec:
template:
kind: GlobalResourcePermitTemplate
name: temporary-pod-reader
params:
user: alice
reason: Investigate incident INC-1042
duration: 20m
kubectl apply -f template.yaml
kubectl wait --for=condition=Ready globalresourcepermittemplate/temporary-pod-reader --timeout=60s
kubectl apply -f request.yaml
kubectl wait --for=jsonpath='{.status.phase}'=Requested resourcepermit/investigate-pods -n solar-uat --timeout=60s
kubectl capsule rp review investigate-pods -n solar-uat --approve
kubectl get resourcepermit investigate-pods -n solar-uat
Run template installation as a template administrator, request creation as the
requester, and review as an authorized reviewer. Generated Role and RoleBinding names
include the permit name so separate permits can coexist in the same namespace.
Capsule overwrites spec.requester with the authenticated user’s name and groups;
clients should not rely on supplying that field themselves.
Security model
The Resource Permit API combines Kubernetes authorization with Capsule admission checks:
- Kubernetes RBAC controls who may create requests or update their status.
- Template approvers and CEL conditions further restrict approval.
- The rendered manifests, reviewer identity, template version and resolved
ServiceAccount are controller-owned status fields and cannot be forged.
- Managed-resource protection can block direct update and delete operations while
a request is active.
- Context reads and resource operations use one recorded ServiceAccount identity.
- Form hints never grant access and must list resources with the requesting user’s
credentials.
The template author remains responsible for defining safe parameters, conditions,
resource policies, and RBAC for the execution identity.
Continue reading
Use Templates to define inputs, rendering, context, and execution
ServiceAccounts. Resource Permits covers lifecycle, approval, commands,
and operations. Reusable behavior is documented under concepts:
1 - Resource Permit Templates
Define reusable resources, inputs, execution identities, readiness, and limits.
A template describes what Capsule renders for a ResourcePermit, who executes it, and
how long the resulting resources may remain active.
Choose the template scope
Use ResourcePermitTemplate when the workflow and its execution identity belong to one
namespace. Use GlobalResourcePermitTemplate when platform administrators provide the
same workflow to several namespaces.
| Behavior | ResourcePermitTemplate | GlobalResourcePermitTemplate |
|---|
| Scope | Namespaced | Cluster |
| Request location | Same namespace | Any allowed namespace |
| ServiceAccount reference | Name only, in the template namespace | Name and namespace |
| Namespace selectors/status | Not applicable | Supported |
| Ready condition and observed generation | Supported | Supported |
A minimal local template renders its namespaced targets into the ResourcePermit
namespace when metadata.namespace is omitted:
apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePermitTemplate
metadata:
name: temporary-config
namespace: solar-uat
spec:
paramSchema:
type: object
required: [owner]
properties:
owner:
type: string
resources:
- targets:
- apiVersion: v1
kind: ConfigMap
metadata:
name: temporary-config
data:
requestedBy: '{{ .owner }}'
For a global template, namespaceSelectors is an OR list. Omitting it allows requests
from every namespace.
apiVersion: capsule.clastix.io/v1beta2
kind: GlobalResourcePermitTemplate
metadata:
name: production-debug
spec:
namespaceSelectors:
- matchLabels:
environment: production
- matchExpressions:
- key: resource-permit.example.com/enabled
operator: In
values: ["true"]
resources:
- targets:
- apiVersion: v1
kind: ConfigMap
metadata:
name: production-debug
Capsule publishes matching namespaces in .status.namespaces; an unrestricted global
template reports "*". Admission rejects a reference when the template is missing,
its current generation has not reconciled, or the request namespace is not selected.
Template readiness
Both template kinds expose .status.observedGeneration and a Ready condition in
.status.conditions. Successful reconciliation reports Ready=True with reason
Succeeded; a failure reports Ready=False with reason Failed and an explanatory
message. The condition’s observedGeneration identifies the template generation
that was checked. Compare it with .metadata.generation after editing a template.
Template readiness covers static validation of the approval policy, duration limits,
parameter schema, and resource definitions. Global templates also resolve namespace
selection. On failure, their namespace list is cleared so it cannot grant access from
an earlier successful reconciliation. A ready template still requires each permit’s
parameters, context reads, rendering, execution permissions, and dry-run to succeed.
For example, an unrestricted global template reports:
status:
observedGeneration: 1
namespaces: ["*"]
conditions:
- type: Ready
status: "True"
reason: Succeeded
message: reconciled
observedGeneration: 1
lastTransitionTime: "2026-09-09T12:00:00Z"
A namespaced template has the same readiness fields without namespaces. Both kinds
show Ready and Status columns in kubectl get output:
kubectl get rpt -n solar-uat
kubectl get grpt
kubectl wait --for=condition=Ready rpt/temporary-config -n solar-uat --timeout=60s
kubectl wait --for=condition=Ready grpt/production-debug --timeout=60s
The controller exports readiness gauges for both kinds; see
Metrics for names, labels, and example queries.
spec.paramSchema validates ResourcePermit.spec.params before context is loaded or
resources are rendered:
paramSchema:
type: object
additionalProperties: false
required: [name, crd]
properties:
name:
type: string
minLength: 1
maxLength: 63
crd:
type: string
See Parameter Schemas and Dynamic Forms
for JSON Schema 2020-12, cross-field CEL, and Kubernetes resource selectors used by
clients such as Headlamp.
Every entry in spec.resources is one resource group. Its policy applies to all
manifests produced by that entry. A group may contain structured targets, a
multi-document template, or both:
resources:
- policy:
creation: Owner
protect: true
force: false
deletion: Remove
targets:
- apiVersion: v1
kind: ConfigMap
metadata:
name: '{{ .name }}'
data:
requestedBy: '{{ .request.username }}'
template: |
---
apiVersion: v1
kind: Secret
metadata:
name: {{ $.params.name }}
stringData:
requestedBy: {{ $.request.username }}
Direct targets expose parameters and loaded context indexes at the root. Generated
templates use a structured root:
| Data | Direct target | Generated template |
|---|
| Request parameters | .name | $.params.name |
| Loaded context | index .crdInfo 0 | index $.context.resources.crdInfo 0 |
| Trusted request data | .request.username | $.request.username |
The trusted request object contains:
| Field | Meaning |
|---|
name | ResourcePermit name |
username | Authenticated requester |
groups | Authenticated groups as a string array |
timestamp | Stable ResourcePermit creation time in UTC RFC3339 |
Use join "," .request.groups for a direct string field and
join "," $.request.groups in a generator. The request key is reserved; parameters
and context indexes cannot replace these trusted values. Missing template keys are
errors, and environment-reading functions such as env are not exposed.
The policy shown above uses all defaults. Its ownership, conflict, protection, and
cleanup semantics are documented once in
Managed Resources and Server-Side Apply.
Load Kubernetes context
Context references load arbitrary Kubernetes GVKs before every resource group is
rendered. Each result is an array, even when selecting one named object. Context
indexes must be distinct from parameter names and the reserved request key. Here,
crd is the input parameter and crdInfo holds the loaded CRD.
context:
resources:
- apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
name: '{{ .crd }}'
index: crdInfo
optional: false
- apiVersion: v1
kind: ConfigMap
namespace: '{{ .managementNamespace }}'
selector:
matchLabels:
access.example.com/source: resource-permit
index: management
name, namespace, and selector values may use declared parameters. For a namespaced
GVK, an omitted namespace means the ResourcePermit namespace; cluster-scoped GVKs ignore
it. The execution ServiceAccount performs all reads, and Capsule sanitizes objects
before exposing them to templates.
Access a ConfigMap value whose key contains punctuation with index:
{{ index (index .management 0).data "team-a" }}
{{ index (index $.context.resources.management 0).data "team-a" }}
The first expression is for a direct target and the second for a generated template.
Choose the execution identity and limits
Capsule resolves one ServiceAccount for context reads, discovery, apply, reconciliation,
dry-run, and cleanup. It always records the result in
.status.request.impersonation, including when the controller ServiceAccount is used.
| Template | Explicit identity | Configuration fallback | Final fallback |
|---|
| Local | spec.impersonation.name in the template namespace | tenantDefaultServiceAccount in that namespace | Capsule controller ServiceAccount |
| Global | spec.impersonation.name and .namespace | globalDefaultServiceAccount in globalDefaultServiceAccountNamespace | Capsule controller ServiceAccount |
Local and global references respectively look like this:
# ResourcePermitTemplate
spec:
impersonation:
name: resource-permit-runner
---
# GlobalResourcePermitTemplate
spec:
impersonation:
name: resource-permit-global-runner
namespace: capsule-system
Configure fallbacks centrally:
apiVersion: capsule.clastix.io/v1beta2
kind: CapsuleConfiguration
metadata:
name: default
spec:
impersonation:
tenantDefaultServiceAccount: resource-permit-runner
globalDefaultServiceAccount: resource-permit-global-runner
globalDefaultServiceAccountNamespace: capsule-system
The ServiceAccount needs get for named context objects or list for selected
context, plus the lifecycle verbs required by every rendered GVK. This usually means
get, create, patch, and update, plus delete for resources that Capsule may
remove. Conditional targets need get even when the expression is always false.
Cleanup also needs get on each target Namespace; the
conditional example limits this with resourceNames.
Cluster-scoped inputs and outputs require appropriately scoped RBAC. Keep this
execution identity separate from approval identities: the approver authorizes the
snapshot; the ServiceAccount executes it.
Capsule performs a server-side dry-run with this identity before the request becomes
reviewable. The ServiceAccount is protected from deletion while it is referenced by a
ResourcePermit that has not expired, a TenantResource, or a GlobalTenantResource.
Duration and retention limits are also template properties:
defaultDuration: 30m
maxDuration: 2h
keepFor: 7d
defaultDuration applies when the request omits a duration, maxDuration limits the
approved value, and keepFor retains an expired request for auditing. Without an
effective duration, access remains until explicitly expired.
Conditional resources
Both template kinds support spec.resources[].policy.condition. The shared
apply-condition contract
defines CEL inputs (object and now), validation, and skipped-target behavior.
Conditions run after rendering, independently for each destination.
| Field | Decision | Inputs |
|---|
spec.approvals.conditions[] | Approval eligibility; conditions are ORed | request, requester, reviewer |
spec.resources[].policy.condition | Whether to apply one target’s rendered content | object, now |
A false apply condition is a successful skip. It does not deny approval, keep the
request pending, or schedule another attempt once Active. A permit can become
Active even when every target is skipped. Use approval conditions to gate the whole
request. Apply expressions are plain CEL and cannot reference template parameters
or loaded context directly; a false expression cannot suppress a rendering error.
For recurring time-based updates, use a TenantResource or GlobalTenantResource,
such as the age-key rotation example.
See conditional target lifecycle for
preflight, activation, snapshot immutability, retries, and cleanup.
Namespaced example
These examples assume a solar Tenant with a solar-system namespace. Run the
ServiceAccount, RBAC, and template setup as a platform administrator. Submit each
ResourcePermit as a tenant owner allowed to create requests in that namespace.
Use controller and CRD versions that support policy.condition.
Create an execution identity that can manage ConfigMaps only in solar-system.
Cleanup also needs permission to read that Namespace; the ClusterRole limits
that read to its exact name. Requester permissions remain separate from the
execution identity’s permissions.
apiVersion: v1
kind: ServiceAccount
metadata:
name: condition-runner
namespace: solar-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: condition-runner
namespace: solar-system
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: condition-runner
namespace: solar-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: condition-runner
subjects:
- kind: ServiceAccount
name: condition-runner
namespace: solar-system
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: solar-condition-namespace-reader
rules:
- apiGroups: [""]
resources: ["namespaces"]
resourceNames: ["solar-system"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: solar-condition-namespace-reader
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: solar-condition-namespace-reader
subjects:
- kind: ServiceAccount
name: condition-runner
namespace: solar-system
Create this template as the administrator, wait for Ready=True, then submit the
ResourcePermit as a tenant owner. Save the template and request in separate files
so they can be applied with their respective identities. Automatic approval is
intentional in this limited ConfigMap example; configure approvers for workflows
that need review.
apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePermitTemplate
metadata:
name: initialize-profile
namespace: solar-system
spec:
impersonation:
name: condition-runner
approvals:
auto: true
defaultDuration: 5m
keepFor: 10m
resources:
- policy:
creation: Owner
force: false
protect: true
deletion: Remove
condition: "object == null"
targets:
- apiVersion: v1
kind: ConfigMap
metadata:
name: conditional-profile
data:
profile: temporary
- policy:
condition: "false"
targets:
- apiVersion: v1
kind: ConfigMap
metadata:
name: skipped-profile
data:
profile: disabled
---
apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePermit
metadata:
name: initialize-profile
namespace: solar-system
spec:
template:
kind: ResourcePermitTemplate
name: initialize-profile
With no preexisting targets, conditional-profile is created and
skipped-profile stays absent. If conditional-profile already exists, it is
skipped without adoption or modification. Both outcomes allow the request to
become Active. keepFor retains the expired request for inspection; it does not
extend the target’s five-minute lifetime.
Omitting a target namespace defaults namespaced resources to the permit’s
namespace. A namespaced template can only be referenced from its own namespace.
The local impersonation reference always resolves there.
Global example
A global template uses the same policy, but its execution identity includes a
namespace. namespaceSelectors restrict where a permit can reference it; they
do not grant the execution identity permission to read or write resources.
The example below reuses the previous ServiceAccount and RBAC and is available
only in solar-system.
apiVersion: capsule.clastix.io/v1beta2
kind: GlobalResourcePermitTemplate
metadata:
name: initialize-global-profile
spec:
impersonation:
name: condition-runner
namespace: solar-system
namespaceSelectors:
- matchLabels:
kubernetes.io/metadata.name: solar-system
approvals:
auto: true
defaultDuration: 5m
keepFor: 10m
resources:
- policy:
creation: Owner
force: false
protect: true
deletion: Remove
condition: "object == null"
targets:
- apiVersion: v1
kind: ConfigMap
metadata:
name: conditional-global-profile
data:
profile: temporary
---
apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePermit
metadata:
name: initialize-global-profile
namespace: solar-system
spec:
template:
kind: GlobalResourcePermitTemplate
name: initialize-global-profile
Wait for solar-system in the global template’s status.namespaces before
submitting its request. A request from another tenant namespace is rejected by
the template’s namespace selection.
Complete CRD access example
This global template loads a CRD and creates a dedicated Role and RoleBinding for the
requester. The namespaced manifests omit metadata.namespace, so Capsule places them
in the ResourcePermit namespace.
Install the target CRD and label the request namespace before using this example:
kubectl label namespace solar-uat resource-permit.example.com/enabled=true
The execution identity must be able to read CRDs and manage and grant the rendered
RBAC permissions. A reviewer must belong to platform-on-call and have permission
to update the permit’s status.
apiVersion: capsule.clastix.io/v1beta2
kind: GlobalResourcePermitTemplate
metadata:
name: temporary-crd-editor
spec:
defaultDuration: 30m
maxDuration: 4h
keepFor: 14d
namespaceSelectors:
- matchLabels:
resource-permit.example.com/enabled: "true"
approvals:
approvers:
- kind: Group
name: platform-on-call
paramSchema:
type: object
additionalProperties: false
required: [name, crd]
properties:
name:
type: string
crd:
type: string
context:
resources:
- apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
name: '{{ .crd }}'
index: crdInfo
optional: false
resources:
- policy:
creation: Owner
protect: true
force: false
deletion: Remove
template: |
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: {{ $.params.name }}
rules:
- apiGroups:
- {{ (index $.context.resources.crdInfo 0).spec.group }}
resources:
- {{ (index $.context.resources.crdInfo 0).spec.names.plural }}
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: {{ $.request.name }}
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: {{ $.params.name }}
subjects:
- kind: User
apiGroup: rbac.authorization.k8s.io
name: {{ $.request.username }}
See Resource Permits for approval, review, activation,
expiry, and troubleshooting.
2 - Resource Permits
Create, review, activate, expire, and monitor permits for Kubernetes resources.
A ResourcePermit is namespaced. Its namespace is the authorization boundary for
creating the request and the default destination for namespaced resources rendered by
its template. A request may also create cluster-scoped resources.
Lifecycle
The normal lifecycle is deliberately small:
| Phase | Meaning |
|---|
Created | Accepted by admission; Capsule is resolving and rendering the request |
Requested | Ready for manual review |
Pending | Reserved for a system-held request |
Denied | A reviewer denied the request |
Approved | Approved and waiting for activation |
Active | The rendered resources are being managed |
Failed | A resource preflight or activation failed and may be retried |
Retrying | A caller requested another preflight or activation attempt |
Expired | Access ended; cleanup and retention apply |
Creation
flowchart LR
create[Create] --> admission[Admission checks]
admission -->|Rejected| rejected[Not created]
admission -->|Accepted| created[Created]
created --> render[Resolve and render]
render -->|Error| createdError[Created; Ready = False]
render --> dryrun[Dry-run as execution identity]
dryrun -->|Manual| requested[Requested]
dryrun -->|Automatic| approved[Approved]
dryrun -->|Error| failed[Failed]
Admission first verifies that the referenced template exists, is available in the
request namespace, and accepts the parameters, requested duration, start time, and
automatic-approval conditions. Capsule then loads context, resolves the execution
ServiceAccount, and renders the resources. It performs a server-side dry-run with the
resolved execution identity before review. Template lookup, context, or rendering
failures remain in Created with Ready=False; a dry-run failure enters Failed.
Nothing is applied before approval.
Review
flowchart LR
requested[Requested] -->|Approve| approved[Approved]
requested -->|Deny| denied[Denied]
requested -->|Cancel| deleted[Deleted]
Pending has the same review and cancellation paths, but is reserved for system
workflows. With automatic approval, Capsule moves directly from successful rendering
to Approved and records a system reviewer.
Access and cleanup
flowchart LR
approved[Approved] --> active[Active]
approved -->|Apply error| failed[Failed]
failed -->|Retry| retrying[Retrying]
retrying -->|Recovered| resume[Requested or Approved]
retrying -->|Error| failed
approved -->|Expire early| expired[Expired]
active -->|Duration ends or expire| expired[Expired]
denied[Denied] -->|Expire| expired
expired --> cleanup[Remove or orphan resources]
cleanup --> retention[Retention if configured]
retention --> deleted[Deleted]
Capsule writes Active only after every resource applies successfully. It uses a
finalizer for cleanup because a namespaced request may manage cluster-scoped objects.
The detailed apply, ownership, and cleanup behavior is described in
Managed Resources and Server-Side Apply.
Normal-user deletion follows the lifecycle:
| Current state | Deletion |
|---|
Created, Requested, or Pending | Allowed as cancellation |
Denied, Approved, Active, Failed, or Retrying | Expire first |
Expired before keepUntil | Wait until retention ends |
Expired without retention, or after keepUntil | Allowed |
Configured Capsule administrators may request deletion in any phase. Finalizer cleanup
still runs for resources Capsule is responsible for removing.
Create and inspect a request
apiVersion: capsule.clastix.io/v1beta2
kind: ResourcePermit
metadata:
name: gateway-editor-incident-1042
namespace: solar-uat
spec:
template:
kind: GlobalResourcePermitTemplate
name: temporary-crd-editor
params:
name: incident-1042-gateway-editor
crd: httproutes.gateway.networking.k8s.io
reason: Restore the public API during INC-1042
duration: 45m
template.kind must be ResourcePermitTemplate or
GlobalResourcePermitTemplate. A local template is resolved in the request namespace;
a global template must select that namespace. The reference is immutable.
This request uses the complete CRD access template.
It grants access to the authenticated requester through $.request.username; the
template does not accept a separate user parameter.
params must satisfy the template schema. duration and startTime are optional,
and a start time supplied during creation must be in the future. Capsule replaces
spec.requester with the authenticated username and groups, so callers cannot claim a
different identity. See
Parameter Schemas and Dynamic Forms for
JSON Schema, CEL, and Kubernetes-backed form fields.
Before review, Capsule records:
- the template kind, name, and
resourceVersion; - the resolved execution ServiceAccount, including the controller ServiceAccount when
it is the fallback;
- the template approval policy used for this request;
- the effective duration and start time; and
- the concrete rendered manifests and their resource-group policies.
The rendered snapshot in .status.request.resources is what reviewers inspect and
what Capsule later applies. Updating the source template does not change an existing
request’s resource or approval-policy snapshot.
The following abridged status omits the rendered manifests and processed items:
status:
request:
template:
kind: GlobalResourcePermitTemplate
name: temporary-crd-editor
resourceVersion: "18422"
impersonation:
name: resource-permit-runner
namespace: capsule-system
approvals:
auto: false
approvers:
- kind: Group
name: platform-on-call
duration: 45m0s
startTime: "2026-08-31T10:00:00Z"
resources: []
phase: Requested
transitions:
- type: Created
timestamp: "2026-08-31T09:58:00Z"
actor:
name: alice
type: User
reason: CreatedByUser
- type: Requested
timestamp: "2026-08-31T09:58:01Z"
actor:
name: alice
type: User
reason: PendingReview
conditions:
- type: Ready
status: "True"
reason: Succeeded
message: rendered resources are ready for review
observedGeneration: 1
lastTransitionTime: "2026-08-31T09:58:01Z"
processedItems: []
size: 0
status.transitions is the chronological lifecycle audit trail. Each entry records
the entered state as type, its timestamp, compact actor identity (name and
type), reason, and optional message. Group claims are deliberately not duplicated
into transitions; requester groups remain in spec.requester.groups. conditions
describe operational state such as Ready, rather than lifecycle history.
Inspect the complete snapshot and conditions with:
kubectl get resourcepermit gateway-editor-incident-1042 -n solar-uat -o yaml
If template lookup, context loading, rendering, impersonation, dry-run, or apply
fails, Capsule sets Ready=False with a reason and message. Typical reasons are
TemplateResolutionFailed, TemplateContextFailed, TemplateRenderingFailed,
ImpersonationFailed, ResourcesNotReady, ResourceDryRunFailed, and
ResourceApplyFailed. A rendering failure remains Created and cannot be approved.
A dry-run or activation failure enters Failed and records its stage, retry phase,
reason, and message under .status.failure.
kubectl get resourcepermit gateway-editor-incident-1042 -n solar-uat \
-o jsonpath='{range .status.conditions[*]}{.type}{"="}{.status}{" "}{.reason}{": "}{.message}{"\n"}{end}'
Review and control a permit
Build the kubectl plugin from the Capsule source tree and put it on PATH:
make kubectl-capsule
export PATH="$PWD/bin:$PATH"
resource-permit, resourcepermit, rp, and permit are equivalent command names. Review
interactively, or provide a decision directly:
kubectl capsule rp review gateway-editor-incident-1042 -n solar-uat
kubectl capsule rp review gateway-editor-incident-1042 \
-n solar-uat --approve \
--message "Approved for INC-1042" \
--duration 30m \
--keep-for 14d
kubectl capsule rp review gateway-editor-incident-1042 \
-n solar-uat --deny --message "Use the read-only template"
The review table shows each concrete manifest as a row and its resource-group policy
in the left column. Reviewers may adjust effective duration, start time, and retention
within the template limits.
For scheduled access, supply a future RFC3339 timestamp through spec.startTime or
the review command’s --start-time flag. Omit it to allow activation after approval.
The CLI’s review command accepts permits in Requested; system workflows managing
Pending permits must use the status API.
Approval rules belong to the referenced template:
spec:
approvals:
auto: false
approvers:
- kind: User
name: Timmy
- kind: Group
name: platform-on-call
conditions:
- "request.spec.reason.startsWith('INC-')"
- "'platform-engineering' in requester.groups"
For manual approval, a non-empty approvers list must match the authenticated
reviewer in addition to Kubernetes RBAC. Supported kinds are User, Group, and
ServiceAccount. An empty list permits any RBAC-authorized reviewer. The approver list
restricts approval, not denial.
Conditions form an OR list: at least one must evaluate to true; an empty list is
unconditional. The approver check and condition block are ANDed. Expressions can use
the ResourcePermit as request, the authenticated requester as requester, and the
authenticated manual reviewer as reviewer.
With auto: true, approvers is ignored and conditions are checked during creation.
A non-matching request is rejected rather than left for manual review. Automatic
approval without conditions is unconditional and should only be used when every
caller who can create the request may safely receive the rendered access.
Clients submit only the desired phase change. Admission reconstructs reviewer identity
and lifecycle fields while preserving the requester, rendered snapshot, resolved
template version, execution ServiceAccount, and activation timestamps. A denied
request cannot later be approved, and Expired is terminal.
Activation normally happens automatically when the approved start time is reached.
Authorized callers can also request activation or end access early:
kubectl capsule rp activate gateway-editor-incident-1042 -n solar-uat
kubectl capsule rp expire gateway-editor-incident-1042 -n solar-uat
kubectl capsule rp retry gateway-editor-incident-1042 -n solar-uat
retry is valid only for a Failed request. After the underlying ServiceAccount,
RBAC, admission, or target problem is corrected, Capsule repeats the failed preflight
or activation path. The requester may request retry or early expiry when Kubernetes
RBAC permits the status update.
The commands support Kubernetes impersonation:
kubectl capsule rp review gateway-editor-incident-1042 \
-n solar-uat --as alice --as-group platform-on-call
Repeat --as-group for multiple groups. A group cannot be supplied without a user
unless the loaded kubeconfig already impersonates one.
Conditional target lifecycle
- Capsule renders the request and records the concrete targets and policies in
status.request.resources. - Preflight evaluates each condition against the live destination and dry-runs
allowed writes using the resolved execution ServiceAccount. No target is
persisted by preflight.
- Activation reevaluates against the live destination and current
now, then
applies allowed targets. A result can change between preflight and activation. - Active permits keep the reviewed snapshot. Editing the template does not
replace that snapshot, and Active permits do not periodically rerender or
reevaluate. Explicit retries of a Failed permit use its captured snapshot.
During an activation retry, a false condition can still reconcile protection
metadata on targets already applied by that permit, using the captured policy.
This does not make template policy edits affect an existing permit.
- Expiration/deletion cleans up applied targets according to
policy.deletion.
A target that was never applied by this permit is left untouched. A target
applied on an earlier attempt remains eligible for cleanup even if a later
attempt skips it.
A changed apply expression requires a new request to capture the updated template.
Retrying a Failed permit retains its reviewed expression and targets. Template policy
edits do not change an existing request’s snapshot. Examples for both template kinds
are in Conditional resources.
Inspect conditions without printing rendered target data, which may contain Secrets:
kubectl get resourcepermit REQUEST -n NAMESPACE -o json \
| jq '.status | {phase, failure, processedItems}'
Applied targets have a lastApply timestamp. Skipped targets report
ConditionNotMet: apply skipped; previously applied targets retain their last
successful timestamp, while never-applied targets have none. The condition never
suppresses expiration cleanup of targets the permit previously applied.
Operations and troubleshooting
Resource Permits depend on their CRDs, controllers, admission webhooks, and Kubernetes RBAC.
Chart RBAC for creating ResourcePermits is opt-in:
helm upgrade --install capsule projectcapsule/capsule \
--namespace capsule-system \
--set rbac.resourcepermits.create=true \
--set-string 'rbac.resourcepermits.labels.rbac\.authorization\.k8s\.io/aggregate-to-admin=true'
The generated role covers resourcepermits and its status subresource. Kubernetes RBAC
still determines the namespaces in which a user may create or review requests.
Keep the mutating and validating webhooks for resourcepermits,
resourcepermittemplates, and globalresourcepermittemplates enabled. The generic
Resource Permit webhook also protects managed resources when a group uses protect: true.
kubectl get validatingwebhookconfigurations,mutatingwebhookconfigurations
kubectl logs -n capsule-system deployment/capsule-controller-manager
Common problems can be narrowed down quickly:
| Symptom | Check |
|---|
| Request creation is forbidden | Enable chart RBAC and bind the user in the request namespace |
| Template reference is rejected | Check kind/name, local namespace, global namespace selection, and observed generation |
| Template is not ready | Inspect its .status.conditions; correct static validation errors or global namespace selection failures |
| Request is not ready | Inspect .status.conditions, .status.request, and processed items |
| Status transition is denied | Use the CLI or a minimal status patch; verify reviewer, CEL, readiness, and duration |
Request is Failed | Inspect .status.failure, correct the reported execution problem, then issue rp retry or rp expire |
| Request deletion is denied | Cancel only Created/Requested/Pending; otherwise expire and wait for retention |
| Managed object cannot be changed | Check whether its resource group uses protect: true |
Template rejected at spec.resources[i].policy.condition | Correct CEL syntax, undeclared variables, or a non-boolean expression such as 42 |
Preflight fails with ResourceDryRunFailed and ConditionEvaluationFailed | Guard missing fields and check timestamp formats; preflight has not persisted targets |
| Activation fails | Inspect failure and processed-item status; other independent targets may already have been applied |
| Conditional target read is forbidden | Grant the execution ServiceAccount get on that target, even for a false expression |
| Namespace read is forbidden during cleanup | Retain get on the target Namespace until cleanup finishes |
| Permit is Active but targets were skipped | A false condition is a successful skip; Active permits do not reevaluate when time passes or a template changes |
Useful diagnostics:
kubectl get resourcepermit REQUEST -n NAMESPACE -o json \
| jq '.status | {phase, request, review, failure, transitions, conditions, processedItems}'
kubectl get events -n NAMESPACE \
--field-selector involvedObject.kind=ResourcePermit,involvedObject.name=REQUEST
kubectl get globalresourcepermittemplate TEMPLATE -o yaml
kubectl get resourcepermittemplate TEMPLATE -n NAMESPACE -o yaml
kubectl get namespace NAMESPACE --show-labels
Configured Capsule administrators may delete ResourcePermits regardless of phase or
retention. This is an emergency escape hatch; cleanup still runs when a finalizer is
present.
A ResourcePermit never keeps its namespace in Terminating. Once namespace deletion
starts, Capsule allows the request to be deleted regardless of lifecycle phase or
archive retention, prunes resources covered by the Remove deletion policy, and
releases the request finalizer without waiting for keepUntil.
Capsule emits lifecycle events when a request becomes Requested, Approved,
Denied, Active, or Expired. Every transition event carries the actor as labels:
event.projectcapsule.dev/actor: alice
event.projectcapsule.dev/actor-kind: User
This makes events selectable by actor, for example:
kubectl get events -n NAMESPACE \
-l event.projectcapsule.dev/actor=alice,event.projectcapsule.dev/actor-kind=User
Kubernetes label values cannot contain characters such as @ or : and have a
63-character limit. Capsule converts such actor names to a stable, label-safe value
with a short hash suffix. The unmodified identity is always available in
.status.transitions[].actor. Transition entries remain the durable in-object audit
trail and include eventTime after Capsule emits the corresponding event.
For audit purposes, retain the trusted requester, reason and parameters, request
snapshot, review decision, transitions, conditions, and managed-resource results.
Configure keepFor on templates that require an in-cluster audit window after access
ends.
Metrics
The Capsule metrics endpoint exposes the following gauges:
| Metric | Labels | Meaning |
|---|
capsule_resourcepermit_phase | name, target_namespace, status | Value 1 for the permit’s current lifecycle phase; status contains the phase name |
capsule_resourcepermittemplate_condition | name, target_namespace, condition | Template readiness; condition="Ready" is 1 when true and 0 otherwise |
capsule_globalresourcepermittemplate_condition | name, condition | Global template readiness with the same values |
The permit phase gauge removes the previous phase series when the phase changes.
Template gauges describe the Ready condition, independently of permit lifecycle
phases. Their series are removed when the template is deleted and restored when
existing templates reconcile after a controller restart. A missing series does not
mean a template is ready.
For example, find failed permits and templates that are not ready:
capsule_resourcepermit_phase{status="Failed"} == 1
capsule_resourcepermittemplate_condition{condition="Ready"} == 0
capsule_globalresourcepermittemplate_condition{condition="Ready"} == 0