This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

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:

KindShort nameScopePurpose
ResourcePermitrpNamespaceA user’s request, parameters, reason, schedule, review, and rendered-resource status
ResourcePermitTemplaterptNamespaceA template available to requests in the same namespace
GlobalResourcePermitTemplategrptClusterA 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.

BehaviorResourcePermitTemplateGlobalResourcePermitTemplate
ScopeNamespacedCluster
Request locationSame namespaceAny allowed namespace
ServiceAccount referenceName only, in the template namespaceName and namespace
Namespace selectors/statusNot applicableSupported
Ready condition and observed generationSupportedSupported

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.

Define inputs and rendered resources

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:

DataDirect targetGenerated template
Request parameters.name$.params.name
Loaded contextindex .crdInfo 0index $.context.resources.crdInfo 0
Trusted request data.request.username$.request.username

The trusted request object contains:

FieldMeaning
nameResourcePermit name
usernameAuthenticated requester
groupsAuthenticated groups as a string array
timestampStable 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.

TemplateExplicit identityConfiguration fallbackFinal fallback
Localspec.impersonation.name in the template namespacetenantDefaultServiceAccount in that namespaceCapsule controller ServiceAccount
Globalspec.impersonation.name and .namespaceglobalDefaultServiceAccount in globalDefaultServiceAccountNamespaceCapsule 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.

FieldDecisionInputs
spec.approvals.conditions[]Approval eligibility; conditions are ORedrequest, requester, reviewer
spec.resources[].policy.conditionWhether to apply one target’s rendered contentobject, 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:

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