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:

PhaseMeaning
CreatedAccepted by admission; Capsule is resolving and rendering the request
RequestedReady for manual review
PendingReserved for a system-held request
DeniedA reviewer denied the request
ApprovedApproved and waiting for activation
ActiveThe rendered resources are being managed
FailedA resource preflight or activation failed and may be retried
RetryingA caller requested another preflight or activation attempt
ExpiredAccess 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 stateDeletion
Created, Requested, or PendingAllowed as cancellation
Denied, Approved, Active, Failed, or RetryingExpire first
Expired before keepUntilWait until retention ends
Expired without retention, or after keepUntilAllowed

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

  1. Capsule renders the request and records the concrete targets and policies in status.request.resources.
  2. Preflight evaluates each condition against the live destination and dry-runs allowed writes using the resolved execution ServiceAccount. No target is persisted by preflight.
  3. Activation reevaluates against the live destination and current now, then applies allowed targets. A result can change between preflight and activation.
  4. 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.
  5. 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:

SymptomCheck
Request creation is forbiddenEnable chart RBAC and bind the user in the request namespace
Template reference is rejectedCheck kind/name, local namespace, global namespace selection, and observed generation
Template is not readyInspect its .status.conditions; correct static validation errors or global namespace selection failures
Request is not readyInspect .status.conditions, .status.request, and processed items
Status transition is deniedUse the CLI or a minimal status patch; verify reviewer, CEL, readiness, and duration
Request is FailedInspect .status.failure, correct the reported execution problem, then issue rp retry or rp expire
Request deletion is deniedCancel only Created/Requested/Pending; otherwise expire and wait for retention
Managed object cannot be changedCheck whether its resource group uses protect: true
Template rejected at spec.resources[i].policy.conditionCorrect CEL syntax, undeclared variables, or a non-boolean expression such as 42
Preflight fails with ResourceDryRunFailed and ConditionEvaluationFailedGuard missing fields and check timestamp formats; preflight has not persisted targets
Activation failsInspect failure and processed-item status; other independent targets may already have been applied
Conditional target read is forbiddenGrant the execution ServiceAccount get on that target, even for a false expression
Namespace read is forbidden during cleanupRetain get on the target Namespace until cleanup finishes
Permit is Active but targets were skippedA 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:

MetricLabelsMeaning
capsule_resourcepermit_phasename, target_namespace, statusValue 1 for the permit’s current lifecycle phase; status contains the phase name
capsule_resourcepermittemplate_conditionname, target_namespace, conditionTemplate readiness; condition="Ready" is 1 when true and 0 otherwise
capsule_globalresourcepermittemplate_conditionname, conditionGlobal 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