Rules
Configure policies and restrictions on a per-Namespace basis with Rules
Namespace rules let administrators configure behavior for resources in Tenant
namespaces. Use namespace selectors to give namespaces within the same Tenant
different profiles. Rules can set workload properties during admission and
validate the resulting resources.
Rules cover two areas:
Mutate: apply typed workload settings to new Pods in selected namespaces.
Enforcement: control allowed workloads, ingress hostnames, service types, NetworkPolicy egress CIDRs, and namespace metadata.
See Conditions for conditional mutation and enforcement.
Order and scope
Capsule first mutates the admission object, then enforces policies against
the result. Both phases use the rules selected for the namespace and requesting
subject.
A Tenant rule’s namespaceSelector selects its namespaces; omitting it selects
all namespaces in that Tenant. Matching Tenant rules retain their declaration
order in the namespace’s effective RuleStatus, including rules containing only
mutate. Tenant/Namespace templating and audience filtering apply
to both mutation and enforcement.
Admission order
- Mutate. Apply applicable
metadata mutations, then
resource request/limit mutations,
then
mutate entries in list order within the effective rule order. Each
entry and its conditions see the object after preceding mutations. When
entries change the same property, its merge or replacement behavior determines
the result. - Enforce. Evaluate applicable policies against the resulting admission
object, including mutated values and values injected by Kubernetes. A mutation
must satisfy the applicable enforcement policies; an incompatible combination
rejects the request.
Mutation runs across the applicable rules before enforcement. Capsule does not
alternate mutation and enforcement for each rule. mutate and enforce are
independent sibling blocks; enforce.action does not select or disable mutations.
Some settings under enforce, such as managed metadata and resource defaults,
produce mutations in the first phase. Their enforce.conditions are checked
before those changes and evaluated again during enforcement. See
Enforcement conditions for which object each phase
uses.
Resource scope
mutate[].workloads applies when a Pod is created, including Pods created by
Deployments, StatefulSets, and other controllers.
security.readOnlyRootFilesystem
and registries.imagePullPolicy
also apply to newly added ephemeral containers on UPDATE pods/ephemeralcontainers; all other workload mutation properties are skipped
on that subresource. Controller templates and existing containers are not
rewritten. Other Pod updates, deletes, and subresources do not run workload
mutations. Rule or namespace-label changes affect new Pods and new ephemeral
containers within the property’s supported scope.
Each rule supports up to 64 mutation entries. Placement mutations do not
accumulate duplicate entries when admission is reinvoked.
Enforcement runs for the resource types and operations supported by each policy.
enforce.workloads.targets
selects native workload kinds and policy locations. Targets-only entries apply
the action to the kind; entries with workload policies scope those checks.
Controller targets explicitly enable template validation and resource
request/limit mutation on controller creation and updates. Omitted targets
preserve Pod-only defaults. Enforcement targets do not control
mutate[].workloads.targets, whose separate
selection rules include all container
groups when pod is selected. Conditions
can narrow a policy’s scope but cannot expand its supported operations.
Audience
Use audience to restrict a rule to requests made by specific users, groups,
service accounts, or Capsule-defined subject categories. The property belongs to
the root of a rule, alongside mutate and enforce:
spec:
rules:
- audience:
- kind: Group
name: system:authenticated
enforce:
action: allow
metadata:
- kinds:
- ConfigMap
annotations:
example.corp/cost-center:
required: true
values:
- exp: "^INV-[0-9]{4}$"
When audience is omitted or empty, the rule applies to every request selected
by the rule.
When an audience is configured, the rule applies if the requesting subject
matches at least one entry. In other words, entries are combined with logical
OR semantics. Audience matching is performed consistently for both validation
and mutation, so a subject excluded from a rule is neither validated nor mutated
by that rule.
Audience filtering uses the actual identity on the Pod admission request. Pods
created by a workload controller are normally submitted by that controller’s
service account, not by the user who originally created the Deployment or Job.
Account for that distinction when combining audience with mutation or enforcement.
The supported standard audience kinds are User, Group, and
ServiceAccount:
spec:
rules:
- audience:
# Match one exact Kubernetes username.
- kind: User
name: alice@example.com
# Match any request carrying this group.
- kind: Group
name: oidc:engineering
# Match one Kubernetes service account.
- kind: ServiceAccount
name: system:serviceaccount:delivery:deployer
enforce:
action: deny
metadata:
- apiGroups:
- v1
kinds:
- Namespace
labels:
pod-security.kubernetes.io/enforce:
managed: restricted
For User, Group, and ServiceAccount, name is compared with the identity
information provided in the Kubernetes admission request. A service account is
represented by its canonical Kubernetes username:
system:serviceaccount:<namespace>:<service-account-name>
For example, a request from the deployer service account in the delivery
namespace has the username
system:serviceaccount:delivery:deployer.
Custom
The Custom kind exposes audiences based on Capsule’s internal identity and
tenant resolution. Its name must be one of the supported values below.
| name | description |
|---|
CapsuleUser | Matches subjects listed by configuration.Users(). |
Administrator | Matches subjects listed by configuration.Administrators(). |
TenantOwner | Matches an owner of the tenant resolved for the current request. A request cannot match when no tenant can be resolved. |
Controller | Matches the service account used by the Capsule controller. |
Custom audiences can be combined with standard audiences. The following rule
applies to Capsule users, Capsule administrators, tenant owners, the Capsule
controller, or members of the Kubernetes system:masters group:
spec:
rules:
- audience:
- kind: Custom
name: CapsuleUser
- kind: Custom
name: Administrator
- kind: Custom
name: TenantOwner
- kind: Custom
name: Controller
- kind: Group
name: system:masters
enforce:
action: allow
metadata:
- apiGroups:
- v1
kinds:
- Namespace
annotations:
example.corp/cost-center:
default: II-1
TenantOwner is request-scoped. Capsule first resolves the tenant associated
with the admission request and then checks the requesting subject against that
tenant’s owners. This makes it suitable for rules that should affect tenant
owners but not unrelated Capsule users:
spec:
rules:
- audience:
- kind: Custom
name: TenantOwner
enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
owner-managed:
default: "true"
Controller specifically identifies the Capsule controller service account. It
is useful when internal reconciliation requests need different policy behavior
from requests made by ordinary users:
spec:
rules:
- audience:
- kind: Custom
name: Controller
enforce:
action: allow
metadata:
- kinds:
- Secret
labels:
capsule.clastix.io/reconciled:
managed: "true"
Unknown audience kinds and unsupported Custom names are rejected when the
rule is admitted. This catches spelling mistakes and prevents a rule from being
silently configured with an audience that can never match.
Match expressions
Rule fields that accept match expressions use a common structure. A matcher must define at least one of exact or exp. Both fields may be set together; in that case, the matcher succeeds when either the exact list or the regular expression matches.
exact:
- value-a
- value-b
exp: "value-[0-9]+"
| Field | Description |
|---|
exact | Up to 64 exact values per matcher. The matcher succeeds when the evaluated value equals one of the listed values. |
exp | A Go regular expression of up to 4,096 Unicode characters, matched against the evaluated value. |
negate | Negates the final match result. This applies to both exact and exp. |
These limits apply wherever match expressions are used, including placement and localhost profile matchers. Policies exceeding either limit are rejected when saved. Both limits still apply when exact and exp are used together or negate is enabled. Existing policies exceeding these limits must be reduced before they can be updated.
Anchor regular expressions with ^ and $ to match the whole string.
For example, this matcher matches registry.local/team-a/app:1.0.0, registry.local/team-b/app:1.0.0, or any reference under registry.local/shared/*:
exact:
- registry.local/team-a/app:1.0.0
- registry.local/team-b/app:1.0.0
exp: "registry.local/shared/.*"
With negate: true, the final match result is inverted. This means negation applies to exact values as well as regular expressions:
exact:
- registry.local/blocked/app:1.0.0
exp: "registry.local/deprecated/.*"
negate: true
This matcher succeeds for every value except registry.local/blocked/app:1.0.0 and values matching registry.local/deprecated/.*.
Conditions
Conditions determine whether a mutation entry or enforcement rule applies to
the current admission request. They are optional and belong alongside action
at mutate[].conditions or enforce.conditions.
| Location | Scope |
|---|
mutate[].conditions | The entire mutation entry, within the property’s supported targets and operations. |
enforce.conditions | The entire enforce block: workloads, Services, metadata, ingress, and storage, including metadata and resource request/limit mutations. |
A false mutation condition skips only its mutation entry. A false enforcement
condition skips that enforce block, while sibling mutate entries and other
rules continue. Namespace selection and audience filtering apply first.
Conditions are evaluated only when the entry or enforcement policy applies to
the resource and operation. A workload-only rule does not evaluate its conditions
for a Service request. If the same enforce block also contains Service policies,
those policies use the same conditions. Use separate rules when resource types
need different conditions.
Expressions
Each entry has an expression containing one Boolean CEL expression and an
optional name used in error messages. Names must be unique within a mutation
entry or enforce block and use a DNS label of up to 63 characters. Each entry
or block supports up to 64 conditions; each expression can contain up to 4096
characters and has a bounded evaluation cost. Expressions are compiled when
rules are validated and cached for reuse.
| Variable | Meaning |
|---|
object | The current resource, using its Kubernetes field structure. Metadata conditions can inspect the full object, including fields such as spec or ConfigMap data. |
request | Admission metadata, such as operation, namespace, name, userInfo, kind, resource, subResource, and dryRun. Raw objects and admission options are not exposed here. |
volume | The referenced PersistentVolume during additional volume access evaluation; null for other condition consumers. The snapshot is read-only. |
Check absent fields with has(...) and map membership with in. The
missing-value example demonstrates both.
The Pod node selector is at object.spec.nodeSelector; it is not a container
property. Use request.userInfo for request identity, while keeping existing
audience settings for straightforward subject selection.
Evaluation
- No conditions means the mutation entry or
enforce block applies within its supported scope. - All conditions must return true for it to apply. Use
|| within an expression for alternatives. - Any false condition skips the entry or block, including when another condition produces an error.
- If none is false, an evaluation error rejects admission and identifies the condition.
- Invalid syntax and non-Boolean expressions are rejected when saving the rule. Templated expressions are checked after rendering into the namespace’s RuleStatus.
Mutation conditions
Without conditions, a workload mutation applies to every Pod selected by the
rule. Add conditions to the mutation entry, alongside action and workloads,
to apply its changes only when all Boolean CEL expressions return true. A false
condition skips that entry; later entries and other rules continue. Conditions
on the sibling enforce block do not control these mutations.
Conditions see the current Pod immediately before the mutation entry runs,
including changes from preceding mutations. They do not see changes from their
own entry. Conditions run on Pod creation and, for applicable
security.readOnlyRootFilesystem and registries.imagePullPolicy mutations, when adding
ephemeral containers. They cannot
expand a property’s supported targets or operations. They select mutations; mutation values are supplied by the
workload properties.
Set a value only when it is missing
merge can overwrite an existing map value. To use a setting as a default,
check whether that specific key is absent:
mutate:
- action: merge
conditions:
- name: missing-os-selector
expression: >-
!has(object.spec.nodeSelector) || !('kubernetes.io/os' in object.spec.nodeSelector)
workloads:
placement:
nodeSelector:
kubernetes.io/os: linux
A Pod with kubernetes.io/os: windows keeps its value. A Pod with only another
selector key receives the Linux selector and retains the other key. If an
earlier mutation already set the OS selector, this entry is skipped.
A condition gates every property in its mutation entry. For example, adding
placement.tolerations beside placement.nodeSelector above would make those tolerations depend on
the OS selector being absent too. Put unconditional changes or changes with
different conditions in separate entries.
For a field Kubernetes fills before admission, see
default a scheduler with a condition.
That example uses conditional replace to override default-scheduler while
preserving custom scheduler names.
Enforcement conditions
enforce.conditions applies to every applicable policy in the enforcement
block. For ingress policies, the conditions are evaluated only when the incoming
resource kind is selected by ingress.types. Metadata policies can inspect the
full resource, including its spec or data.
For storage volume rules, object is the
incoming PVC and volume is the referenced PV. These conditions govern
additional access to PVs without a Tenant label; they do not revoke existing
Tenant-owned volume access. Keep volume-specific conditions in their own
storage block because other condition consumers see volume as null.
Conditions under enforce also gate metadata defaults, managed metadata, and
resource request/limit mutations.
On Pod creation, applicable enforcement conditions are evaluated against the
same object before metadata and resource mutations run. Capsule then applies
metadata mutations, resource policies, and the ordered mutate entries.
During enforcement, conditions are evaluated again against the resulting
admission object. A condition based on a mutated field can therefore have a
different result in the two phases.
On Pod updates, conditional placement enforcement is reevaluated even when only
labels or another condition input changes. Typed workload mutations apply on
creation, with the additional security.readOnlyRootFilesystem and
registries.imagePullPolicy scope for new ephemeral containers. Conditions do not expand a policy’s supported operations or
subresources.
Managed metadata sets labels or
annotations during admission, overwriting existing values. For rules without
conditions, the RuleStatus controller also reconciles these values onto
existing objects.
When enforce.conditions is nonempty, managed metadata is applied only on
admission requests that satisfy those conditions. The RuleStatus controller
skips background reconciliation for that rule, even if its expression is true.
Keep managed metadata in an unconditional rule when existing objects must be
kept in sync.
Independent resource conditions
Split resource policies into separate rules when they need independent
conditions. Repeat any namespace selector or audience settings that should
apply to both rules:
rules:
- enforce:
action: deny
conditions:
- name: restricted-pods
expression: |
has(object.metadata.labels) &&
'example.com/restricted' in object.metadata.labels &&
object.metadata.labels['example.com/restricted'] == 'true'
workloads:
placement:
nodeSelector:
- key: {exact: [infrastructure.example.com/pool]}
values: {exact: [dedicated]}
- enforce:
action: deny
conditions:
- name: external-traffic
expression: object.spec.type == 'NodePort'
services:
types: [NodePort]
The first rule denies the selected node-selector entry on matching Pods. The
second denies NodePort Services independently. The Service-specific expression
is evaluated only for Service requests because its rule has only Service
policies. When sharing an enforce block across resource types, write conditions
that handle every selected type, using has(...) or request.kind as needed.
Skipping an enforce block removes it from the ordered allow/deny/audit
evaluation, including any allow-list requirement it would otherwise introduce.
Find the failing condition
A mutation condition error includes its location and optional condition name:
rules[0].mutate[1]: conditions[0] ("shared-pool"): ...
Indices are zero-based within the effective rules and mutation entries evaluated
for this request. Inspect the namespace’s effective RuleStatus and its audience
selection when tracing a rendered rule back to its source. A mutation condition
gates every property in that entry.
Mutation errors identify the affected property where applicable. For example,
an affinity merge that exceeds the term limit identifies
affinity: nodeAffinity.requiredDuringSchedulingIgnoredDuringExecution together
with the rule and mutation indices. Enforcement validation condition errors
identify the enforce block, rule index, and condition:
enforce: enforcement rule[0]: conditions[0] ("restricted-pods"): ...
Condition errors during metadata or resource mutation instead identify
rules[0].enforce before the condition index and name.
1 - Mutate
Set resource properties during admission with typed mutations
Use spec.rules[].mutate to set resource properties during admission. It is an
ordered list of entries, each with an optional action and a typed resource
block. Currently, workloads applies to Pods on creation;
security.readOnlyRootFilesystem
and registries.imagePullPolicy
also apply to newly added ephemeral containers.
Mutation targets select compatible Pod
locations, with pod including all container groups.
mutate and enforce are sibling keys. Mutation
changes the resource; enforcement validates the result. Either can be used alone,
and mutated values remain subject to applicable enforcement policies.
See Order and scope for the admission sequence
and Conditions for conditional mutations.
Action
Each mutate entry supports an action field:
| Action | Behavior |
|---|
merge (default) | Apply supplied values while retaining unrelated settings. Map keys are set, list entries are added or updated, and affinity restrictions are combined according to the property’s rules. |
replace | Replace each supplied property in full. Other properties remain unchanged. |
The action applies to every property in that entry. Use separate entries when
properties need different actions:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: merge
workloads:
placement:
nodeSelector:
kubernetes.io/os: linux
- action: replace
workloads:
placement:
tolerations:
- key: infrastructure.example.com/dedicated
operator: Equal
value: shared
effect: NoSchedule
This retains other node-selector keys, but replaces the entire tolerations list.
Other admission controllers can subsequently add tolerations.
Replacement happens at the supplied property’s boundary. For example,
placement.nodeSelector is replaced as one map and placement.affinity as one object, including
all its branches. The placement reference
explains which list entries match during a merge and how required affinity is
combined. Scalar settings such as security.hostUsers, security.readOnlyRootFilesystem, and
registries.imagePullPolicy
overwrite selected values with either action.
Omitted and empty values
| Value in the mutation | Effect |
|---|
Property omitted or null | Leave the existing property unchanged with either action. |
Empty placement.nodeSelector, placement.tolerations, placement.topologySpreadConstraints, placement.affinity, or registries.imagePullSecrets with replace | Clear that property. |
Empty placement: {}, security: {}, or registries: {} | Supply no changes; another mutation property must be supplied. |
security.hostUsers: false or security.readOnlyRootFilesystem: false | Set an explicit Boolean value; false is not an omission. |
For example, this clears node selectors and affinity while retaining tolerations
and topology-spread constraints:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: replace
workloads:
placement:
nodeSelector: {}
affinity: {}
1.1 - Workloads
Configure Pod placement, security, and image pull policies with ordered mutations
Workload mutations set Pod properties under spec.rules[].mutate[].workloads
using native Kubernetes Pod syntax. Use placement settings to
select schedulers, assign node pools, add tolerations, spread workloads across topology domains,
and configure affinity. Use security settings to configure Pod user
namespaces, seccomp/AppArmor profiles, and read-only container root filesystems.
Use registry settings to configure container image pull policies
and Pod image pull secret references. Placement enforcement
validates the resulting placement settings using matchers.
This page covers configuration and mutation behavior. The
Reference provides a complete Tenant combining all workload
mutation properties with placement, security-profile, and image pull policy enforcement.
Each mutation entry has an optional action and a
workloads block containing at least one property to set. Scheduling fields live
under workloads.placement; security fields live under workloads.security.
targets stays directly under workloads and applies to all configured groups.
The action applies to each supplied field inside a group: replacing
placement.scheduler preserves an omitted placement.nodeSelector and all
omitted security fields. Empty placement: {} or security: {} groups supply
no mutations; use an explicit empty map or list on a supported field to clear it. For conditional
mutations, see Conditions.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
environment: production
mutate:
- action: merge
workloads:
placement:
nodeSelector:
kubernetes.io/os: linux
enforce:
action: allow
workloads:
placement:
nodeSelector:
- key:
exact: [kubernetes.io/os]
values:
exact: [linux]
In selected namespaces, this rule sets the Linux node selector. The allow rule
then rejects any additional node-selector entries. mutate and enforce are
sibling keys and can be used independently. enforce.action does not select or
disable mutations.
Workload mutations apply on Pod creation. security.readOnlyRootFilesystem and
registries.imagePullPolicy also apply to newly added ephemeral containers. See
Order and scope for the mutate-then-enforce
sequence, rule selection, and supported operations.
Targets
Set mutate[].workloads.targets to select where the entry applies. Omitting
targets, using targets: [], or selecting pod includes all compatible
locations. For mutation, pod includes every regular, init, and ephemeral
container. Native sidecars belong to the init-container group.
The table lists supported mutation targets. ✅ indicates mutation support;
❌ means that property cannot be mutated through that target. Controller and
volume targets are supported by enforcement
but are rejected by the mutation API.
Pods created by controllers are still mutated
Every new Pod is evaluated against the applicable Pod mutation rules, whether
created directly or by a Deployment, StatefulSet, DaemonSet, Job, CronJob, or
another controller. Stored controller templates remain unchanged.
The resulting Pods still receive applicable mutations when they are created.
Namespace selection, mutation targets, conditions, and audience
still determine which mutations apply. Audience matching uses the identity
creating the Pod, usually the controller’s service account.
Placement includes placement.scheduler, placement.nodeSelector, placement.tolerations,
placement.topologySpreadConstraints, and placement.affinity. These properties, security.hostUsers,
security.seccompProfile, security.appArmorProfile, and registries.imagePullSecrets require
pod or omitted/empty targets.
Seccomp and AppArmor mutation preserves explicit container profiles with both
merge and replace; container-specific profile mutation is not supported.
security.readOnlyRootFilesystem and registries.imagePullPolicy support container-specific
mutation targets. Use separate mutation entries to combine Pod-level settings with a
narrower container selection. Controller and volume targets are not supported
for typed workload mutations.
Targets combine by inclusion: listing pod alongside a narrower target still
selects all groups. A targets-only mutation is invalid because it has no property
to set. Enforcement targets
are configured independently and have property-specific matching semantics.
Regular and init containers are mutated on Pod creation. Ephemeral containers
are mutated only when newly added through UPDATE pods/ephemeralcontainers.
An existing ephemeral container is left unchanged, even if the rule changed
since it was added. Other Pod updates, deletes, and subresources do not apply
these mutations. On an ephemeral update, the entry’s conditions see the full
current Pod, including earlier mutations, but only the new ephemeral containers'
securityContext.readOnlyRootFilesystem and imagePullPolicy values can change.
Placement
Scheduler
placement.scheduler sets the Pod’s spec.schedulerName. Use the name of a scheduler
configured in your cluster; Capsule selects the scheduler but does not deploy it.
See Kubernetes multiple schedulers.
To select a scheduler for every new Pod covered by the rule, use replace:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: replace
workloads:
placement:
scheduler: tenant-scheduler
| Action | Behavior |
|---|
merge | Fill an empty spec.schedulerName. Preserve every non-empty value, including default-scheduler. |
replace | Set spec.schedulerName to the configured value when the entry’s conditions match. |
Omitting placement.scheduler or setting it to null leaves the current value unchanged.
An empty or blank configured name is invalid. To select the built-in scheduler,
set scheduler: default-scheduler explicitly.
Default a scheduler with a condition
Kubernetes fills an omitted or empty spec.schedulerName with default-scheduler
before Capsule receives the admission request. To use a tenant scheduler as the
default while preserving custom scheduler names, combine replace with a
mutation condition:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: replace
conditions:
- name: default-scheduler
expression: >-
!has(object.spec.schedulerName) || object.spec.schedulerName in ['', 'default-scheduler']
workloads:
placement:
scheduler: tenant-scheduler
| Scheduler in the submitted Pod | Result with this condition |
|---|
| Omitted or empty | tenant-scheduler |
default-scheduler | tenant-scheduler |
batch-scheduler | batch-scheduler |
The condition cannot distinguish an omitted value from an explicitly selected
default-scheduler: both reach admission with the built-in name. This rule
deliberately replaces both. A custom scheduler supplied by the user or an earlier
mutation makes the condition false and is retained.
Keep this scheduler default in its own mutation entry. Conditions gate every
property in an entry, so adding node selectors or tolerations to it would make
those changes depend on the scheduler condition too. Namespace selection and
audience still determine which rules apply. Only new Pods are changed; existing
Pods and controller templates retain their settings.
The final scheduler name remains subject to
scheduler enforcement.
Allow the configured default in any applicable scheduler allow-list, or admission
will reject the mutated Pod.
Node selectors
placement.nodeSelector is a map of node-label keys and values. Every entry must match a
node for the Pod to be scheduled there.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: merge
workloads:
placement:
nodeSelector:
infrastructure.example.com/pool: shared
| Action | Behavior |
|---|
merge | Set the supplied keys, overwriting their existing values. Retain other keys. |
replace | Replace the entire map. Use nodeSelector: {} to clear it. |
For a Pod whose selector is
{infrastructure.example.com/pool: private, disk: ssd}, the example produces:
| Action | Resulting node selector |
|---|
merge | {infrastructure.example.com/pool: shared, disk: ssd} |
replace | {infrastructure.example.com/pool: shared} |
Later mutations can overwrite the same key. For defaults, see
set a value only when it is missing.
Use node-selector enforcement
to restrict which keys and values remain on the admitted Pod.
Tolerations
placement.tolerations is a list of native Pod tolerations. A toleration permits scheduling
onto a node with a matching taint; it does not require that node. Use a node
selector or required node affinity to restrict placement to a node pool.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: merge
workloads:
placement:
tolerations:
- key: infrastructure.example.com/dedicated
operator: Equal
value: shared
effect: NoExecute
tolerationSeconds: 60
| Action | Behavior |
|---|
merge | Add or update the toleration identified by key + operator + value + effect. An omitted operator means Equal. |
replace | Replace the entire list, including tolerations with other identities. Use tolerations: [] to clear it. |
The duration is not part of the merge identity. A matching toleration receives
the supplied tolerationSeconds. Omitting the duration removes an existing
limit and makes the toleration unlimited.
| Existing toleration | Supplied toleration | Result with merge |
|---|
pool=private, Equal, NoSchedule | pool=shared, Equal, NoSchedule | Both remain because their values differ. |
pool=shared, Equal, NoExecute, 30 seconds | Same identity, 60 seconds | One toleration with a 60-second duration. |
pool=shared, Equal, NoExecute, 30 seconds | Same identity, no duration | One toleration with unlimited duration. |
Changing a key, operator, value, or effect adds another toleration under merge.
Use replace when only the supplied list should remain. Other admission
controllers may subsequently inject additional tolerations. All final entries
are subject to toleration enforcement,
including Kubernetes-injected entries.
Topology spread constraints
placement.topologySpreadConstraints is a list of native Pod spread constraints. Each
constraint specifies a topology domain and the Pods to count when spreading.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: merge
workloads:
placement:
topologySpreadConstraints:
- topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
maxSkew: 1
labelSelector:
matchLabels:
app: checkout
This expresses a preference to spread Pods labeled app: checkout across hosts.
Use DoNotSchedule when the spread constraint must prevent scheduling that
would violate it.
| Action | Behavior |
|---|
merge | Add or replace the entire constraint identified by topologyKey + whenUnsatisfiable. Retain constraints with other identities. |
replace | Replace the entire list. Use topologySpreadConstraints: [] to clear it. |
If the Pod already has a kubernetes.io/hostname / ScheduleAnyway constraint
with maxSkew: 3, the example replaces it with maxSkew: 1 and the supplied
selector. Old selector fields and optional fields are not carried forward.
An existing topology.kubernetes.io/zone / DoNotSchedule constraint remains with
merge and is removed with replace unless it is also supplied.
Changing whenUnsatisfiable creates a different merge identity, even if the
topology key is unchanged. For allowed topology keys, skew ranges, and selector
operators, see spread enforcement.
Affinity
placement.affinity uses the native Pod structure with three branches: nodeAffinity,
podAffinity, and podAntiAffinity. Each can contain required and preferred
terms.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: merge
workloads:
placement:
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [zone-a, zone-b]
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: cache
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: checkout
This requires a node in zone-a or zone-b, prefers a host with cache Pods, and
prefers to separate checkout Pods across hosts. The referenced node labels must
exist on the intended nodes. Omitting namespace selection in these Pod-affinity
terms limits their Pod matching to the incoming Pod’s namespace.
Merge
| Affinity term | Behavior with merge |
|---|
| Required node affinity | Require both the existing restrictions and the supplied restrictions. Combine existing and supplied alternatives to preserve that AND relationship. |
| Required Pod affinity or anti-affinity | Add distinct complete terms. All required terms remain required; identical terms are retained once. |
| Preferred affinity of any type | Add distinct complete terms or replace the weight of a matching term. |
For example, existing node alternatives (disk=ssd OR arch=arm64) combined with
supplied zone=a become (disk=ssd AND zone=a) OR (arch=arm64 AND zone=a).
Required restrictions accumulate across applicable mutations. Capsule rejects
combinations that would produce more than 256 node-selector alternatives.
An empty node-selector term matches no nodes.
Preferred-term identity includes the complete node preference or Pod-affinity
term, excluding its weight. Requirement and selector-value order are normalized
for matching. Supplying the same term with weight 80 updates an existing weight
50; changing the selector adds a distinct preferred term.
Replace
replace replaces all of placement.affinity, including branches omitted from the
mutation. Supplying only nodeAffinity also removes existing podAffinity and
podAntiAffinity. Use affinity: {} to clear all three branches.
For allowed types, required/preferred modes, selector requirements, and namespace
scope, see affinity enforcement.
Its matcher syntax uses one flat list across all three affinity types.
Security
Seccomp and AppArmor mutations set only the Pod-level profile on creation.
Use targets: [pod] or omit mutation targets for these Pod-level properties.
enforce.workloads.targets selects enforcement locations independently. Both merge and replace
preserve explicit regular, init, and ephemeral container profiles. Containers
without an override inherit the Pod default. See the
Pod targets table and
profile target example
for how enforcement checks those defaults and overrides.
Read-only root filesystem
security.readOnlyRootFilesystem sets the Boolean on every selected container’s
securityContext. Like security.hostUsers, both merge and replace set the supplied
value, including an explicit false. Omitted or null leaves the value
unchanged. Later matching entries can overwrite earlier values; all other
container security-context fields are preserved.
This full Tenant makes all container root filesystems read-only in namespaces
labeled filesystem-profile: readonly. Namespaces labeled
filesystem-profile: init-only apply the setting only to init containers,
including native sidecars. Other namespaces keep their submitted values.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
filesystem-profile: readonly
mutate:
- action: merge
workloads:
targets: [pod]
security:
readOnlyRootFilesystem: true
- namespaceSelector:
matchLabels:
filesystem-profile: init-only
mutate:
- action: replace
workloads:
targets: [pod/initcontainers]
security:
readOnlyRootFilesystem: true
In the first profile, pod also selects new ephemeral containers. Choose
pod/containers for regular containers only or pod/ephemeralcontainers for
new debug containers only. Neither the rule nor a namespace-label change
rewrites existing containers. Pods declaring spec.os.name: windows are skipped.
A read-only root filesystem does not make mounted volumes read-only. Applications
that write to paths such as /tmp need suitable writable volume mounts. See the
Kubernetes security-context documentation.
This field is a mutation setting; there is no corresponding
enforce.workloads.security.readOnlyRootFilesystem matcher.
Host user namespace
security.hostUsers selects whether the Pod uses the host user namespace.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- mutate:
- action: merge
workloads:
security:
hostUsers: false
| Supplied value | Behavior with merge or replace |
|---|
false | Request a separate user namespace for the Pod. |
true | Use the host user namespace. |
Both actions overwrite an existing Boolean. A later entry can change false
to true or the reverse. This property is independent of a container’s
runAsUser and has no corresponding enforce.workloads.security.hostUsers matcher.
The Kubernetes version, operating system, and container runtime must support
the requested setting. Kubernetes validates incompatible Pod settings; Capsule
does not adjust other security or host-namespace fields. See
Kubernetes user namespaces.
Seccomp
security.seccompProfile sets spec.securityContext.seccompProfile on new Linux Pods.
Containers inherit this Pod default unless their own security context supplies
a seccomp profile. This includes regular, init, and ephemeral containers.
| Property | Meaning |
|---|
type: RuntimeDefault | Use the container runtime’s default seccomp profile. Omit localhostProfile. |
type: Localhost | Use a seccomp profile file on the node. Set localhostProfile to its relative path. |
type: Unconfined | Disable seccomp filtering. Omit localhostProfile. |
Seccomp mutation behavior
| Action | Behavior |
|---|
merge | Set the configured profile only when the Pod has no spec.securityContext.seccompProfile. Preserve any supplied profile, including its localhost path. |
replace | Replace the complete Pod profile with the configured type and localhost path. |
| Property omitted | Preserve the Pod’s seccomp profile. |
Merge treats the profile as one property: it does not fill a missing
localhostProfile inside an already supplied profile. A Localhost profile must
include its path. Replacing Localhost with RuntimeDefault removes the old path.
Other security-context fields and explicit container profiles are preserved.
Use seccomp enforcement to prevent
container overrides from bypassing the intended profile. Mutations use the
existing conditions and order, run only on
Pod creation, and skip Pods declaring spec.os.name: windows. They do not
change running Pods or controller templates.
Default to RuntimeDefault
This complete Tenant defaults seccomp in namespaces labeled
security-profile: confined. The allow rule requires every container’s
effective profile to be RuntimeDefault, including containers added later
through pods/ephemeralcontainers.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: confined
mutate:
- action: merge
workloads:
security:
seccompProfile:
type: RuntimeDefault
enforce:
action: allow
workloads:
security:
seccompProfiles:
- types: [RuntimeDefault]
An explicit container-level Unconfined profile remains unchanged by mutation
and is rejected by enforcement. Namespaces without the selector label do not
receive either rule effect.
Set a local seccomp path
Set type: Localhost together with localhostProfile. The path is relative to
the kubelet’s seccomp directory and must not be absolute or contain .. path
segments. For example, profiles/solar.json refers to
/var/lib/kubelet/seccomp/profiles/solar.json when the kubelet uses its default
root directory. It is a file on the node, independent of the container’s
filesystem. See the Kubernetes seccomp tutorial.
This Tenant replaces the Pod default with that local profile and allows only
that profile for the effective container checks:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: local-seccomp
mutate:
- action: replace
workloads:
security:
seccompProfile:
type: Localhost
localhostProfile: profiles/solar.json
enforce:
action: allow
workloads:
security:
seccompProfiles:
- types: [Localhost]
localhostProfiles:
- exact: [profiles/solar.json]
Use action: merge instead to supply this profile only when the Pod profile
is absent. With the allow-list above, an existing different profile is then
preserved by merge and rejected by enforcement.
Install the file on every eligible node before using the rule, or combine the
rule with placement that selects prepared nodes. Capsule writes
the profile reference; it does not distribute or inspect the file. A Pod can
pass admission and still fail to start if the profile is unavailable on its node.
AppArmor
security.appArmorProfile sets spec.securityContext.appArmorProfile on new Linux Pods.
The Pod value supplies the default for containers that have no AppArmor
override. Use this mutation only for workloads placed on nodes with AppArmor
support. Explicit RuntimeDefault can prevent a Pod from starting on a node
without that support. See the AppArmor prerequisites.
| Property | Meaning |
|---|
type: RuntimeDefault | Use the container runtime’s default AppArmor profile. Omit localhostProfile. |
type: Localhost | Use an AppArmor profile already loaded on the node. Set localhostProfile to its loaded name. |
type: Unconfined | Disable AppArmor confinement. Omit localhostProfile. |
AppArmor mutation behavior
| Action | Behavior |
|---|
merge | Set the configured profile only when the Pod has no spec.securityContext.appArmorProfile. Preserve any supplied profile, including its localhost name. |
replace | Replace the complete Pod profile with the configured type and localhost name. |
| Property omitted | Preserve the Pod’s AppArmor profile. |
Merge does not complete a partially supplied Localhost profile. Replacing a
Localhost profile with RuntimeDefault removes the old localhostProfile.
Other security-context fields and container overrides, including legacy
AppArmor annotations, remain unchanged. Pair mutation with
AppArmor enforcement to validate
these effective container choices.
The existing conditions and order apply.
Mutations run only on Pod creation and skip Pods declaring
spec.os.name: windows; they do not change running Pods or controller templates.
Default the AppArmor profile
This Tenant supplies RuntimeDefault and requires it for every effective
container profile in the selected namespaces:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: apparmor
mutate:
- action: merge
workloads:
security:
appArmorProfile:
type: RuntimeDefault
enforce:
action: allow
workloads:
security:
appArmorProfiles:
- types: [RuntimeDefault]
Set a local AppArmor profile
For AppArmor, localhostProfile is the loaded profile name, such as
solar-confined. It is not a filesystem path to the profile definition.
The name must match the profile loaded on the node. See
Specifying AppArmor confinement.
This Tenant replaces the Pod default with that name and restricts effective
container profiles to the same choice:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: local-apparmor
mutate:
- action: replace
workloads:
security:
appArmorProfile:
type: Localhost
localhostProfile: solar-confined
enforce:
action: allow
workloads:
security:
appArmorProfiles:
- types: [Localhost]
localhostProfiles:
- exact: [solar-confined]
Use action: merge to preserve an existing Pod profile instead of replacing
it; the enforcement rule still rejects any different effective profile.
Load solar-confined on every eligible node before applying the rule, or
restrict placement to prepared nodes. Capsule does not load AppArmor profiles
or verify that their definitions are consistent across nodes.
Registries
Configure registry-related mutations under
mutate[].workloads.registries. Set imagePullPolicy for selected containers
and imagePullSecrets for the Pod. The targets table shows which
locations support each property. Both settings apply to Linux and Windows Pods.
Image pull policy
Set registries.imagePullPolicy to one of the native Kubernetes values:
| Value | Container image behavior |
|---|
Always | Resolve the image through the registry at each container start; cached image layers can still be reused. |
IfNotPresent | Pull the image when it is not already available on the node. |
Never | Use an image already available on the node; the container cannot start if it is missing. |
See Kubernetes image pull policies
for runtime behavior. Capsule changes the container’s policy; it does not pull
images or create registry credentials.
| Mutation configuration | Behavior |
|---|
action: merge or omitted action | Overwrite the selected containers’ pull policies. |
action: replace | Overwrite the selected containers’ pull policies. |
registries or imagePullPolicy omitted or null | Preserve existing pull policies. |
registries: {} | Leave pull policies unchanged. The mutation entry still needs a property to set. |
Empty string or a value other than Always, IfNotPresent, or Never | Reject the rule configuration. |
Kubernetes fills an omitted pull policy before mutation. Capsule overwrites
both that default and an explicit user value with either action: merge does
not mean “only when unset” for this property. Later applicable mutation
entries can overwrite it again. Conditions
gate the whole entry and see changes made by preceding entries.
registries is a settings object. The mutation applies the same policy to every
selected container, regardless of its image registry. To restrict image sources
or validate the resulting pull policy, use
registry enforcement, where
registries is a list of matchers with an optional policy allowlist.
Apply different namespace profiles
This Tenant uses Always for all container groups in namespaces labeled
image-profile: always-pull and allows images only from registry.k8s.io.
Namespaces labeled image-profile: init-only receive the mutation only for
init containers and native sidecars, preserving regular-container policies.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
image-profile: always-pull
mutate:
- action: replace
workloads:
targets: [pod]
registries:
imagePullPolicy: Always
enforce:
action: allow
workloads:
targets: [pod/containers, pod/initcontainers, pod/ephemeralcontainers]
registries:
- exact: [registry.k8s.io]
policy: [Always]
- namespaceSelector:
matchLabels:
image-profile: init-only
mutate:
- action: merge
workloads:
targets: [pod/initcontainers]
registries:
imagePullPolicy: Always
| Request | Result from these rules |
|---|
New Pod in always-pull, with omitted, Never, or IfNotPresent policies | Regular and init containers receive Always; images must pass the registry allowlist. |
New ephemeral container in always-pull | The new container receives Always; registry enforcement still applies. |
New Pod in init-only | Init containers and native sidecars receive Always; regular containers keep their policies. |
| Namespace with neither profile label | Neither rule applies. |
Existing containers and stored controller templates are unchanged. Pods created
by controllers receive the mutation when admitted. Ordinary Pod updates do not
rewrite pull policies, and ephemeral-container updates change only newly added
containers. Image-volume pull policies are outside the mutation targets.
Image pull secrets
registries.imagePullSecrets sets the Pod’s spec.imagePullSecrets using a
list of references with a name field. This is a Pod-level setting: use
targets: [pod], targets: [], or omit targets. Container-only targets are
rejected, including when clearing the list.
| Configuration | Behavior |
|---|
merge or omitted action | Keep each existing name once, in first-occurrence order, then append missing configured names. |
replace | Set the complete list to the configured references. |
imagePullSecrets: [] with replace | Clear the Pod’s image pull secret references. |
imagePullSecrets: [] with merge | Remove duplicate references; otherwise preserve the existing list. |
Property omitted or null | Preserve the list with either action. |
Merge retains each name already supplied on the Pod once, including
ServiceAccount defaults.
Replace can remove those references. Later mutation entries see and can change
the resulting list. Repeated names on the incoming Pod are removed, and overlapping
rules or repeated admission do not introduce duplicates. Omitting the property
leaves the list untouched. Each entry accepts up to 64 references with unique,
valid Secret names; duplicate configured names are rejected for both actions.
Referenced Secrets must exist in the Pod’s namespace and contain suitable
registry credentials. Kubernetes accepts kubernetes.io/dockerconfigjson or
kubernetes.io/dockercfg Secrets for this purpose; see
image pull secrets.
Capsule writes only the references: it does not create, copy, read, or verify
the Secrets. Provision the credentials in each selected namespace separately,
for example with TenantResource or
GlobalTenantResource.
This Tenant adds a shared reference in one namespace profile, replaces the list
in another, and clears it in a third. Create platform-registry or team-registry
in the corresponding namespaces before workloads need to pull private images.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
pull-credentials: shared
mutate:
- action: merge
workloads:
targets: [pod]
registries:
imagePullSecrets:
- name: platform-registry
- namespaceSelector:
matchLabels:
pull-credentials: dedicated
mutate:
- action: replace
workloads:
registries:
imagePullSecrets:
- name: team-registry
- namespaceSelector:
matchLabels:
pull-credentials: none
mutate:
- action: replace
workloads:
registries:
imagePullSecrets: []
These rules apply only when a Pod is created, including Pods created by
controllers. They do not rewrite controller templates, ServiceAccounts, existing
Pods, or the secret list when adding ephemeral containers. Configure both
imagePullPolicy and imagePullSecrets in one registries block when using the
Pod target. For a container-specific pull policy, use a separate mutation entry.
Reference
This complete Tenant combines every supported workload mutation property with
placement, security-profile, and image pull policy enforcement. It selects namespaces labeled
example.com/application: checkout and configures workloads for Linux nodes.
The tenant-scheduler scheduler, node pool, and zone labels must exist in the
cluster. Eligible nodes must support user namespaces
and AppArmor. Provision the checkout-registry image pull Secret
in each selected namespace. Application Pods should carry
app.kubernetes.io/part-of: checkout so the spread and Pod-affinity selectors
describe the intended workload.
The first mutation entry replaces the built-in scheduler default while
preserving custom scheduler names. The second entry independently applies the
remaining placement settings, supplies missing Pod-level security profiles, and
sets all selected container root filesystems read-only and image pull policies
to Always. It also adds checkout-registry to the Pod’s image pull secret
references. Provide writable volume mounts for application paths that need them.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: checkout
spec:
owners:
- kind: User
name: checkout-owner
rules:
- namespaceSelector:
matchLabels:
example.com/application: checkout
mutate:
- action: replace
conditions:
- name: default-scheduler
expression: >-
!has(object.spec.schedulerName) || object.spec.schedulerName in ['', 'default-scheduler']
workloads:
placement:
scheduler: tenant-scheduler
- action: merge
workloads:
targets: [pod]
security:
readOnlyRootFilesystem: true
hostUsers: false
seccompProfile:
type: RuntimeDefault
appArmorProfile:
type: RuntimeDefault
registries:
imagePullPolicy: Always
imagePullSecrets:
- name: checkout-registry
placement:
nodeSelector:
kubernetes.io/os: linux
infrastructure.example.com/pool: shared
tolerations:
- key: infrastructure.example.com/dedicated
operator: Equal
value: shared
effect: NoSchedule
# Application Pods must carry app.kubernetes.io/part-of: checkout.
topologySpreadConstraints:
- topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
maxSkew: 1
labelSelector:
matchLabels:
app.kubernetes.io/part-of: checkout
affinity:
nodeAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
nodeSelectorTerms:
- matchExpressions:
- key: topology.kubernetes.io/zone
operator: In
values: [zone-a, zone-b]
podAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 50
podAffinityTerm:
topologyKey: topology.kubernetes.io/zone
labelSelector:
matchLabels:
app.kubernetes.io/part-of: checkout
app.kubernetes.io/component: cache
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app.kubernetes.io/part-of: checkout
enforce:
action: allow
workloads:
targets:
- pod
- pod/containers
- pod/initcontainers
- pod/ephemeralcontainers
# Allow any image source, but require the mutated pull policy.
registries:
- exp: '.*'
policy: [Always]
security:
seccompProfiles:
- types: [RuntimeDefault]
appArmorProfiles:
- types: [RuntimeDefault]
placement:
nodeSelector:
- key: {exact: [kubernetes.io/os]}
values: {exact: [linux]}
- key: {exact: [infrastructure.example.com/pool]}
values: {exact: [shared]}
- key: {exp: '^placement\.example\.com/[a-z0-9-]+$'}
values: {exp: '^[a-z0-9-]+$'}
tolerations:
- key: {exact: [infrastructure.example.com/dedicated]}
operators: [Equal]
values: {exact: [shared]}
effects: [NoSchedule]
- key:
exact:
- node.kubernetes.io/not-ready
- node.kubernetes.io/unreachable
operators: [Exists]
effects: [NoExecute]
tolerationSeconds:
max: 600
allowUnlimited: false
- key: {exact: [node.kubernetes.io/memory-pressure]}
operators: [Exists]
effects: [NoSchedule]
topologySpreadConstraints:
- topologyKey:
exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
whenUnsatisfiable: [DoNotSchedule, ScheduleAnyway]
maxSkew: {min: 1, max: 3}
labelSelector:
required: true
requirements:
- key: {exact: [app.kubernetes.io/part-of]}
operators: [In]
values: {exact: [checkout]}
affinity:
- types: [nodeAffinity]
modes: [required]
requirements:
- key: {exact: [topology.kubernetes.io/zone]}
operators: [In]
values: {exact: [zone-a, zone-b]}
- types: [nodeAffinity]
modes: [preferred]
weight: {min: 1, max: 100}
requirements:
- key: {exact: [kubernetes.io/arch]}
operators: [In]
values: {exact: [amd64, arm64]}
- types: [podAffinity, podAntiAffinity]
modes: [preferred]
topologyKey:
exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
namespaceScope: SameNamespace
weight: {min: 1, max: 100}
labelSelector:
required: true
requirements:
- key: {exact: [app.kubernetes.io/part-of]}
operators: [In]
values: {exact: [checkout]}
- key: {exact: [app.kubernetes.io/component]}
operators: [In, NotIn]
values: {exact: [api, worker, cache]}
| Submitted value or scope | Result |
|---|
Scheduler omitted or default-scheduler | Replaced with tenant-scheduler. |
| Custom scheduler name | Preserved; the placement and security mutations still apply. |
| Existing image pull secret references | Each name is kept once in first-occurrence order; checkout-registry is appended if absent. |
Image pull policy omitted, IfNotPresent, or Never | Set to Always on regular/init containers at creation and newly added ephemeral containers. |
Root filesystem flag omitted or false | Set to true on regular/init containers at creation and newly added ephemeral containers. |
| Pod seccomp or AppArmor profile omitted | Defaulted to RuntimeDefault. |
| Explicit Pod or container profile | Preserved by mutation, then rejected unless its effective type is RuntimeDefault. |
| Existing node selector or required affinity | Combined with the configured restrictions, then checked by the placement allowlists. |
Namespace without example.com/application: checkout | These rules do not apply. |
Under enforce, the pod target validates placement and Pod-level profile defaults. The three
container targets validate effective profiles and image pull policies for regular,
init, and ephemeral containers, including container profile overrides. The registry
matcher accepts all image sources while requiring Always. Under mutate, targets: [pod]
selects all container groups for the root filesystem flag and image pull policy,
and Pod-level fields, including image pull secrets, for the other properties; profile mutation still writes only Pod-level profiles. An Unconfined override or privileged
container is rejected after mutation.
The toleration allowlist includes common Kubernetes-injected tolerations.
Controllers such as DaemonSets may add others; include the entries needed by
the workloads selected by your rule. The affinity validation uses one flat list
for node affinity, Pod affinity, and Pod anti-affinity.
See the placement matcher reference
for regular expressions, empty matchers, durations, selector operators, and
namespace scope.
These mutations run on Pod creation. The root filesystem flag and image pull policy
also apply to new ephemeral containers; the other properties are skipped on that subresource.
Controller templates and existing containers are not rewritten. Profile enforcement also applies on subsequent Pod updates and
pods/ephemeralcontainers updates. See Order and scope
for rule composition and admission order.
2 - Enforce
Enforcement policies and restrictions on a per-Namespace basis with Rules
Namespace rules can enforce admission behavior for selected resources in Tenant namespaces. Each enforce block can define an action and one or more matchers.
Each policy chapter identifies its API configuration path, then opens with
Rule Composition: Actions and order, followed by Admission scope and
limits. Policy sections explain configuration and examples. The final
Reference section provides complete Tenant examples. Use the
policy reference to choose a chapter.
See Order and scope for how mutation precedes
enforcement, and Conditions for conditional enforcement.
Audience and match expressions
are documented under Rules.
Rule Composition
Actions and order
Rules are evaluated in declaration order. If multiple allow or deny rules match the same evaluated value, the last matching allow or deny rule wins. If at least one allow rule is configured for a matcher and no allow or deny rule matches the evaluated value, Capsule denies the request. In other words, allow rules create an allow-list for that matcher. audit rules are purely observational: they never influence the allow/deny decision, and report matches through Kubernetes events. Some policies also add admission warnings; see the reporting behavior documented for each policy.
Each enforce block supports an action field:
| Action | Behavior |
|---|
allow | Allows the matching request and enables allow-list behavior for the matcher. If at least one allow rule exists and no allow or deny rule matches a value, Capsule denies that value. Additional constraints, such as image pull policy, must also be satisfied. |
deny | Denies the matching request. A later matching allow rule can override it. |
audit | Reports matching values without allowing or denying the request. NetworkPolicy CIDR rules emit Kubernetes events; other policies may also return admission warnings. |
If action is omitted, Capsule treats the rule as deny.
Storage volume rules use these actions only
for additional access to PVs without a Tenant ownership label. They preserve
existing Tenant-owned volume access, and cannot override another Tenant’s PV
label. A storage audit match never grants access.
Allow-list behavior is evaluated per matcher and per evaluated value. For example, if a registry allow rule exists for harbor/.*, a Pod image from docker.io/library/nginx:latest is denied unless another later or earlier allow rule also matches that image. Audit rules do not satisfy this allow-list requirement.
This precedence model allows both broad defaults and specific exceptions. For example, you can allow all Harbor images but deny a customer path afterwards:
rules:
- enforce:
action: allow
workloads:
registries:
- exp: "harbor/.*"
- enforce:
action: deny
workloads:
registries:
- exp: "harbor/customer/.*"
In this example, harbor/nginx:1.14.2 is allowed, while harbor/customer/app:1.0.0 is denied because the later, more specific deny rule also matches.
You can also deny broadly and allow a more specific exception afterwards:
rules:
- enforce:
action: deny
workloads:
registries:
- exp: "harbor/customer/.*"
- enforce:
action: allow
workloads:
registries:
- exp: "harbor/customer/prod-image/.*"
In this example, harbor/customer/test-image/app:1.0.0 is denied, while harbor/customer/prod-image/app:1.0.0 is allowed.
Audit
Use action: audit to observe workload usage without directly blocking the request. Audit rules emit Kubernetes events and add warnings to the admission response, but they do not allow or deny the request. If an allow-list is active for the same matcher and no allow rule matches the evaluated value, the request is still denied even when an audit rule matches.
For registry enforcement:
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: audit
workloads:
targets:
- pod/containers
registries:
- exp: "docker.io/.*"
Applying a Pod with docker.io/library/nginx:latest succeeds in this audit-only example because no registry allow-list is configured. The API server response contains an admission warning and Capsule emits a related event for the Pod.
For QoS enforcement:
rules:
- enforce:
action: audit
workloads:
qosClasses:
- Burstable
Applying a Burstable Pod succeeds in this audit-only example because no QoS allow-list is configured. Capsule emits an event and returns an admission warning.
For scheduler enforcement:
rules:
- enforce:
action: audit
workloads:
placement:
schedulers:
- exact:
- custom-scheduler
Applying a Pod with spec.schedulerName: custom-scheduler succeeds in this audit-only example because no scheduler allow-list is configured. Capsule emits an audit event and returns an admission warning.
When audit rules are used together with allow rules, the matching value must still be allowed explicitly. For example, an audited registry reference that does not match any registry allow rule is denied by the allow-list, but Capsule still emits the audit event before denying the request.
Admission scope and limits
Enforcement runs during admission for resources in the namespaces selected by
the rule. Audience filters the caller, and
conditions decide whether an enforcement
block participates. Each policy chapter lists its supported resources,
operations, subresources, and handling of missing values.
Mutation runs before validation. An allow decision applies to its evaluated
policy and value; other applicable checks, RBAC, and Kubernetes API validation
must still pass. In particular, an enclosing action: audit does not disable
resource defaulting or mutation.
Enforcement evaluates incoming writes. Changing a rule or namespace profile
does not by itself repair existing resources or revoke established bindings.
Storage ownership, resource mutation, and checks involving related resources
have specific limits documented in their chapters.
Reference
The following fields belong to spec.rules[].enforce. Each policy page includes
a complete Tenant example and explains its supported resources, matching
behavior, and admission scope.
To disallow CIDR-based ingress and egress grants in every namespace of a Tenant:
rules:
- enforce:
action: deny
network:
policies:
ingress:
cidrs:
- 0.0.0.0/0
- ::/0
egress:
cidrs:
- 0.0.0.0/0
- ::/0
Place this rules block under Tenant.spec. The rule rejects grants from or to any
IPv4 or IPv6 CIDR, as well as unrestricted ingress or egress rules with omitted
peers. Selector-based peers and empty grant lists pass their CIDR check.
Each direction is independent; omit ingress or egress to leave that direction
unconstrained.
The NetworkPolicy reference
shows how later rules can permit approved ranges in selected namespaces.
2.1 - Workloads
Workload enforcement
Workload enforcement is configured under spec.rules[].enforce.workloads.
Use targets to select native workload kinds or locations inside their Pod
specs. A targets-only rule matches the selected kinds themselves. When workload
policies are present, targets scope those policies: resource management,
OCI registries, scheduler names, QoS, placement,
security profiles, and PodDisruptionBudgets.
Rules use the shared namespace selection,
audiences,
conditions, and
actions and order.
See Reference for a complete Tenant example.
Rule Composition
Actions and order
Rules are evaluated in declaration order after namespace selection, audience,
and condition filtering. The last matching allow or deny decision wins for
each evaluated value. An omitted action defaults to deny.
| Action | Behavior |
|---|
allow | Creates an allow-list for the evaluated matcher. A value must match an applicable allow or deny rule; otherwise an active allow-list rejects it. |
deny | Rejects matching values, unless a later matching allow rule overrides the decision. |
audit | Reports matches without allowing or denying them. Audit matches never satisfy an allow-list. |
Each policy and value is evaluated independently. Allowing a registry does not
bypass a scheduler, security, placement, or other applicable constraint.
For constraints on requests and limits and
PodDisruptionBudgets, deny rejects violations and allow
requires compliance. Their policy sections explain which values constitute a
match and how audit events and admission warnings are reported.
Resource policies also have a mutation phase. The last applicable policy is
effective for each target, resource, and request or limit field. The enclosing
action: audit does not disable resource mutation. See
Rule order and policy overrides for how
Preserve and Default reset earlier constraints.
Admission scope and limits
Workload rules govern admission of the selected resources and Pod-spec
locations. Workload Targets maps kinds to their Pod specs;
the controller and Pod tables show where
each policy takes effect.
| Scope | Behavior |
|---|
| Targets omitted | Policies use their supported Pod locations. Controller templates require explicit controller targets. |
| Creation and updates | Validation checks the incoming resource on supported write paths. Policy-specific subresource handling is described below. |
| Resource mutation | Request and limit policies mutate Pods on creation only. With explicit controller targets, they also mutate controller templates on creation and update. |
| Pod subresources | Resource validation skips Pod subresources, including ephemeralcontainers. Registry and security policies use their supported container targets. |
| PDB relationships | Checks can run on workload writes, PDB writes, and supported controller replica changes, including /scale. See Evaluation for triggers and limits. |
| Existing resources | Rule or namespace-profile changes affect subsequent relevant admissions; they do not rewrite existing workloads. |
A controller write and the Pods it later creates are separate admissions, often
with different callers. Include the required targets and
audiences for both paths. Conditions inspect the actual
incoming resource, which can also be a PDB or a Scale object.
Kubernetes validation, LimitRanges, ResourceQuotas, and other admission policies
still apply after Capsule’s checks. See
Admission lifecycle for the resource policy stages and
Interaction with Kubernetes resource controls
for their combined behavior.
Workload Targets
Configure targets under spec.rules[].enforce.workloads.targets. The same list
selects workloads for resource, registry, scheduler, QoS, placement, security, and
PodDisruptionBudget policies;
individual policies do not have separate target lists.
- With no workload policies, targets apply the rule’s
action to the selected
workload kinds. For example, targets: [daemonset] with action: deny rejects
DaemonSets. - With workload policies, targets scope those policies to the selected workloads
and Pod-spec locations. Adding a policy means the rule checks that property
instead of restricting the kind itself. Use separate rule entries when you
need both kind restrictions and property checks.
Omitted or empty targets preserve the default Pod scopes described below and do
not enable controller checks. An empty workload block has no effect. Namespace
selectors, audiences, and enforce.conditions apply to both forms.
Each target selects the object being admitted. With workload policies
configured, Capsule reads the Pod spec at the following location:
| Target | Native API group / kind | Pod spec evaluated on that object |
|---|
pod | core / Pod | spec |
deployment | apps / Deployment | spec.template.spec |
statefulset | apps / StatefulSet | spec.template.spec |
daemonset | apps / DaemonSet | spec.template.spec |
replicaset | apps / ReplicaSet | spec.template.spec |
replicationcontroller | core / ReplicationController | spec.template.spec |
job | batch / Job | spec.template.spec |
cronjob | batch / CronJob | spec.jobTemplate.spec.template.spec |
For example, cronjob/containers checks regular containers at
spec.jobTemplate.spec.template.spec.containers[] on the CronJob itself.
It does not select a Job or Pod by following its CronJob owner.
Requests match their own API group and kind, without following owner references.
A custom resource named DaemonSet in another API group is not a native target.
Targets use the exact lowercase values shown here; wildcards such as pod/*,
deployment/*, and * are not supported.
Migration: targets-only rules previously had no effect. They now apply the
action to the selected kinds, and an omitted action defaults to deny. Remove
unused targets-only entries before upgrading if they were placeholders.
Controller targets
Controller targets check the stored Pod template during controller creation and
main-resource updates. They do not wait for a Pod to be created. A denial rejects
the controller write; an accepted template supplies the values Kubernetes later
uses to create child workloads. Existing children are not rewritten by admission.
The table uses cronjob as an example. The same suffixes and policy scopes apply
to every controller kind listed above, using that kind’s Pod-spec path.
Paths in this table are relative to spec.jobTemplate.spec.template.spec.
✅ indicates support; ❌ means the policy does not apply to that target.
Only policies configured in the rule are evaluated.
| Policy | cronjob | cronjob/containers | cronjob/initcontainers | cronjob/volumes |
|---|
| Resource policies | ✅ Pod-level resources and regular/init-container resources | ✅ containers[].resources | ✅ initContainers[].resources | ❌ |
| Registry policies | ✅ Regular/init-container images and image volumes | ✅ Regular container images | ✅ Init container images | ✅ Image volumes under volumes[].image |
| Placement policies | ✅ Pod placement fields | ❌ | ❌ | ❌ |
| Seccomp / AppArmor | ✅ Effective regular/init-container profiles | ✅ Effective regular-container profiles | ✅ Effective init-container profiles | ❌ |
| PDB policies | ✅ Template labels: overlap and unhealthy eviction policy | ❌ | ❌ | ❌ |
For example, deployment/initcontainers has the same scope as
cronjob/initcontainers, but reads spec.template.spec.initContainers[].
Controller templates do not support /ephemeralcontainers. Resource policies
cannot be combined with volume targets.
The placement row covers node selectors, tolerations, topology spread, and
affinity. Scheduler and QoS policies always evaluate the whole selected Pod
spec, even with a container or volume suffix. For example,
cronjob/initcontainers with a scheduler policy checks
spec.jobTemplate.spec.template.spec.schedulerName.
A whole-controller target includes container resources and image references;
the explicit pod target has the narrower scope in the Pod table.
For security profiles, the effective value is the container override or its
inherited Pod default. A whole-controller target does not independently require
a Pod-level default when every container supplies its own allowed profile.
Whole-controller PDB checks use the Pod template’s labels to find matching
budgets. CronJobs, Jobs, and DaemonSets support overlap and unhealthy eviction
policy checks, but not evictableReplicas. See the
PDB target table for the complete mapping.
Resource request/limit policies also mutate the selected template locations on
controller creation and updates. Targets do not extend mutate[].workloads to
controllers; mutations configured there remain limited to Pod creation. See
Admission lifecycle.
Conditions see the whole admitted controller: use object.spec.template.spec
for a Deployment and object.spec.jobTemplate.spec.template.spec for a CronJob.
Events concern that controller object. Namespace selection, audiences, and
conditions must match each request independently.
Example: CronJob admission
This rule denies a forbidden scheduler or image in Deployment and CronJob
templates while permitting compliant ones:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets: [deployment, cronjob]
placement:
schedulers:
- exact: [forbidden-scheduler]
registries:
- exact: ['example.com/blocked/app:v1']
For the CronJob path, assuming the namespace, audience, and conditions match:
| Admission request | Does this rule run? | Effect |
|---|
| Create or update the CronJob | ✅ cronjob is selected | Checks the scheduler and image references under spec.jobTemplate.spec.template.spec. A match rejects the CronJob write. |
| The CronJob controller creates a Job | ❌ job is not selected | The Job inherits the template, but this rule does not check its admission. Add job to check it. |
| The Job controller creates a Pod | ❌ No Pod target is selected | The Pod inherits the template, but this rule does not check its admission. Add the relevant Pod targets to check it. |
To check all three stages, include cronjob, job, and the Pod targets needed
for the policy. For registry checks, use pod/containers,
pod/initcontainers, pod/ephemeralcontainers, and/or pod/volumes for the image
locations you want to cover; pod alone does not select images. Controller-created
Jobs and Pods can have different callers, so include those identities in any
audience filter.
With no workload policies, targets: [cronjob] instead acts on the kind:
deny rejects CronJobs, while allow establishes a workload-kind allow-list.
A targets-only allow-list must also allow job and pod for scheduled workloads
to run. See Select Workloads.
Pod targets
When workload policies are present, Pod targets select these locations.
✅ indicates support; ❌ means the policy does not apply to that target.
| Target | Resource policies | Registry policies | Placement policies | Seccomp / AppArmor | PDB policies |
|---|
pod | ✅ Pod-level spec.resources | ❌ | ✅ Pod placement fields | ✅ Pod default only | ✅ Overlap and unhealthy eviction policy |
pod/containers | ✅ spec.containers[].resources | ✅ Regular container images | ❌ | ✅ Effective regular-container profiles | ❌ |
pod/initcontainers | ✅ spec.initContainers[].resources | ✅ Init container images | ❌ | ✅ Effective init-container profiles | ❌ |
pod/ephemeralcontainers | ❌ | ✅ Ephemeral container images | ❌ | ✅ Effective ephemeral-container profiles | ❌ |
pod/volumes | ❌ | ✅ Image volumes under spec.volumes[].image | ❌ | ❌ | ❌ |
For seccomp and AppArmor, an effective profile resolves a container override
before the Pod default. Init containers include restartable sidecars.
Selecting pod alone checks the default and leaves container overrides
unchecked. See Target security profiles for a
complete Tenant and a comparison of these scopes.
This table describes enforcement. Profile mutations under mutate[].workloads
set Pod-level defaults on Pod creation, using pod or omitted mutation targets.
Neither merge nor replace modifies container profile overrides. For
security.readOnlyRootFilesystem and registries.imagePullPolicy mutations, pod selects all container groups and
container targets can narrow the selection. See the separate
mutation targets table. Enforcement
targets do not control mutation.
Scheduler and QoS policies always evaluate the whole Pod selected by any Pod
target. For example, pod/containers does not limit QoS calculation to regular
containers. Resource policies cannot be combined with targets for ephemeral
containers or volumes.
Omitting targets, or setting it to [], selects all compatible Pod locations:
Pod-level resources, regular and init container resources, regular/init/ephemeral
container images, image volumes, and Pod placement fields. Scheduler and QoS
checks also apply. Profile policies check effective regular, init, and ephemeral
container profiles. PDB overlap and unhealthy eviction policies check the Pod;
eviction-count bounds require an explicit supported controller target.
Resource-name compatibility is described under
Targeting resource locations.
An explicit targets: [pod] is narrower than omitted targets: it selects
Pod-level resources, placement, and security-profile defaults, together with scheduler and QoS checks. It
does not select container resources or any image references. List the desired
Pod parts explicitly when narrowing container, image, or profile policies.
For example, deny matching images only in init containers:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets: [pod/initcontainers]
registries:
- exp: "harbor/init-only/.*"
The same image reference in a regular container, ephemeral container, or image
volume is unaffected by this rule. Combine targets to check multiple locations:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets: [pod/containers, pod/ephemeralcontainers]
registries:
- exp: "debug/.*"
This checks regular and ephemeral container images. It does not check init
containers or image volumes.
Targets without policies match the whole kind. A targets-only rule with
targets: [pod/containers] and action: deny rejects Pods, just like
targets: [pod]. A part suffix narrows property checks only when workload
policies are configured.
Target security profiles
This complete Tenant gives namespaces two different profile policies. Namespaces
labeled profile-scope: pod-only require RuntimeDefault in the Pod’s security
context. Namespaces labeled profile-scope: containers require RuntimeDefault
for every effective regular, init, and ephemeral container profile. Namespaces
with neither label are unaffected by these rules.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
profile-scope: pod-only
enforce:
action: allow
workloads:
targets: [pod]
security:
seccompProfiles:
- types: [RuntimeDefault]
appArmorProfiles:
- types: [RuntimeDefault]
- namespaceSelector:
matchLabels:
profile-scope: containers
enforce:
action: allow
workloads:
targets:
- pod/containers
- pod/initcontainers
- pod/ephemeralcontainers
security:
seccompProfiles:
- types: [RuntimeDefault]
appArmorProfiles:
- types: [RuntimeDefault]
These rules supply no defaults. For Linux Pods, each mechanism is checked
independently:
| Submitted profile configuration | profile-scope: pod-only | profile-scope: containers |
|---|
| Pod RuntimeDefault; all containers inherit it | ✅ Allowed | ✅ Allowed |
| Pod RuntimeDefault; regular container overrides it with Unconfined | ✅ Allowed | ❌ Rejected |
| Pod RuntimeDefault; init container overrides it with Unconfined | ✅ Allowed | ❌ Rejected |
| Pod RuntimeDefault; ephemeral container overrides it with Unconfined | ✅ Allowed | ❌ Rejected |
| No Pod profile; every container explicitly uses RuntimeDefault | ❌ Rejected | ✅ Allowed |
| No profile at either level | ❌ Rejected | ❌ Rejected |
List only pod/initcontainers to restrict the profile checks to init containers.
Combine pod with the three container targets to require both an allowed Pod
default and allowed effective container profiles. Omitting targets checks all
three container groups without independently requiring a Pod default.
The security reference combines Pod-level defaulting with
container enforcement. A container override survives mutation and must then
pass the selected enforcement checks, including on pods/ephemeralcontainers
updates.
Select Workloads
A targets-only allow rule establishes an allow-list across the eight supported
native workload kinds. Other resource kinds remain outside this allow-list.
Allow controller-created children explicitly:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
targets: [deployment, replicaset, pod]
This allows Deployments, ReplicaSets, and Pods while rejecting the other
supported native workload kinds. Allowing a Deployment alone does not allow its
ReplicaSets or Pods: each request is evaluated separately. Kind restrictions
apply to creation and main-resource updates, not deletion or subresources.
Deny DaemonSets
Add a targets-only rule under Tenant.spec.rules to deny DaemonSets in namespaces
labeled env: test:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
env: test
enforce:
action: deny
workloads:
targets: [daemonset]
This rejects DaemonSet creation and main-resource updates in the selected
namespaces. It does not remove existing DaemonSets or block deletion or
subresources. Other workload kinds and non-selected namespaces are unaffected
by this rule. To cover all namespaces in the Tenant, omit namespaceSelector.
Use action: audit to record matching requests without blocking them. For kind
restrictions, the last matching allow or deny rule wins; audit does not
change that decision.
Resource Management
Configure CPU, memory, and other resource requests and limits, together with
Pod QoS policies.
Requests and Limits
Resource policies let a Tenant administrator normalize and enforce the
requests and limits of Pods created in Tenant namespaces. They cover common
requirements that a Kubernetes LimitRange cannot express directly, including:
- always removing a resource limit;
- making a limit equal to its request;
- defaulting a missing request or limit without replacing an explicit value;
- deriving a missing limit from a request using a maximum ratio; and
- rejecting or auditing explicit limits that exceed that ratio.
Resource policies are configured under
spec.rules[].enforce.workloads.resources. The requests and limits maps are
keyed by Kubernetes resource name. Each map entry contains a case-sensitive
policy and, for policies that need one, a value.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
rules:
- enforce:
action: deny
workloads:
resources:
limits:
cpu:
policy: Remove
memory:
policy: MatchRequest
At every compatible resource location, this example produces the following
behavior:
- CPU limits are removed, even when the submitted Pod defines them;
- memory limits are set to the corresponding memory requests; and
- because
targets is omitted, the policy applies to Pod-level resources,
regular containers, and init containers.
Before and after admission
Consider this submitted Pod:
apiVersion: v1
kind: Pod
metadata:
name: api
spec:
containers:
- name: api
image: registry.example.com/api:1.0.0
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "1"
memory: 1Gi
Capsule admits it with the equivalent resource configuration:
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 512Mi
The CPU request is untouched, the CPU limit is removed, and the memory limit is
replaced with the memory request. Because targets is omitted, Capsule applies
the same policy independently to spec.resources, every regular container, and
every init container in the Pod. The submitted example has no Pod-level or init
container resources, so those locations remain empty.
If MatchRequest finds neither a request nor a limit, the resource is
compliant and remains undefined. If it finds a limit without a corresponding
request, it cannot derive a replacement and the final state violates the
policy. With action: deny the Pod is rejected; with action: audit Capsule
admits it and emits an audit event and admission warning.
Policy reference
After starting with the common example above, use this reference to
choose the precise behavior for each request and limit.
Requests
The following policies are supported under resources.requests:
| Policy | value | Mutation behavior | Validation behavior |
|---|
Preserve | Not allowed | Leaves an existing or missing request unchanged. | Adds no constraint and clears earlier constraints for the same target and resource. |
Default | Required | Sets the configured quantity only when the request is absent. An explicit request is preserved. | Adds no constraint and clears earlier constraints for the same target and resource. |
Remove | Not allowed | Removes the request when it is present. | Requires the request to be absent in the final Pod. |
Default a request without overriding tenant workloads that already specify it:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
resources:
requests:
cpu:
policy: Default
value: 100m
memory:
policy: Default
value: 256Mi
If a container requests 500m CPU, Capsule preserves 500m. If the CPU
request is missing, Capsule adds 100m.
Remove an extended resource request:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
resources:
requests:
example.com/temporary-device:
policy: Remove
Resource names must be valid Kubernetes qualified names. Custom and extended
resource names can be used for container and init-container policies, subject
to the normal Kubernetes rules for that resource.
Limits
The following policies are supported under resources.limits:
| Policy | value | Mutation behavior | Validation behavior |
|---|
Preserve | Not allowed | Leaves an existing or missing limit unchanged. | Adds no constraint and clears earlier constraints for the same target and resource. |
Default | Required | Sets the configured quantity only when the limit is absent. An explicit limit is preserved. | Adds no constraint and clears earlier constraints for the same target and resource. |
Remove | Not allowed | Removes the limit when it is present. | Requires the limit to be absent in the final Pod. |
MatchRequest | Not allowed | If a request exists, sets the limit to exactly the request, replacing a different explicit limit. If no request exists, it does not add a limit. | Requires request and limit to be equal. When the request is absent, the limit must also be absent. |
Ratio | Required | If a positive request exists and the limit is absent, sets the limit to request * value. An explicit limit is preserved for validation. | Requires a positive request and a limit no greater than request * value. |
Default is a fill-only policy. It does not mean that every limit must equal
the configured value. Use MatchRequest when Capsule should own the limit and
keep it equal to the request, or Ratio when explicit tenant values are allowed
within a bounded range.
Ratio limits
Ratio defines the maximum permitted limit as a multiple of the request. It is
available only for limits and supports these resource names:
| Resource | Calculation precision |
|---|
cpu | Exact decimal arithmetic, rounded down to the nearest millicore. |
memory | Exact decimal arithmetic, rounded down to a whole byte. |
ephemeral-storage | Exact decimal arithmetic, rounded down to a whole byte. |
The ratio must be greater than or equal to 1. Quote fractional ratios in YAML
for clarity, for example "1.5".
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
resources:
limits:
memory:
policy: Ratio
value: "1.5"
For a memory request of 1Gi, the maximum limit is 1536Mi:
| Submitted limit | Mutation | Result with action: deny |
|---|
| Missing | Capsule adds 1536Mi. | Admitted. |
1Gi | Explicit value is preserved. | Admitted because it is below the maximum. |
1536Mi | Explicit value is preserved. | Admitted because it equals the maximum. |
2Gi | Explicit value is preserved. | Denied because it exceeds the maximum. |
An explicit limit is never reduced by Ratio. This is deliberate: preserving
the submitted value allows allow, deny, and audit to decide how a ratio
violation should be handled. Use MatchRequest when the limit should always be
rewritten instead.
Ratio requires a positive request. When the request is missing or zero,
Capsule cannot calculate a default limit. The missing or non-positive request
is therefore a policy violation. A deny action blocks it, an audit action
reports it, and an allow action treats it as an allow-list miss and blocks it.
Capsule calculates ratios without floating-point arithmetic and rounds down so
that the generated limit never exceeds the configured factor. For example, a
CPU request of 101m with a ratio of 1.5 produces a limit of 151m, not
152m.
Targeting resource locations
Resource policies reuse enforce.workloads.targets to select resource locations.
See Pod targets for the location table and
Workload targets for controller targets.
Ephemeral containers and volumes cannot be selected for resource policies.
When targets is omitted or empty, each resource policy applies to every
compatible location: Pod-level spec.resources, regular containers, and init
containers. An explicit targets list narrows that default.
Compatibility is evaluated per resource name. Kubernetes Pod-level resources
support only cpu, memory, and huge-page resources. With omitted targets,
those names apply at all three locations, while other valid names such as
ephemeral-storage and extended resources apply only to regular and init
containers. Capsule skips those incompatible names at spec.resources; it does
not reject the rule or discard their container policies.
Only regular containers:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
resources:
limits:
cpu:
policy: Remove
Only init containers:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/initcontainers
resources:
requests:
cpu:
policy: Default
value: 25m
Both regular and init containers, explicitly:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
- pod/initcontainers
resources:
limits:
memory:
policy: MatchRequest
Only Pod-level resources:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod
resources:
requests:
cpu:
policy: Default
value: 500m
memory:
policy: Default
value: 1Gi
limits:
cpu:
policy: Ratio
value: "2"
memory:
policy: Ratio
value: "1.5"
The Kubernetes API server must support spec.resources for Pod-level resource
policies to be useful. Pod-level policies support cpu, memory, and huge-page
resources. Because Ratio itself supports only CPU, memory, and ephemeral
storage, a pod-level ratio can be configured for CPU or memory, but not for huge
pages. Use an explicit pod target when the policy must not affect containers.
Target compatibility
When a workload block contains resources, every explicitly configured target
in that block must support resource policies. A block that combines resource
policies with pod/ephemeralcontainers or pod/volumes is invalid. Put registry
or image-volume enforcement that needs those targets in a separate rule.
If pod is explicitly combined with a container target, every configured
resource name must also be valid at Pod level. Omit targets when a policy
should use the broad default and automatically skip only its incompatible
Pod-level location.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
# Valid: resource and registry policies use separate target scopes.
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
resources:
limits:
cpu:
policy: Remove
- enforce:
action: deny
workloads:
targets:
- pod/ephemeralcontainers
- pod/volumes
registries:
- exp: "untrusted.example.com/.*"
Advanced behavior
The following concepts are mainly relevant when combining multiple rules,
selectors, admission actions, or Kubernetes resource-management components.
Admission lifecycle
Resource policy admission has a mutation phase and a validation phase. Knowing
which phase a policy uses is important when choosing a policy and an action.
| Admission phase | Operations | Behavior |
|---|
| Mutation | Pod CREATE | Capsule applies the effective Default, Remove, MatchRequest, or Ratio mutation to the incoming Pod. Preserve does not change the field. |
| Validation | Pod CREATE and normal UPDATE | Capsule checks the final resource values for Remove, MatchRequest, and Ratio. Preserve and Default do not add a validation constraint. |
| Pod subresources | Any | Resource validation is skipped for Pod subresources, including ephemeralcontainers. Resource policies do not mutate ephemeral containers. |
For Pods, resource mutation is limited to creation. Capsule does not try to
rewrite resource fields on existing Pods, where Kubernetes immutability rules
would normally reject the change. Normal Pod updates are still validated so
that the policy describes the accepted final state.
With omitted targets or Pod targets, Capsule mutates and validates the resulting
Pods without rewriting controller templates. With explicit controller targets,
resource request/limit policies also mutate and validate the stored Pod template
on controller creation and updates. Defaults therefore appear directly in the
controller. Other workload mutations under mutate[].workloads remain Pod-only.
Important
The enclosing action does not disable mutation. A rule with action: audit
still applies its resource mutation within the supported target and operation. The action controls the result
of a remaining validation violation. For example, Ratio leaves an explicit
limit unchanged, then deny rejects an excessive value while audit reports
it without blocking the Pod.Actions and compliance
The action belongs to the enclosing enforce block and applies to
validation constraints created by Remove, MatchRequest, and Ratio:
| Action | When the final value complies | When the final value violates the policy |
|---|
deny | No deny decision is produced. | The Pod is denied. |
allow | The policy produces an allow decision. | The value does not satisfy the resource allow-list and the Pod is denied unless a later matching rule allows it. |
audit | No audit is emitted. | The Pod is admitted by this rule, and Capsule emits a Kubernetes event and admission warning. Other rules can still deny it. |
As with other enforcement matchers, the last matching allow or deny
decision wins. Audit decisions never override allow or deny decisions.
The following example denies memory limits above 1.5 times the request by
default but permits up to 2 times the request in namespaces labeled
burst-memory=true:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
resources:
limits:
memory:
policy: Ratio
value: "1.5"
- namespaceSelector:
matchLabels:
burst-memory: "true"
enforce:
action: allow
workloads:
targets:
- pod/containers
resources:
limits:
memory:
policy: Ratio
value: "2"
A container with a 1Gi request and a 1792Mi limit violates the first rule
but complies with the later allow rule. It is admitted only in a namespace that
matches the selector on the second rule.
Audit explicit ratio violations without rejecting them:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: audit
workloads:
targets:
- pod/containers
resources:
limits:
ephemeral-storage:
policy: Ratio
value: "2"
Remember that a missing limit is still defaulted during create. The audit is
emitted only when the final Pod remains noncompliant, such as when it contains
an explicit excessive limit or a limit without a positive request.
Rule order and policy overrides
Rules are processed in declaration order after namespaceSelector and
audience filtering. Resource policies are resolved independently for every
combination of target, resource name, and field (request or limit). This
allows CPU and memory, or requests and limits, to be managed independently.
For mutation, the last applicable policy for a target, resource name, and field
is effective. A later Preserve therefore prevents an earlier mutation:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
resources:
limits:
cpu:
policy: Remove
- namespaceSelector:
matchLabels:
preserve-cpu-limit: "true"
enforce:
action: allow
workloads:
targets:
- pod/containers
- pod/initcontainers
resources:
limits:
cpu:
policy: Preserve
In matching namespaces, the later Preserve policy leaves CPU limits intact.
In other namespaces, the earlier Remove policy remains effective.
For validation, Preserve and Default also clear earlier constraints for the
same target, field, and resource. Constraints declared after that reset point
are still evaluated. This makes Preserve useful as a namespace-specific
escape hatch and Default useful when switching from enforcement back to
fill-only behavior.
For the request identity of controller-created Pods, see Audience.
Configuration validation
Capsule validates resource policy configuration before using it. Invalid rules
are reported on the RuleStatus and are not silently accepted.
The following requirements apply:
- At least one non-empty
requests or limits map is required. - Policy names are case-sensitive.
value is required only by request Default, limit Default, and limit
Ratio.value must not be supplied to Preserve, Remove, or MatchRequest.- Default quantities must not be negative. A zero default is accepted, although
Kubernetes or another admission policy can impose a stricter requirement.
- A ratio must be greater than or equal to
1. Ratio supports only cpu, memory, and ephemeral-storage.pod/ephemeralcontainers, pod/volumes, and the deprecated pod/images
target cannot be used in a workload block containing resource policies.- A request cannot use
Remove while the same rule uses MatchRequest or
Ratio for that resource’s limit, because those limit policies require the
request. - When the
pod target is explicitly present, resource names in that block are
restricted to cpu, memory, and huge-page resources supported by
Kubernetes pod-level resources. - When
targets is omitted, Pod-incompatible resource names remain valid and
are applied only to regular and init containers.
For example, this configuration is invalid:
resources:
requests:
memory:
policy: Remove
limits:
memory:
policy: Ratio
value: "1.5"
The following is also invalid because value is not accepted by Remove:
resources:
limits:
cpu:
policy: Remove
value: "1"
Interaction with Kubernetes resource controls
Resource policies complement Kubernetes resource controls; they do not replace
or disable them.
LimitRange can still apply its own defaults and min/max validation.
Configure its constraints consistently with Capsule resource policies to
avoid admission behavior that depends on webhook and admission-plugin order.ResourceQuota, Global Resource Quota, and Resource Pools evaluate the
resulting Pod resources according to their own semantics.- Other mutating webhooks can also change resources. Capsule’s generic mutating
webhook requests reinvocation when another mutator changes the Pod, and the
validating webhook checks the final object it receives.
- Kubernetes performs its own resource validation after mutation. A resource
configuration accepted by a Capsule policy can still be rejected by the API
server or another admission policy.
- A Vertical Pod Autoscaler or another controller that creates replacement Pods
is subject to the policy when those Pods are admitted. Normal updates are
validated but not mutated.
To inspect the effective resources after admission, query the Pod rather than
only its workload-controller template:
kubectl get pod api -n solar-apps \
-o jsonpath='{.spec.containers[*].resources}'
If admission is denied, Capsule’s error identifies the target path, resource
field, and failed requirement. Audit violations appear as admission warnings
and Kubernetes events associated with the Pod and Tenant.
QoS Classes
For example, reject BestEffort Pods in the Tenant namespaces:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
rules:
- enforce:
action: deny
workloads:
qosClasses:
- BestEffort
QoS class enforcement allows administrators to allow, deny, or audit Pods based on their computed Kubernetes QoS class.
QoS rules are configured under enforce.workloads.qosClasses.
Supported QoS classes are:
| QoS class | Description |
|---|
Guaranteed | The Pod has CPU and memory requests and limits set so that requests equal limits. |
Burstable | The Pod has at least one CPU or memory request or limit, but does not qualify as Guaranteed. |
BestEffort | The Pod has no CPU or memory requests or limits. |
Capsule evaluates the QoS class of the incoming Pod during create and update admission. If Kubernetes has already populated status.qosClass, Capsule can use that value; otherwise it computes the QoS class from the Pod specification.
Deny BestEffort Pods:
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: deny
workloads:
qosClasses:
- BestEffort
With this rule, a Pod without CPU or memory requests and limits is denied:
apiVersion: v1
kind: Pod
metadata:
name: best-effort
spec:
containers:
- name: shell
image: harbor/platform/debian:latest
command: ["sleep", "infinity"]
Example rejection:
Error from server (Forbidden): error when creating "pod.yaml": admission webhook "pods.projectcapsule.dev" denied the request: QoS class "BestEffort" at status.qosClass is denied by namespace rule
Audit Burstable Pods:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: audit
workloads:
qosClasses:
- Burstable
A matching Pod is admitted in this audit-only example, but Capsule emits an event and the API server response contains an admission warning. If a QoS allow-list is also configured and the Pod’s QoS class is not allowed, the Pod is denied while the audit event is still emitted.
Allow BestEffort only for selected namespaces:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
qosClasses:
- BestEffort
- namespaceSelector:
matchLabels:
allow-best-effort: "true"
enforce:
action: allow
workloads:
qosClasses:
- BestEffort
Because later matching allow or deny rules take precedence, namespaces labeled allow-best-effort=true can run BestEffort Pods, while other namespaces cannot.
Placement
Configure scheduling matchers under enforce.workloads.placement, including
schedulers, nodeSelector, tolerations, topologySpreadConstraints, and
affinity. targets remains at enforce.workloads.targets and scopes all
configured workload policies.
Configure placement policies under spec.rules[].enforce.workloads. Each property
has its own matcher list:
Read Order and scope for how mutation precedes
enforcement, and Conditions to apply checks
conditionally. The Reference combines all placement policies with
resource and image rules. To configure mutations alongside enforcement, see the
complete mutation example.
Scheduler policies evaluate one scheduler name. Node selectors, tolerations,
topology spread constraints, and affinity are evaluated entry by entry. For
these structured entries, fields within a matcher are combined with AND, and
entries in the matcher list are alternatives. A match requires one complete
matcher to match the entry; different matchers cannot authorize separate parts
of the same affinity term or spread constraint.
The action rules apply to each evaluated value:
allow enables an allow-list, deny rejects matches, and audit only records
them. The last matching allow or deny wins. Every supplied entry must pass its
policy; a single rejected entry denies the request. Mutated values and
Kubernetes-injected values are evaluated in the same way.
Allow rules do not require a placement property to be present. Omitted
properties and empty lists have no entries to validate. Use
workload mutation to establish settings before
enforcement. An omitted policy list or [] adds no matchers. For node selectors,
tolerations, spread constraints, and affinity, [{}] matches every present
entry. Scheduler matchers require a match expression instead.
Those four structured properties use the pod target or a whole-controller
target such as deployment. Omitting targets includes Pod placement checks;
selecting only container or volume targets skips them. Scheduler checks apply
to the whole selected Pod spec even when a part is selected. See
Workload targets for all target scopes.
For those four properties, main-resource Pod updates check changed values and
recheck affinity and spread selectors when labels change. Subresources skip
these checks.
Matcher fields
Scheduler matchers and the key, values, topologyKey, and namespaces fields
use the common match expression structure:
exact, exp, and optional negate.
Omit a matcher field to leave it unrestricted. A supplied expression must contain
exact or exp; key: {} is invalid. Use exp: '^$' to match an empty string.
The property references below list the additional fields supported by each
matcher, such as operators, effects, and numeric bounds.
Selector policies
Topology-spread and Pod-affinity matchers use the following policy structure for
labelSelector and, where supported, namespaceSelector:
| Field | Behavior |
|---|
required | When true, require at least one effective selector requirement. |
requirements | Allowlist for individual selector requirements. Omission or an empty list leaves requirements unrestricted. |
Each requirement matcher supports key, values, and operators. Label
selectors support In, NotIn, Exists, and DoesNotExist. Every actual
requirement must match one complete entry, and every supplied value must match
its values expression. matchLabels entries are evaluated as singleton In
requirements.
Exists and DoesNotExist have no values to test. Permitting those operators
permits their valueless form; use operators: [In] when allowed values must
restrict the selection. A requirement allowlist constrains supplied requirements;
it does not require every listed key to appear.
Dynamic matchLabelKeys and mismatchLabelKeys are checked as In and NotIn
requirements using the incoming Pod’s label values. Missing dynamic labels are
ignored, matching Kubernetes behavior. required: true uses this effective
selector, including any dynamic requirements.
Scheduler names
Configure scheduler matchers under enforce.workloads.placement.schedulers. Capsule checks
spec.schedulerName during create and update admission.
Kubernetes defaults an omitted or empty name to default-scheduler before Pod
admission. Enforcement checks that name unless a mutation changes it; the
matcher itself does not fill an empty value. A
conditional scheduler default
can replace the built-in scheduler while preserving custom names.
Allow only these schedulers:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
placement:
schedulers:
- exact: [tenant-scheduler, batch-scheduler]
| Pod scheduler name at enforcement | Result |
|---|
tenant-scheduler or batch-scheduler | Allowed |
other-scheduler | Denied |
default-scheduler, including a name defaulted by Kubernetes | Denied |
To allow a scheduler family as well as fixed names, combine exact and exp.
Either one can satisfy this matcher:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
placement:
schedulers:
- exact: [default-scheduler, batch-scheduler]
exp: '^tenant-[a-z0-9-]+$'
Deny one scheduler while leaving other names unrestricted by this rule:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
placement:
schedulers:
- exact: [unsafe-scheduler]
Alternatively, negate a trusted set to deny all other names:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
placement:
schedulers:
- exact: [default-scheduler, tenant-scheduler]
negate: true
To record usage without changing the admission decision, use audit:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: audit
workloads:
placement:
schedulers:
- exact: [custom-scheduler]
This emits an event and an admission warning for custom-scheduler. Other
policies still apply; an audit match cannot satisfy a scheduler allow-list.
Node selectors
Each matcher evaluates one key/value pair in spec.nodeSelector.
| Field | Matches |
|---|
key | The node-label key. |
values | The node-label value, including an explicitly empty value. |
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
placement:
nodeSelector:
- key:
exact: [kubernetes.io/os]
values:
exact: [linux]
- key:
exp: '^placement\.example\.com/[a-z0-9-]+$'
values:
exact: [shared, batch]
This allows kubernetes.io/os: linux and keys such as
placement.example.com/pool: shared. It rejects kubernetes.io/os: windows,
placement.example.com/pool: dedicated, and unlisted keys. A Pod with no node
selector is allowed by this rule.
Tolerations
Each matcher evaluates one complete toleration in spec.tolerations.
| Field | Matches |
|---|
key | Taint key expression. An empty Pod key is a literal empty string. |
values | Toleration value expression. |
operators | Equal or Exists. An omitted Pod operator is Equal. |
effects | NoSchedule, PreferNoSchedule, NoExecute, or the literal empty string "". |
tolerationSeconds | Inclusive min and max bounds, with optional allowUnlimited. |
An empty toleration key or effect has broad Kubernetes semantics. It does not
match an allowlist of specific keys or effects. To allow an empty effect, include
"" explicitly in effects.
An absent tolerationSeconds means unlimited. Bounds apply to finite durations;
allowUnlimited defaults to true. Set it to false to require a finite duration:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
placement:
tolerations:
- key:
exact: [node.kubernetes.io/not-ready, node.kubernetes.io/unreachable]
operators: [Exists]
effects: [NoExecute]
tolerationSeconds:
max: 600
allowUnlimited: false
This permits only the two listed NoExecute tolerations, each with a finite
duration of at most 600 seconds. Add other matchers for any additional tolerations
needed by your workloads or injected by Kubernetes.
Disallow every toleration
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
placement:
tolerations:
- {}
The empty matcher rejects every toleration, including injected ones. Kubernetes
normally adds not-ready and unreachable tolerations, so this rule rejects ordinary
Pods with those entries too. Use an allowlist when system tolerations must remain
permitted. A Pod with no tolerations has no entry for this deny rule to match.
Topology spread constraints
Each matcher evaluates one complete entry in spec.topologySpreadConstraints.
| Field | Matches |
|---|
topologyKey | Topology-key expression. |
whenUnsatisfiable | List containing DoNotSchedule or ScheduleAnyway. |
maxSkew | Inclusive min and max bounds. |
minDomains | Inclusive bounds; an omitted Pod value is evaluated as 1. |
nodeAffinityPolicy | Honor or Ignore; an omitted Pod value is Honor. |
nodeTaintsPolicy | Honor or Ignore; an omitted Pod value is Ignore. |
labelSelector | Selector policy for the Pods counted by the constraint. |
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
placement:
topologySpreadConstraints:
- topologyKey:
exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
whenUnsatisfiable: [DoNotSchedule, ScheduleAnyway]
maxSkew:
min: 1
max: 3
labelSelector:
required: true
requirements:
- key:
exact: [app]
operators: [In]
values:
exact: [checkout]
This allows zone or host spreading with a skew of 1–3 and a selector for
app: checkout. A constraint for another topology key, a skew of 4, or a selector
using another key is rejected. The rule constrains supplied entries; it does not
add a spread constraint or require one to be present.
Affinity
All three affinity types use one flat list under enforce.workloads.placement.affinity.
Each matcher evaluates a complete required or preferred term from spec.affinity.
| Field | Applies to | Behavior |
|---|
types | All | Select nodeAffinity, podAffinity, or podAntiAffinity. Omission selects all compatible types. |
modes | All | Select required, preferred, or both. Omission selects both. |
weight | Preferred terms | Inclusive min and max bounds within 1–100. Requires modes: [preferred]. |
requirements | Node affinity | Match matchExpressions using key, values, and operators. |
fieldRequirements | Node affinity | Match matchFields using the same requirement structure. |
topologyKey | Pod affinity and anti-affinity | Topology-key expression. |
labelSelector | Pod affinity and anti-affinity | Selector policy for matching Pods. |
namespaceScope | Pod affinity and anti-affinity | SameNamespace or Any. Omission imposes no scope restriction. |
namespaces | Pod affinity and anti-affinity | Expression matched against every explicitly supplied namespace. |
namespaceSelector | Pod affinity and anti-affinity | Selector policy for matching namespaces. |
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
placement:
affinity:
- types: [nodeAffinity]
modes: [required, preferred]
requirements:
- key:
exact: [topology.kubernetes.io/zone]
operators: [In]
values:
exact: [zone-a, zone-b]
- types: [podAffinity, podAntiAffinity]
modes: [preferred]
topologyKey:
exact: [kubernetes.io/hostname]
namespaceScope: SameNamespace
weight:
min: 1
max: 100
This allows node affinity using the listed zone requirements and preferred Pod
affinity or anti-affinity within the Pod’s namespace, grouped by host. Required
Pod affinity and anti-affinity are rejected. The second matcher leaves Pod-label
selectors unrestricted; add labelSelector to constrain them.
Node requirements also support Gt and Lt. The value matcher checks the
literal numeric operand; it does not query node labels. When either node
requirement list constrains a term, requirements from the other source must be
explicitly allowed if present. For example, fieldRequirements: [{}] permits
any native-valid field requirement alongside constrained requirements.
SameNamespace permits an omitted namespaces list or explicit references to
the Pod’s own namespace, and requires the Pod term’s namespaceSelector to be
absent. Even namespaceSelector: {} selects more than the current namespace and
does not match this scope. Any leaves namespace scope unrestricted. A
namespaces expression alone does not restrict namespaces selected through a
namespaceSelector.
Type-specific fields only match types on which they are meaningful. Explicitly
incompatible combinations are rejected when the policy is saved. Matching
examines the submitted terms without listing nodes, Pods, or namespaces.
Pod Disruption Budgets
Use enforce.workloads.disruptionBudgets to constrain PodDisruptionBudgets (PDBs)
covering selected workloads in a namespace. These policies can prevent overlapping
budgets, require room for voluntary disruption at the desired replica count, and
allow eviction of unhealthy Pods during maintenance.
The policies check existing PDBs; they do not create a PDB or require one to exist.
For example, require matching PDBs to leave room for at least one voluntary
eviction per Deployment or StatefulSet with a nonzero desired replica count.
This rule applies to every namespace in the Tenant:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
targets: [deployment, statefulset]
disruptionBudgets:
evictableReplicas:
min: 1
For three desired replicas, a PDB with minAvailable: 2 passes, while
minAvailable: 3 is rejected. This checks the configured allowance; actual
eviction still depends on workload health.
The Reference includes all three properties in the combined Tenant.
For maintenance scenarios, see workload best practices.
The shared workloads.targets list selects the Pods and controller templates
whose PDBs are checked. Capsule also checks PDB writes that affect those workloads
in the same namespace. A controller need not have running Pods for its template
to be checked.
| Target | allowOverlap | unhealthyPodEvictionPolicies | evictableReplicas |
|---|
Omitted or pod | ✅ Pod labels | ✅ PDBs selecting the Pod | ❌ |
deployment, statefulset, replicaset, replicationcontroller | ✅ Template labels | ✅ PDBs selecting the template | ✅ Desired spec.replicas, including /scale |
daemonset, job, cronjob | ✅ Template labels | ✅ PDBs selecting the template | ❌ No supported desired replica count |
| Container or volume part targets | ❌ | ❌ | ❌ |
Controller targets are explicit opt-ins. Include pod as well to check labels on
actual Pods, including those created by controllers. CronJobs use the Pod template
inside their Job template. Part targets such as pod/containers and
deployment/containers do not participate in PDB checks.
An evictableReplicas policy must explicitly select at least one of the four
supported controller targets. Other targets in the same rule still participate
in overlap and unhealthy-policy checks.
Evictable replicas
evictableReplicas.min and .max define inclusive bounds on the configured
number of replicas that a PDB would allow to be evicted from a selected controller
at its desired size, assuming those replicas are healthy. Both bounds are
nonnegative integers. An omitted bound is unrestricted; for example, min: 1
requires room for at least one disruption without setting an upper limit.
Capsule calculates this allowance from the PDB and the controller’s desired
spec.replicas:
| PDB size field | Configured allowance |
|---|
Integer minAvailable | Desired replicas minus minAvailable, with a minimum of zero |
Percentage minAvailable | Desired replicas minus the rounded-up percentage of desired replicas, with a minimum of zero |
Integer maxUnavailable | maxUnavailable, capped at the desired replica count |
Percentage maxUnavailable | Rounded-up percentage of desired replicas, capped at the desired replica count |
| Both size fields omitted | Zero |
Both percentage forms use Kubernetes’ upward rounding.
For an allow rule with evictableReplicas.min: 1:
| PDB configuration | Desired replicas | Allowance | Admission |
|---|
minAvailable: "75%" | 3 | 0 | ❌ Rejected |
minAvailable: "75%" | 4 | 1 | ✅ Allowed |
minAvailable: 2 | 3 | 1 | ✅ Allowed |
maxUnavailable: "25%" | 1 | 1 | ✅ Allowed; the only replica can be evicted |
maxUnavailable: 0 | 4 | 0 | ❌ Rejected |
Replica changes are checked through both the controller resource and its /scale
subresource, including requests from kubectl scale and autoscalers. With the
first two rows above, scaling from four replicas down to three is rejected.
Zero replicas are exempt from the replica-count check. Scaling a controller
to zero remains possible. Scaling back above zero checks the resulting allowance.
Overlap and unhealthy-policy checks still apply to zero-replica templates.
Each matching PDB is checked against each selected controller independently.
Capsule does not add replicas from multiple controllers or calculate a combined
live PDB allowance. This can be stricter than Kubernetes for a PDB that covers
multiple workloads. The max bound applies to this per-controller calculation;
it does not cap aggregate live disruptions across multiple controllers. Use a PDB
selector that covers the complete intended controller rather than an arbitrary
subset of its Pods.
Configured allowance and live eviction
These checks use desired replicas, not status.disruptionsAllowed. Pending,
unhealthy, missing or terminating Pods and rollout state can still prevent an
eviction. A compliant configuration does not guarantee that a drain will finish.
PDBs also do not limit direct Pod deletion or controller scale-down.Unhealthy Pods
unhealthyPodEvictionPolicies matches the effective value of the PDB’s
spec.unhealthyPodEvictionPolicy:
| Value | Behavior for unhealthy Running Pods |
|---|
IfHealthyBudget | Eviction requires the application’s healthy Pod count to satisfy its budget. This is also the effective value when the PDB omits the field. |
AlwaysAllow | Unhealthy Running Pods may be evicted even when the healthy budget is not satisfied. Healthy Pods still obey the PDB. |
An allow rule listing only AlwaysAllow rejects a matching PDB that omits this
field or sets IfHealthyBudget. Alternatively, a deny rule listing
IfHealthyBudget rejects that value, including omission. An omitted or empty
policy list adds no restriction. These rules validate the field; they do not
populate it. See Kubernetes’ unhealthy Pod eviction policies
for their effect during a drain.
Overlap
allowOverlap: false requires at most one PDB to select each Pod or selected
controller template. Kubernetes accepts overlapping PDBs, but an eviction can
fail when more than one PDB selects the Pod. See API-initiated eviction.
Matching uses Pod or Pod-template labels and each PDB’s matchLabels and
matchExpressions in the same namespace. With policy/v1, a null selector
matches no Pods, while an empty selector {} matches every Pod in the namespace.
An omitted allowOverlap field adds no constraint. A later allow rule with
allowOverlap: true can permit overlap, but it does not change Kubernetes'
eviction behavior.
For example, two PDBs selecting app: frontend can be created before any matching
Pod or selected controller template exists. Creating a matching workload is then
rejected. If the workload already exists, creating the second PDB is rejected
instead. Deleting a conflicting PDB or changing selectors to remove the overlap
allows subsequent writes.
Evaluation
Checks run on both sides of the relationship:
| Request | PDB checks |
|---|
| Selected Pod creation or changed labels | Matching PDB overlap and unhealthy eviction policies; label changes through Pod /status are also checked |
| Selected controller creation or changed template labels | Matching PDB policies for that controller target |
Supported controller replica change with an applicable evictableReplicas policy, including /scale | Resulting configured eviction allowance and the other applicable PDB policies |
| PDB creation or relevant spec change | Policies for existing selected Pods and controller templates that its selector matches |
| Unchanged relevant fields | No new PDB check |
| Deletion | Allowed by these policies, so a conflicting PDB can be removed |
A PDB with no matching selected workload can be created. The workload is checked
when it is later admitted, allowing either creation order without making PDB
existence a requirement.
Actions differ between numeric/boolean constraints and the list of policy values:
| Property | allow | deny | audit |
|---|
allowOverlap and evictableReplicas | Requires compliance | Rejects violations | Reports violations |
unhealthyPodEvictionPolicies | Accepts listed values | Rejects listed values | Reports listed values |
An omitted action defaults to deny. Normal rule ordering
applies independently to each property: the last matching allow/deny decision
wins. Allowing one property does not bypass a failure of another. Audit rules
emit events without changing the admission decision. Omitted properties never
override an earlier rule.
Namespace selection, audiences, and
enforcement conditions use the usual rules
scope. Conditions inspect the actual incoming object: a PDB during a PDB write,
a controller during a controller write, and an autoscaling/v1 Scale during a
/scale update. Include all relevant actors and write paths when using these
gates; a condition restricted to Pod creation does not protect controller, PDB,
or scale updates.
Changing a rule or namespace profile affects subsequent relevant admissions; it
does not repair existing PDBs or immediately revalidate stored workloads.
Concurrent PDB and workload writes can race because admission across separate
resources is not atomic. Monitor actual PDB status and workload health when
planning maintenance.
Security
Configure seccompProfiles and appArmorProfiles under
enforce.workloads.security. The workload-level targets field selects the
Pod or container locations to check.
seccompProfiles and appArmorProfiles allow, deny, or audit profile choices
for Linux workloads. The Seccomp and AppArmor sections
below describe each mechanism and its local-profile matching. Use
security mutations to supply Pod
defaults before enforcement. These checks complement namespace Pod Security
labels; they skip Pods and templates declaring spec.os.name: windows.
Both matchers use these fields and the usual rule order:
| Field | Meaning |
|---|
types | Required list of 1–3 entries containing RuntimeDefault, Localhost, or Unconfined. |
localhostProfiles | Optional list of up to 64 exact, exp, and negate expressions for local profile paths or names. Requires Localhost in types. |
Each enforcement rule supports up to 64 seccompProfiles matchers and up to
64 appArmorProfiles matchers. Oversized lists are rejected when the policy
is saved, limiting nested matching work during workload admission.
Multiple matchers are alternatives. The expressions in localhostProfiles
constrain only Localhost; other types in the same matcher still match normally.
Omitting these expressions permits any non-empty Localhost path or name.
Audit reports matching admission choices through Capsule events; it does not
configure kernel syscall logging or AppArmor learning modes.
The shared workload targets select where checks apply. Omitted
targets check effective profiles for regular, init, and ephemeral containers.
Explicit pod checks only the Pod default, so it does not prevent container
overrides. Controller targets inspect effective profiles in their templates,
including CronJobs’ nested templates. Targeted templates must declare allowed
profiles themselves because profile mutation applies only to Pod creation.
Validation runs on Pod and controller create/update requests, and on
pods/ephemeralcontainers updates. It does not run on status, resize, or delete
requests and does not reconfigure existing containers. Policy changes apply
to subsequent admissions, so a previously admitted Pod may fail a later update
under a stricter policy.
Seccomp
enforce.workloads.security.seccompProfiles evaluates a container’s explicit
securityContext.seccompProfile, falling back to the Pod’s
spec.securityContext.seccompProfile. Regular containers, init containers
(including restartable sidecars), and ephemeral containers use this resolution.
A Pod default is optional when every selected container declares an allowed
profile of its own.
| Effective type | What the matcher checks |
|---|
RuntimeDefault | The workload selects the container runtime’s default seccomp profile. |
Localhost | The workload selects a local seccomp file. localhostProfiles matches its relative path. |
Unconfined | The workload explicitly disables seccomp, or the selected container is privileged. |
Privileged containers are evaluated as Unconfined even if their manifest
names a different profile. When neither the container nor Pod supplies a
profile, Capsule evaluates it as Unset. An allow-list rejects this value;
Capsule does not infer the kubelet’s seccomp default. Unset appears only in
diagnostics and cannot be listed in types. A deny rule for Unconfined alone
therefore permits an unset profile. Pair an allow-list with
seccomp mutation when a declared
profile is required.
Match local seccomp paths
For Localhost, localhostProfiles matches the value of localhostProfile,
such as profiles/solar.json, relative to the kubelet’s seccomp directory.
It does not match an absolute node path or inspect the file’s contents.
This complete Tenant allows RuntimeDefault, the exact local path
profiles/solar.json, and files matching ^profiles/shared/[a-z0-9-]+\.json$
in the selected namespaces. A local path outside those matches, Unconfined,
or an unset effective profile is denied. It does not add defaults; use
seccomp mutation for that.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: confined
enforce:
action: allow
workloads:
security:
seccompProfiles:
- types: [RuntimeDefault, Localhost]
localhostProfiles:
- exact: [profiles/solar.json]
- exp: '^profiles/shared/[a-z0-9-]+\.json$'
Admission checks the reference, while nodes apply the actual profile. Install
approved files on eligible nodes before use. See the
Kubernetes seccomp tutorial.
AppArmor
enforce.workloads.security.appArmorProfiles checks effective AppArmor profiles in this
order: the container’s structured securityContext.appArmorProfile, a legacy
per-container AppArmor annotation, then the Pod’s
spec.securityContext.appArmorProfile. This also covers init, restartable
sidecar, and ephemeral containers. A container override therefore remains
subject to enforcement even when the Pod default is allowed.
| Effective type | What the matcher checks |
|---|
RuntimeDefault | The workload selects the container runtime’s default AppArmor profile. |
Localhost | The workload selects a loaded AppArmor profile. localhostProfiles matches its name. |
Unconfined | The workload explicitly disables AppArmor, or the selected container is privileged. |
A missing effective profile is reported as Unset and fails an allow-list.
Capsule does not infer a runtime’s implicit AppArmor behavior. A deny rule
matching only Unconfined permits an unset profile. Use
AppArmor mutation with an allow-list
to require a declared profile. Privileged containers evaluate as Unconfined
regardless of the profile written in the manifest.
Match loaded AppArmor names
localhostProfiles matches a loaded profile name such as solar-confined.
The value is not the filesystem path where the AppArmor definition is stored.
For example, exact: [solar-confined] checks the name in localhostProfile;
exp: '^solar-[a-z0-9-]+$' would permit any matching name.
This complete Tenant allows RuntimeDefault and only the named local profile
in selected namespaces. Other Localhost names, Unconfined, and unset effective
profiles are rejected. It supplies no defaults by itself.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: apparmor
enforce:
action: allow
workloads:
security:
appArmorProfiles:
- types: [RuntimeDefault, Localhost]
localhostProfiles:
- exact: [solar-confined]
Capsule does not verify that AppArmor is supported, that the named profile is
loaded, or that its contents are identical on all nodes. Configure eligible
nodes before using these profiles. See the
Kubernetes AppArmor guide.
Security reference
This complete Tenant defaults both profiles in selected namespaces. It allows
RuntimeDefault and one approved Localhost profile for each mechanism. The same
checks apply to explicitly targeted Deployment templates, which must supply
their own profile configuration. Apply this example only with eligible Linux
nodes supporting AppArmor and with the custom profiles installed before use.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
security-profile: confined
mutate:
- action: merge
workloads:
security:
seccompProfile:
type: RuntimeDefault
appArmorProfile:
type: RuntimeDefault
enforce:
action: allow
workloads:
targets:
- pod/containers
- pod/initcontainers
- pod/ephemeralcontainers
- deployment
security:
seccompProfiles:
- types: [RuntimeDefault, Localhost]
localhostProfiles:
- exact: [profiles/solar.json]
appArmorProfiles:
- types: [RuntimeDefault, Localhost]
localhostProfiles:
- exact: [solar-confined]
| Container choice | Result |
|---|
| No override; Pod default is RuntimeDefault | Allowed through inheritance. |
| Approved Localhost profile | Allowed and preserved by merge. |
| Unapproved Localhost profile | Denied. |
| Explicit Unconfined or privileged container | Denied. |
| No profile at either level in a targeted template | Denied. |
| Ephemeral container overriding the Pod default with Unconfined | Denied. |
Registries
Registry enforcement allows administrators to allow, deny, or audit Pod image references. Registry matchers are evaluated against the full OCI reference string, including registry, repository path, image name, tag, or digest.
Registry rules are configured under enforce.workloads.registries. The shared Workload targets field under enforce.workloads.targets selects image references in Pods or controller templates. See Pod targets for container and image-volume scopes and examples.
Registry matchers use the common match expression structure:
registries:
- exact:
- harbor/platform/debian:latest
- harbor/platform/busybox:latest
- exp: "harbor/platform/.*"
Use exact for a fixed list of complete references and exp for path or registry patterns. A single matcher may contain both fields:
registries:
- exact:
- harbor/platform/debian:latest
exp: "harbor/shared/.*"
This matcher succeeds for harbor/platform/debian:latest or any reference matching harbor/shared/.*.
The following example allows Harbor images by default, denies a more specific customer path for regular containers and image volumes, allows and audits regular container images from an audit registry, and allows a production image path only for namespaces matching env=prod:
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: allow
workloads:
registries:
- exp: "harbor/.*"
- enforce:
action: deny
workloads:
targets:
- pod/containers
- pod/volumes
registries:
- exp: "harbor/customer/.*"
- enforce:
action: allow
workloads:
targets:
- pod/containers
registries:
- exp: "audit/.*"
- enforce:
action: audit
workloads:
targets:
- pod/containers
registries:
- exp: "audit/.*"
- namespaceSelector:
matchExpressions:
- key: env
operator: In
values: ["prod"]
enforce:
action: allow
workloads:
targets:
- pod/containers
- pod/volumes
registries:
- exp: "harbor/customer/prod-image/.*"
policy: ["Always"]
Apply the following Pod in namespace solar-test, which does not match the env=prod selector:
apiVersion: v1
kind: Pod
metadata:
name: image-volume
spec:
containers:
- name: shell
command: ["sleep", "infinity"]
imagePullPolicy: IfNotPresent
image: harbor/customer/test-image/debian:latest
volumeMounts:
- name: volume
mountPath: /volume
volumes:
- name: volume
image:
reference: quay.io/crio/artifact:v2
pullPolicy: IfNotPresent
The request is denied:
kubectl apply -f pod.yaml -n solar-test
Error from server (Forbidden): error when creating "pod.yaml": admission webhook "pods.projectcapsule.dev" denied the request: containers[0] reference "harbor/customer/test-image/debian:latest" is denied by registry rule "harbor/customer/.*"
The Pod is denied because the regular container image matches both harbor/.* and harbor/customer/.*. Since the deny rule is declared later, it has higher precedence.
The image volume reference is not denied by the shown deny rule because it does not match harbor/customer/.*. If the image volume used a matching reference, for example harbor/customer/volume-artifact:v1, the same deny rule would apply because it targets both pod/containers and pod/volumes.
In a namespace matching env=prod, the more specific production allow rule is also considered:
apiVersion: v1
kind: Pod
metadata:
name: prod-image
spec:
containers:
- name: shell
command: ["sleep", "infinity"]
imagePullPolicy: Always
image: harbor/customer/prod-image/debian:latest
The request is allowed because the namespace-specific rule matches later and allows harbor/customer/prod-image/.* with imagePullPolicy: Always.
Target-specific registry rules allow different behavior for different parts of the same Pod. For example, this rule denies the registry only for init containers:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/initcontainers
registries:
- exp: "harbor/init-only/.*"
A matching reference under spec.initContainers is denied. The same reference under spec.containers is ignored by this rule.
PullPolicy
Define the allowed image pull policies for a matching registry rule. Supported policies are:
Always: The image is always pulled.IfNotPresent: The image is pulled only if it is not already present on the node.Never: The image is never pulled. If the image is not present on the node, the Pod fails to start.
The policy field is optional. If no policy is specified, all image pull policies are accepted for the matching registry rule.
To set the policy before validation, use
mutate[].workloads.registries.imagePullPolicy.
The configured mutation must produce a policy accepted by the matching enforcement rule.
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: allow
workloads:
targets:
- pod/containers
registries:
- exp: "harbor/v2/customer-registry/.*"
policy: ["IfNotPresent", "Always"]
If the final matching registry decision is allow and that matching registry rule defines policy, the Pod must use one of the configured pull policies. For example, this rule allows the registry but only with Always:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
targets:
- pod/containers
registries:
- exp: "harbor/v2/customer-registry/.*"
policy: ["Always"]
A Pod using imagePullPolicy: Never for that registry is rejected:
Error from server (Forbidden): error when creating "pod.yaml": admission webhook "pods.projectcapsule.dev" denied the request: containers[0] reference "harbor/v2/customer-registry/debian:latest" uses pullPolicy=Never which is not allowed (allowed: Always)
Policy is checked only after the final registry decision is allow. A final deny decision always denies the request, regardless of the configured pull policy.
Negation
A registry matcher can be negated with negate: true. Negation applies to the final result of the matcher, including both exact and exp.
For example, the following rule denies every regular container image that is not from the trusted registry path:
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
registries:
- exp: "trusted/.*"
negate: true
With this rule:
trusted/backend/api:1.0.0 is allowed in this deny-only example because it does not match the negated deny rule and no registry allow-list is configured.docker.io/library/nginx:latest is denied because it does not match trusted/.*, so the negated matcher evaluates to true.
Negation also applies to exact values:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
registries:
- exact:
- trusted/backend/api:1.0.0
- trusted/frontend/web:1.0.0
negate: true
This rule denies every explicit container image except the two exact references listed, as long as no separate registry allow-list requires an explicit allow. If an allow rule is configured for the same matcher scope, the excepted references must also match an allow rule.
You can combine exact values, regular expressions, negation, namespace selectors, and action precedence. For example, deny all untrusted container images by default, but allow a controlled exception in production namespaces:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
registries:
- exact:
- trusted/base/debian:latest
exp: "trusted/platform/.*"
negate: true
- enforce:
action: allow
workloads:
targets:
- pod/containers
registries:
- exact:
- trusted/base/debian:latest
exp: "trusted/platform/.*"
- namespaceSelector:
matchLabels:
env: prod
enforce:
action: allow
workloads:
targets:
- pod/containers
registries:
- exp: "partner-registry/prod-approved/.*"
The second rule explicitly allows the trusted references that were excluded from the negated deny rule, which is required when registry allow-list behavior is active. In a namespace labeled env=prod, partner-registry/prod-approved/app:1.0.0 is allowed because the later matching allow rule overrides the earlier negated deny rule.
OCI Examples
Registry exact match examples
Use exact when you want to allow or deny a fixed set of complete image references:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
targets:
- pod/containers
registries:
- exact:
- harbor/platform/debian:latest
- harbor/platform/busybox:1.36
A Pod using harbor/platform/debian:latest or harbor/platform/busybox:1.36 is admitted. A Pod using harbor/platform/nginx:latest is denied because an allow rule exists for registry enforcement but does not match that reference.
You can combine exact and exp in the same registry matcher:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
workloads:
registries:
- exact:
- harbor/platform/debian:latest
exp: "harbor/shared/.*"
This rule allows the exact Debian image and any image under harbor/shared/*.
Reference
This complete Tenant combines workload kind restrictions, resource policies,
image rules, QoS, all five placement policies, and PDB constraints. It applies
only to its namespaces labeled example.com/profile: restricted; other namespaces in the
Tenant keep their own profiles. Replace solar-owner with your owner identity.
The kind rule rejects DaemonSets. Resource, image, QoS, and placement policies
use Pod targets and run when Pods are admitted, including controller-created
Pods. The PDB rule also selects Deployment and StatefulSet templates, covering
PDB writes and replica changes. See Workload targets to
expand the scope of individual policies.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
# Keep kind restrictions separate from property policies.
- namespaceSelector:
matchLabels:
example.com/profile: restricted
enforce:
action: deny
workloads:
targets: [daemonset]
# Manage resources on regular and init containers.
- namespaceSelector:
matchLabels:
example.com/profile: restricted
enforce:
action: deny
workloads:
targets: [pod/containers, pod/initcontainers]
resources:
requests:
cpu:
policy: Default
value: 100m
memory:
policy: Default
value: 128Mi
limits:
cpu:
policy: Remove
memory:
policy: Ratio
value: "2"
# Include the whole Pod for placement and its parts for image checks.
- namespaceSelector:
matchLabels:
example.com/profile: restricted
enforce:
action: allow
workloads:
targets:
- pod
- pod/containers
- pod/initcontainers
- pod/ephemeralcontainers
- pod/volumes
registries:
- exp: '^registry\.example\.com/solar/.+$'
policy: [Always, IfNotPresent]
qosClasses: [Burstable, Guaranteed]
placement:
schedulers:
- exact: [default-scheduler, batch-scheduler]
nodeSelector:
- key: {exact: [kubernetes.io/os]}
values: {exact: [linux]}
- key: {exact: [infrastructure.example.com/pool]}
values: {exact: [shared, batch]}
tolerations:
- key: {exact: [infrastructure.example.com/dedicated]}
operators: [Equal]
values: {exact: [shared]}
effects: [NoSchedule]
- key:
exact:
- node.kubernetes.io/not-ready
- node.kubernetes.io/unreachable
operators: [Exists]
effects: [NoExecute]
tolerationSeconds:
max: 600
allowUnlimited: false
- key: {exact: [node.kubernetes.io/memory-pressure]}
operators: [Exists]
effects: [NoSchedule]
topologySpreadConstraints:
- topologyKey:
exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
whenUnsatisfiable: [DoNotSchedule, ScheduleAnyway]
maxSkew: {min: 1, max: 3}
labelSelector:
required: true
requirements:
- key: {exact: [app.kubernetes.io/part-of]}
operators: [In]
values: {exact: [solar]}
affinity:
- types: [nodeAffinity]
modes: [required, preferred]
requirements:
- key: {exact: [topology.kubernetes.io/zone]}
operators: [In]
values: {exact: [zone-a, zone-b]}
- types: [podAffinity, podAntiAffinity]
modes: [preferred]
topologyKey:
exact: [topology.kubernetes.io/zone, kubernetes.io/hostname]
namespaceScope: SameNamespace
weight: {min: 1, max: 100}
labelSelector:
required: true
requirements:
- key: {exact: [app.kubernetes.io/part-of]}
operators: [In]
values: {exact: [solar]}
# Keep selected workloads compatible with voluntary maintenance.
- namespaceSelector:
matchLabels:
example.com/profile: restricted
enforce:
action: allow
workloads:
targets: [pod, deployment, statefulset]
disruptionBudgets:
allowOverlap: false
evictableReplicas:
min: 1
max: 2
unhealthyPodEvictionPolicies:
- AlwaysAllow
# Override the broader registry allow rule for this path.
- namespaceSelector:
matchLabels:
example.com/profile: restricted
enforce:
action: deny
workloads:
registries:
- exp: '^registry\.example\.com/solar/blocked/.+$'
- namespaceSelector:
matchLabels:
example.com/profile: restricted
enforce:
action: audit
workloads:
registries:
- exp: '^registry\.example\.com/solar/legacy/.+$'
With this configuration:
- DaemonSet creation and main-resource updates are denied in selected namespaces.
- On Pod creation, missing regular and init container requests default to
100m
CPU and 128Mi memory. CPU limits are removed. Missing memory limits become
twice the corresponding request; explicit limits above that ratio are denied.
Resource admission lifecycle describes update behavior. - Every selected image reference must use
registry.example.com/solar/ with an
allowed pull policy. The later deny rule blocks the solar/blocked/ path.
Images under solar/legacy/ are audited while the other checks still apply. - The resulting Pod must have
Burstable or Guaranteed QoS and use
default-scheduler or batch-scheduler. A custom scheduler must already be
installed to schedule Pods assigned to it. - Supplied node selectors, tolerations, spread constraints, and affinity terms
must match the configured shapes. These allow-lists do not require those
properties to be present or populate them. Use
workload mutation
to establish placement settings before enforcement.
- The toleration list includes common Kubernetes-injected entries. Add any
additional tolerations required by your selected workloads or cluster.
- Matching PDBs must not overlap and must use
AlwaysAllow for unhealthy Pod
eviction. Each selected Deployment or StatefulSet must have one to two
configured evictions available at its desired replica count when a PDB
covers it; zero replicas are exempt. Remove max: 2 if only a minimum is
needed. These checks neither require a PDB nor guarantee live drain progress.
2.2 - Metadata
Metadata Enforcement
Metadata enforcement is configured under spec.rules[].enforce.metadata.
Use Metadata Targets to select resource kinds, then configure
labels and annotations, including defaults, managed
values, required keys, and value validation. Migration covers the
legacy Namespace, Pod, and Service metadata options.
Rules use the shared namespace selection,
audiences,
conditions, and
actions and order.
See Reference for a complete Tenant example.
Rule Composition
Actions and order
Metadata rules are evaluated in declaration order after namespace selection,
audience, and condition filtering. The last matching allow or deny wins for
each metadata key and value.
| Action | Behavior |
|---|
allow | Creates an allow-list for the evaluated metadata key. A present value with no matching allow or deny decision is rejected when an allow-list is active. |
deny | Rejects matching metadata values. A later matching allow rule can override that decision. |
audit | Emits Kubernetes events and admission warnings without allowing or denying the request. Audit matches never satisfy an allow-list. |
If action is omitted, it defaults to deny. Each label and annotation key
is evaluated independently; allowing one key does not authorize another.
Admission scope and limits
Metadata rules are evaluated during create and update admission. On creation,
all present controlled keys are checked. On update, unchanged values skip value
matching, while required keys must still be present. Keep the following scope
and limits in mind:
| Behavior | Explanation |
|---|
| Namespaced resources and explicitly selected Namespaces are evaluated | Metadata rules normally target resources inside Tenant namespaces. Namespace is the only supported cluster-scoped kind, and it must be selected explicitly with kinds: ["Namespace"]. |
| Managed objects remain subject to enforcement | Setting managed-by=controller does not bypass metadata validation. Capsule-managed resources retain their separate protection against unauthorized edits. |
| Capsule-managed metadata is ignored | Built-in Capsule labels and annotations are treated as managed metadata and are ignored by metadata validation. Do not rely on metadata rules to validate Capsule-owned keys. |
| Managed annotation prefixes are ignored | Capsule-managed annotation prefixes such as resource quota and resource usage annotations are ignored. |
| Missing optional metadata is ignored | If required: false, the key is only evaluated when it is present. |
required applies to allow rules | required: true enforces presence for action: allow. deny and audit rules match values; they do not require missing keys to exist. |
| Empty metadata values are valid values | A label or annotation with an empty string value is still present and can be matched with exact: [""]. |
| Labels and annotations are independent | A matching annotation does not satisfy a required label with the same key, and a matching label does not satisfy a required annotation. |
Empty apiGroups means core v1 | Omitted apiGroups, an empty list, or an empty entry selects the core Kubernetes v1 API. Use apiGroups: ["*"] to match every API group and version. |
kinds must be set | Use kinds: ["*"] to match all namespaced kinds. A wildcard does not implicitly include Namespace. |
Capsule-managed labels include labels used to track Tenant ownership, resource pools, freeze and cordon state, promotion state, Capsule ownership, and generated namespace resources. Capsule-managed annotations include release and reconciliation annotations, available class and registry annotations, forbidden namespace metadata annotations, protected Tenant annotations, and resource quota or resource usage annotation prefixes.
Because these keys are owned by Capsule, metadata rules that reference them are ignored by default. Use application-specific labels and annotations for Tenant policy enforcement.
Metadata Targets
Each metadata rule defines which resource kinds it applies to:
| Field | Description |
|---|
apiGroups | List of API group or group/version selectors. Empty or omitted means core v1; apps matches every version in that group; apps/v1 matches that exact group/version; and "*" matches all groups and versions. |
kinds | List of Kubernetes kind selectors. "*" and partial wildcards match namespaced kinds, but Namespace must always appear as a separate literal entry to include it. |
Examples:
metadata:
- kinds:
- ConfigMap
- Service
This targets core v1 ConfigMap and Service resources because apiGroups
is omitted.
metadata:
- apiGroups:
- apps/v1
kinds:
- Deployment
- StatefulSet
This targets only apps/v1 Deployment and StatefulSet resources.
metadata:
- apiGroups:
- "*"
kinds:
- "*"
This targets all namespaced resources handled by the generic metadata webhook.
It does not target Namespace, despite both selectors being wildcards.
Partial wildcards are also supported:
metadata:
- apiGroups:
- "apps/*"
kinds:
- "*Set"
This can match resources such as apps/v1 ReplicaSet and apps/v1 StatefulSet.
Namespace
Namespace is the only cluster-scoped resource supported by metadata rules. It
is deliberately opt-in: the kinds list must contain the literal,
case-sensitive value Namespace. This prevents a broad rule intended for
resources inside Tenant namespaces from accidentally changing or rejecting the
Namespace object itself.
The Namespace GVK is core v1, Kind=Namespace. The apiGroups selector must
therefore match core v1. The clearest form is:
metadata:
- apiGroups:
- "v1"
kinds:
- "Namespace"
Because omitted apiGroups defaults to core v1, this shorter form is
equivalent:
metadata:
- kinds:
- Namespace
An API-group wildcard may also match core v1, but it still does not remove the
explicit-kind requirement. For example, this targets all namespaced kinds and
Namespace:
metadata:
- apiGroups:
- "*"
kinds:
- "*"
- Namespace
The following selectors do not target Namespace:
# A full kind wildcard is not an explicit Namespace opt-in.
metadata:
- apiGroups:
- "*"
kinds:
- "*"
# A partial kind wildcard is not an explicit Namespace opt-in either,
# even when its pattern would otherwise match the word "Namespace".
- apiGroups:
- "v1"
kinds:
- "Name*"
In short, both conditions must be true: apiGroups must match core v1, and
kinds must contain a dedicated Namespace entry.
API group selection
Omitted or empty apiGroups does not mean all API groups and versions. It
means the core Kubernetes API version v1.
For example:
metadata:
- kinds:
- Deployment
This does not match apps/v1 Deployment, because omitted apiGroups is
interpreted as core v1.
To match apps/v1 deployments, set the API group/version selector explicitly:
metadata:
- apiGroups:
- apps/v1
kinds:
- Deployment
To match deployments across all API groups and versions, use "*":
metadata:
- apiGroups:
- "*"
kinds:
- Deployment
Labels and Annotations
The following rule requires an approved Tenant label and validates an optional
cost-center annotation on ConfigMaps and Services:
rules:
- enforce:
action: allow
metadata:
- apiGroups:
- "*"
kinds:
- ConfigMap
- Service
labels:
corp.com/tenant:
required: true
values:
- exact:
- prod
- test
annotations:
example.corp/cost-center:
required: false
values:
- exp: "^INV-[0-9]{4}$"
exact:
- prod
- test
Label rules
Label rules are configured under metadata[].labels. Each map key is the label key to validate.
rules:
- enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
env:
required: true
values:
- exact:
- prod
- test
With this rule, a matching ConfigMap must contain metadata.labels["env"], and its value must be either prod or test.
This object is admitted:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
env: prod
data:
key: value
This object is denied because the required label is missing:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
key: value
This object is denied because the label value does not match the allow-list:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
env: stage
data:
key: value
Example rejection:
Error from server (Forbidden): error when creating "configmap.yaml": admission webhook "rules.generic.projectcapsule.dev" denied the request: metadata label "env" is required at metadata.labels["env"]
Annotation rules
Annotation rules are configured under metadata[].annotations. Each map key is the annotation key to validate.
rules:
- audience:
- kind: "Group"
name: "system:authenticated"
- kind: "Custom"
name: "CapsuleUser"
- kind: "Custom"
name: "Administrator"
- kind: "Custom"
name: "TenantOwner"
enforce:
action: allow
metadata:
- apiGroups:
- "v1"
kinds:
- Namespace
annotations:
example.corp/cost-center:
required: false
values:
- exp: "^INV-[0-9]{4}$"
# Overwrites anything, even if the user has set a value, Should be applied using SSA by the rulestatus controller, if removed also removes (one fieldmanager per rulestatus which controlles all managed metadata). Also enforce at admission
managed: "INV-10"
example.corp/cost-center-2:
values:
- exp: "II-10"
default: "{{$.tenant.spec.data.costCenter}}"
With this rule, the annotation is optional. If the object does not contain metadata.annotations["example.corp/cost-center"], Capsule ignores the rule. If the annotation is present, its value must match the configured expression.
This object is admitted because the annotation is absent and required is false:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
data:
key: value
This object is admitted because the annotation value matches:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
annotations:
example.corp/cost-center: INV-1234
data:
key: value
This object is denied because the annotation is present but does not match:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
annotations:
example.corp/cost-center: BAD-1234
data:
key: value
Default
The default field provides a value for coressponding field should no value be provided by the user. This is only applied at admission time and does not enforce the value to be present in the object.
default is meaningful for action: allow. deny and audit rules are value matchers; they do not require missing metadata to exist.
rules:
- enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
cost-center:
default: "internal"
Default values still validate against the configured values matchers. If the default value does not match any allow or deny rule, the request is denied.
Managed
Managed values set metadata to the configured value during admission,
overwriting an existing value. For background reconciliation behavior, see
managed metadata and conditions.
rules:
- enforce:
action: allow
metadata:
- apiGroups:
- "v1"
kinds:
- Namespace
annotations:
example.corp/cost-center:
required: false
values:
- exp: "^INV-[0-9]{4}$"
# Overwrites anything, even if the user has set a value, Should be applied using SSA by the rulestatus controller, if removed also removes (one fieldmanager per rulestatus which controlles all managed metadata). Also enforce at admission
managed: "INV-10"
Required
The required field controls whether the metadata key must be present.
required | Behavior |
|---|
true | For action: allow, the key must be present on matching objects. |
false | The key is optional. If it is missing, Capsule ignores it. If it is present, configured values are evaluated. |
required is meaningful for action: allow. deny and audit rules are value matchers; they do not require missing metadata to exist.
Presence-only enforcement is possible by setting required: true and omitting values:
rules:
- enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
tenant-approved:
required: true
With this rule, matching ConfigMap resources must contain the tenant-approved label, but any value is accepted.
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
tenant-approved: "true"
data:
key: value
If the label is missing, the request is denied.
Validation
The values field uses the common match expression structure with exact, exp, and optional negate.
values:
- exact:
- prod
- test
- exp: "^sandbox-[0-9]+$"
A metadata value matches if any configured value matcher matches.
exact and exp can be combined in the same matcher:
values:
- exact:
- prod
- test
exp: "^dev-[0-9]+$"
This matcher allows prod, test, and values matching ^dev-[0-9]+$.
negate: true inverts the final matcher result:
rules:
- enforce:
action: deny
metadata:
- apiVersion: "*"
kinds:
- ConfigMap
labels:
team:
values:
- exp: "^trusted-.*"
negate: true
With this rule:
team=trusted-platform is not denied by this deny-only rule.team=untrusted is denied.
If an allow-list also exists for the same metadata key, values excluded from a negated deny rule still need a matching allow rule.
An allow rule creates an allow-list for the specific metadata key it controls.
rules:
- enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
env:
required: true
values:
- exact:
- prod
- test
With this rule:
| Object label | Result |
|---|
env=prod | Allowed |
env=test | Allowed |
env=stage | Denied |
missing env | Denied because required: true |
If required is false, missing metadata is ignored:
rules:
- enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
env:
required: false
values:
- exact:
- prod
- test
With this rule:
| Object label | Result |
|---|
env=prod | Allowed |
env=test | Allowed |
env=stage | Denied |
missing env | Allowed |
Allow-list behavior is evaluated per metadata key. A matching value for one key does not satisfy another required key.
For example:
rules:
- enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
env:
required: true
values:
- exact:
- prod
team:
required: true
values:
- exact:
- platform
The object must contain both env=prod and team=platform.
Use action: deny to reject specific metadata values.
rules:
- enforce:
action: deny
metadata:
- kinds:
- ConfigMap
labels:
environment:
values:
- exact:
- deprecated
This ConfigMap is denied:
apiVersion: v1
kind: ConfigMap
metadata:
name: app-config
labels:
environment: deprecated
data:
key: value
A later matching allow rule can override an earlier deny rule:
rules:
- enforce:
action: deny
metadata:
- kinds:
- ConfigMap
labels:
environment:
values:
- exact:
- deprecated
- namespaceSelector:
matchLabels:
allow-deprecated: "true"
enforce:
action: allow
metadata:
- kinds:
- ConfigMap
labels:
environment:
required: true
values:
- exact:
- deprecated
In namespaces labeled allow-deprecated=true, environment=deprecated is admitted because the later namespace-specific allow rule matches.
Use action: audit to observe metadata usage without blocking the request.
rules:
- enforce:
action: audit
metadata:
- apiGroups:
- "*"
kinds:
- ConfigMap
- Service
labels:
example.corp/audit:
values:
- exp: "^audit-.*"
A matching object is admitted in this audit-only example, but Capsule emits an audit event and returns an admission warning.
If an allow-list also exists for the same metadata key, audit does not satisfy that allow-list. The metadata value must still match an allow rule.
Multiple resource kinds
A single metadata rule can target multiple kinds:
rules:
- enforce:
action: allow
metadata:
- apiGroups:
- "*"
kinds:
- ConfigMap
- Service
labels:
corp.com/tenant:
required: true
values:
- exact:
- prod
- test
With this rule, both matching ConfigMap and Service objects must contain corp.com/tenant=prod or corp.com/tenant=test.
Metadata enforcement supports namespaceSelector like other namespace rules.
rules:
- namespaceSelector:
matchLabels:
environment: prod
enforce:
action: allow
metadata:
- kinds:
- ConfigMap
annotations:
example.corp/approval:
required: true
values:
- exact:
- approved
This rule only applies to namespaces labeled environment=prod. In those namespaces, matching ConfigMap objects must contain example.corp/approval=approved.
Migration
The legacy metadata fields under Tenant.spec can be migrated to metadata
rules. Rules use the same policy structure for Namespaces, Pods, Services, and
other resources, and allow validation, admission defaults, and reconciled
managed values to be combined.
Migrate one resource type at a time. When old and new configuration overlap,
keep their values identical until the rule has been verified.
The legacy Namespace metadata
options map to rules as follows:
requiredMetadata becomes an allow rule with required: true and a
values expression.additionalMetadata and additionalMetadataList become managed values.
Managed values are applied during admission and reconciled on existing
Namespaces. Use default instead when a value should only be added during
admission and remain user-editable afterwards.- An
additionalMetadataList[].namespaceSelector moves to the
rules[].namespaceSelector at the same rule level. For a Namespace target,
the selector is evaluated against that Namespace’s labels. - Exact entries in
forbiddenLabels.denied and
forbiddenAnnotations.denied become deny rules that match any value for
the concrete metadata key.
Namespace must be selected explicitly. Use the concrete core v1 API and
the literal Namespace kind, especially when the rule contains managed
values:
apiGroups:
- "v1"
kinds:
- Namespace
The following rules replace the requiredMetadata,
additionalMetadataList, and exact forbidden-key examples from the legacy
page:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- name: alice
kind: User
rules:
# Require user-provided metadata and manage common metadata on every
# Namespace in the Tenant.
- enforce:
action: allow
metadata:
- apiGroups:
- "v1"
kinds:
- Namespace
labels:
env:
required: true
values:
- exp: "^(prod|test|dev)$"
projectcapsule.dev/backup:
managed: "true"
templated-label:
managed: "{{ .namespace.metadata.name }}"
annotations:
example.corp/cost-center:
required: true
values:
- exp: "^INV-[0-9]{4}$"
storagelocationtype:
managed: s3
templated-annotation:
managed: "{{ .tenant.metadata.name }}"
# Apply the baseline security profile unless the Namespace explicitly
# opts into the low-security profile.
- namespaceSelector:
matchExpressions:
- key: projectcapsule.dev/low_security_profile
operator: NotIn
values:
- "true"
enforce:
action: allow
metadata:
- apiGroups:
- "v1"
kinds:
- Namespace
labels:
pod-security.kubernetes.io/enforce:
managed: baseline
# The selectors are mutually exclusive, so only one rule manages the Pod
# Security Admission label for a Namespace.
- namespaceSelector:
matchExpressions:
- key: projectcapsule.dev/low_security_profile
operator: In
values:
- "true"
enforce:
action: allow
metadata:
- apiGroups:
- "v1"
kinds:
- Namespace
labels:
pod-security.kubernetes.io/enforce:
managed: privileged
# Deny the presence of these concrete labels or annotations, regardless
# of their values.
- enforce:
action: deny
metadata:
- apiGroups:
- "v1"
kinds:
- Namespace
labels:
foo.acme.net:
values:
- exp: ".*"
bar.acme.net:
values:
- exp: ".*"
annotations:
foo.acme.net:
values:
- exp: ".*"
bar.acme.net:
values:
- exp: ".*"
With these rules:
- Tenant owners must provide a valid
env label and cost-center annotation. - Common and templated metadata is enforced on new and existing Namespaces.
- The Pod Security Admission profile follows the Namespace selector.
foo.acme.net and bar.acme.net are denied as both labels and annotations,
including when their value is an empty string.
Legacy options without a direct equivalent
Metadata rules are keyed by concrete label and annotation names. They do not
currently provide a direct replacement for these key-wide legacy behaviors:
managedMetadataOnly: true, which rejects all user metadata not managed by
Capsule; andforbiddenLabels.deniedRegex or
forbiddenAnnotations.deniedRegex, which match metadata keys by regular
expression.
Enumerate known sensitive keys with deny rules as shown above. If the policy
must reject every unlisted key or a changing family of keys, retain the legacy
option during migration or enforce that part with another admission policy.
Value regular expressions remain supported through values[].exp; the
limitation only applies to regular expressions over metadata key names.
Rollout
- Add the equivalent rules while the legacy values are still configured.
Ensure overlapping
managed values are identical. - Check the generated
RuleStatus objects and verify both creation and update
admission with a test Namespace. - Confirm that managed metadata has been reconciled onto existing Namespaces.
- Remove the migrated
requiredMetadata, additionalMetadata,
additionalMetadataList, and exact forbidden-key entries. - Keep
managedMetadataOnly or key-regex restrictions until an alternative
policy covers them.
Use the following rule to migrate the old
spec.podOptions.additionalMetadata API to the
new spec.rules[].enforce.metadata API:
rules:
- enforce:
action: allow
metadata:
- kinds:
- Pod
annotations:
storagelocationtype:
managed: s3
labels:
projectcapsule.dev/backup:
managed: "true"
Use the following rule to migrate the old
spec.serviceOptions.additionalMetadata API
to the new spec.rules[].enforce.metadata API:
rules:
- enforce:
action: allow
metadata:
- kinds:
- Service
- Endpoints
annotations:
customer.corp/routable:
managed: "true"
labels:
customer.corp/network-tenant:
managed: "{{ .tenant.metadata.name }}"
- apiGroups:
- "discovery.k8s.io/v1"
kinds:
- EndpointSlice
annotations:
customer.corp/routable:
managed: "true"
labels:
customer.corp/network-tenant:
managed: "{{ .tenant.metadata.name }}"
Reference
This complete Tenant combines required labels, optional annotations, multiple kinds, audit rules, deny rules, and namespace-specific exceptions. Replace solar-owner with your owner identity.
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
metadata:
- apiGroups:
- "*"
kinds:
- ConfigMap
- Service
labels:
corp.com/tenant:
required: true
values:
- exact:
- prod
- test
annotations:
example.corp/cost-center:
required: false
values:
- exp: "^INV-[0-9]{4}$"
- exact:
- prod
- test
- enforce:
action: deny
metadata:
- kinds:
- ConfigMap
labels:
environment:
values:
- exact:
- deprecated
- enforce:
action: audit
metadata:
- apiGroups:
- "*"
kinds:
- ConfigMap
- Service
labels:
example.corp/audit:
values:
- exp: "^audit-.*"
- namespaceSelector:
matchLabels:
environment: prod
enforce:
action: allow
metadata:
- kinds:
- ConfigMap
annotations:
example.corp/approval:
required: true
values:
- exact:
- approved
With this configuration:
ConfigMap and Service objects must contain corp.com/tenant=prod or corp.com/tenant=test.example.corp/cost-center is optional, but if present it must match ^INV-[0-9]{4}$, prod, or test.ConfigMap objects with environment=deprecated are denied unless a later matching allow rule overrides the decision.- Objects with
example.corp/audit values matching ^audit-.* emit audit events. - In namespaces labeled
environment=prod, ConfigMap objects must also contain example.corp/approval=approved.
2.3 - Network
Enforcement for NetworkPolicies, Services, and ingress hostnames
Network enforcement covers three policy groups under spec.rules[].enforce:
Use these policies to control ingress and egress CIDR grants, Service types and addresses,
and hostnames on ingress resources. Reference provides a complete
Tenant example for each policy group.
Rules use the shared namespace selection,
audiences,
conditions, and
actions and order.
Rule Composition
Actions and order
Rules are evaluated in declaration order after namespace selection, audience,
and condition filtering. The last matching allow or deny wins for each
evaluated value. If action is omitted, it defaults to deny.
| Action | Behavior |
|---|
allow | Creates an allow-list for the evaluated matcher. Values outside the allow-list are denied unless another applicable allow or deny rule matches. |
deny | Rejects matching values. A later matching allow rule can override that decision. |
audit | Reports matches without allowing or denying the request. It never satisfies an allow-list. |
The three policy groups evaluate different values. Every applicable check must
pass; an allow decision in one group does not bypass another.
| Policy | Unit of evaluation | Audit reporting |
|---|
| NetworkPolicy ingress and egress CIDRs | Every address granted after subtracting ipBlock.except. All granted addresses must pass. | NamespaceRuleAudit Kubernetes events; no admission warnings. |
| Services | Each Service type, IP address, ExternalName hostname, and node port covered by a matcher. Type and value allow-lists are independent. | Kubernetes events and admission warnings. |
| Ingress | Each hostname on the selected resource types, including both routing and TLS hosts on an Ingress. | Kubernetes events, including reports of missing hostname fields. |
For NetworkPolicies, ordering applies per address and direction. A later allow for
10.20.0.0/17 overrides an earlier deny for 10.20.0.0/16 only within the
smaller range. Several allowed CIDRs can together cover one grant. See
Combine CIDR rules. Ingress and egress have independent
allow-lists: an allow in one direction cannot override a deny or satisfy an
allow-list in the other. When an enforcement block includes both directions,
its action and conditions apply to both.
For Services, type-specific constraints do not grant the Service type. If a
services.types allow-list is active, it must allow the type independently of
any matching address or port rule. See Combining Service Rules.
Admission scope and limits
These policies validate creation and updates in the selected namespaces.
They do not install network configuration or repair existing resources when a
rule or namespace label changes. Those changes affect subsequent admissions.
NetworkPolicies
- These policies check ingress source addresses and egress destination addresses.
They do not constrain
podSelector, namespaceSelector, or ports. - An omitted direction in the Capsule rule imposes no constraint in that
direction. Existing egress-only rules keep their behavior.
- An empty NetworkPolicy
ingress or egress list contains no grants in that
direction and passes its CIDR check. Only directions enabled by the
NetworkPolicy’s policyTypes are evaluated, including Kubernetes defaults. - A rule with omitted peers (
ingress: [{}] or egress: [{}]) grants every
IPv4 and IPv6 address. An empty peer (from: [{}] or to: [{}]) does too.
Restricting ports does not reduce this address grant. - Policy updates are checked against the new object, including metadata changes
that affect enforcement conditions. This check does not block deletion.
- CIDRs must include a prefix length and be valid IPv4 or IPv6 prefixes.
IPv4-mapped IPv6 prefixes are unsupported. Host bits are normalized to the
containing network. Each list accepts up to 64 unique CIDRs.
This is admission validation of NetworkPolicy grants. It does not itself install
a policy or enforce packet filtering. Traffic behavior still depends on the
NetworkPolicies present and the cluster’s network implementation.
Services
| Behavior | Explanation |
|---|
services.types is the type gate | Type-specific sections do not automatically grant the Service type. Include the Service type in services.types when an allow-list for Service types is active. |
| Value constraints create allow-lists for their values | If loadBalancers.cidrs, externalIPs.cidrs, externalNames.hostnames, or nodePorts.ports is configured with action: allow, non-matching values are denied. |
| External IP rules do not require a value | externalIPs.cidrs is evaluated only when a Service supplies spec.externalIPs; every supplied address is evaluated independently. |
| An empty external IP deny rule denies all external IPs | action: deny with externalIPs: {} or externalIPs.cidrs: [] denies any Service that supplies an external IP. |
loadBalancers.cidrs requires explicit values | When CIDR constraints are configured, LoadBalancer Services must set spec.loadBalancerIP or spec.loadBalancerSourceRanges. |
nodePorts.ports requires explicit node ports | When port constraints are configured, NodePort Services and LoadBalancer Services with node port allocation enabled must set spec.ports[].nodePort. |
| LoadBalancer node port allocation matters | LoadBalancer Services are subject to NodePort range checks unless spec.allocateLoadBalancerNodePorts: false is set. |
| Negation applies to the whole matcher | negate: true inverts the result of both exact and exp. |
Ingress
Ingress rules are evaluated during create and update admission. A rule only
participates when its types list contains the incoming resource kind. Other
resource types are unaffected. For example, types: [Ingress, HTTPRoute]
applies the hostname policy to those two kinds; it does not prohibit Gateways
or other omitted kinds. The action applies to the selected resources'
hostnames, not to the resource kinds themselves.
Ingress Types lists the API versions handled by Capsule. Gateway API and OpenShift
resources require their respective APIs to be installed in the cluster.
When an allow or deny hostname rule targets a resource type, its expected
hostname fields must be non-empty. Audit-only rules report missing values
without making hostnames mandatory. See Missing hostnames.
Network Policies
NetworkPolicy enforcement applies to native networking.k8s.io/v1
NetworkPolicies in the namespaces selected by the rule.
Ingress CIDRs
Configure ingress source constraints under
spec.rules[].enforce.network.policies.ingress.cidrs. Capsule checks the
addresses granted by spec.ingress[].from[].ipBlock during NetworkPolicy
creation and updates, after subtracting ipBlock.except.
This controls source addresses in NetworkPolicies. Hostname enforcement for
Ingress, Gateway API, and OpenShift Route resources is covered under
Ingress.
Disallow CIDR sources
To prevent users from granting ingress from any CIDR source, deny both address
families. This rule applies to every namespace in the Tenant:
rules:
- enforce:
action: deny
network:
policies:
ingress:
cidrs:
- 0.0.0.0/0
- ::/0
With this rule, the following NetworkPolicy is rejected:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-cidr-ingress
spec:
podSelector: {}
policyTypes: [Ingress]
ingress:
- from:
- ipBlock:
cidr: 192.0.2.0/24
A rejection identifies the source grant, field path, and matching rule:
networkPolicy ingress CIDR "192.0.2.0/24" at spec.ingress[0].from[0].ipBlock.cidr matches deny rule for CIDR 0.0.0.0/0
| NetworkPolicy ingress grant | Result with the rule above |
|---|
Any IPv4 or IPv6 ipBlock granting addresses | Denied |
ingress: [{}], including a rule with only ports | Denied: omitted from grants every source address |
from: [{}] | Denied: the empty peer grants every source address |
Peers using podSelector or namespaceSelector | Passes the CIDR check |
ingress: [] | Passes: no ingress addresses are granted |
| An IP block whose exceptions exclude every address | Passes: the block grants no addresses |
Setting ingress: {} or ingress.cidrs: [] in the Capsule rule applies no
source constraint. The rule above leaves egress unchanged; configure
network.policies.egress.cidrs separately to constrain destinations.
Allow approved source ranges
Allow ingress CIDR grants only from approved source networks in selected
namespaces, then exclude a reserved subnet with a later deny:
rules:
- namespaceSelector:
matchLabels:
network-profile: internal
enforce:
action: allow
network:
policies:
ingress:
cidrs:
- 10.20.0.0/16
- fd00:1234::/48
- namespaceSelector:
matchLabels:
network-profile: internal
enforce:
action: deny
network:
policies:
ingress:
cidrs:
- 10.20.66.0/24
| NetworkPolicy source | Exceptions | Result in a selected namespace |
|---|
10.20.1.0/24 or fd00:1234::/64 | None | Allowed |
10.20.0.0/16 | None | Denied: includes the reserved subnet |
10.20.0.0/16 | 10.20.66.0/24 | Allowed: the reserved subnet is fully excluded |
10.0.0.0/8 | None | Denied: extends outside the approved sources |
192.0.2.0/24 | None | Denied: outside the allow-list |
Omitted from | None | Denied: grants all IPv4 and IPv6 sources |
Namespaces without network-profile: internal are unaffected by these rules.
Partial exceptions leave any remaining prohibited addresses subject to denial.
Multiple allowed ranges can together cover a source grant, using the same
CIDR composition as egress. Use action: audit to report
matching ingress grants without allowing or denying them; audit-only matches
emit NamespaceRuleAudit events and do not satisfy an allow-list.
Egress CIDRs
Configure egress CIDR constraints under
spec.rules[].enforce.network.policies.egress.cidrs. Capsule checks these grants
during NetworkPolicy creation and updates.
Disallow CIDR destinations
To prevent users from granting egress to any CIDR destination, deny both address
families. 0.0.0.0/0 covers every IPv4 address; ::/0 covers every IPv6 address.
This example applies to every namespace in the Tenant:
rules:
- enforce:
action: deny
network:
policies:
egress:
cidrs:
- 0.0.0.0/0
- ::/0
With this rule, the following NetworkPolicy is rejected:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-cidr-egress
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 192.0.2.0/24
An egress rule with no to destinations, such as egress: [{}], grants access
to all destinations. Capsule checks it against both address families and rejects
it too. Restricting such a rule to particular ports does not bypass the CIDR
restriction.
Selector-based peers remain permitted by this check. For example, this policy
allows egress to matching Pods in the same namespace:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-selected-pods
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- podSelector:
matchLabels:
app: backend
Use both universal CIDRs when blocking all CIDR destinations. Denying only
0.0.0.0/0 leaves explicit IPv6 grants unaffected. Setting egress: {} or
cidrs: [] in the Capsule rule applies no CIDR constraint.
This check evaluates the addresses a policy grants, rather than the presence of
the ipBlock field. If except excludes every address in an IP block, that
block grants nothing and passes this check.
Disallow selected CIDRs
List specific CIDRs to protect only those ranges. This rule applies only to
namespaces labeled network-profile: restricted:
rules:
- namespaceSelector:
matchLabels:
network-profile: restricted
enforce:
action: deny
network:
policies:
egress:
cidrs:
- 10.20.0.0/16
- fd00:1234::/48
Matching uses address overlap, including subnets and broader ranges. Capsule
subtracts ipBlock.except before evaluating the remaining grant:
| NetworkPolicy destination | Exceptions | Result with the rule above |
|---|
10.20.4.0/24 | None | Denied: inside the protected range |
10.0.0.0/8 | None | Denied: includes the protected range |
0.0.0.0/0 | None | Denied: includes the protected range |
10.0.0.0/8 | 10.20.0.0/16 | Allowed: the protected range is fully excluded |
10.0.0.0/8 | 10.20.0.0/17 | Denied: part of the protected range remains |
10.0.0.0/8 | 10.20.0.0/17, 10.20.128.0/17 | Allowed: the exceptions together exclude the protected range |
192.0.2.0/24 | None | Allowed: no overlap |
::/0 | None | Denied: includes the protected IPv6 range |
::/0 | fd00:1234::/48 | Allowed: the protected IPv6 range is fully excluded |
For example, this policy passes the CIDR check:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: exclude-protected-range
spec:
podSelector: {}
policyTypes: [Egress]
egress:
- to:
- ipBlock:
cidr: 10.0.0.0/8
except:
- 10.20.0.0/16
A rejection identifies the destination, its field path, and the matched rule:
networkPolicy egress CIDR "10.0.0.0/8" at spec.egress[0].to[0].ipBlock.cidr matches deny rule for CIDR 10.20.0.0/16
Combine CIDR rules
For example, these two allowed ranges together cover 192.0.2.0/24:
rules:
- enforce:
action: allow
network:
policies:
egress:
cidrs:
- 192.0.2.0/25
- 192.0.2.128/25
A grant to 192.0.2.0/24 passes. A grant to 0.0.0.0/0 fails because it
includes addresses outside the allow-list. An unrestricted egress rule requires
complete coverage of both IPv4 and IPv6 address space.
To observe matching grants without blocking them, use an audit-only rule:
rules:
- enforce:
action: audit
network:
policies:
egress:
cidrs:
- 10.20.0.0/16
This rule admits a grant to 10.20.0.0/16 and emits an audit event. NetworkPolicy
CIDR audit rules report through events; they do not add admission warnings.
Services
Service enforcement is configured under spec.rules[].enforce.services.
Use Service Types to control which types can be created, then constrain
LoadBalancer addresses, External IPs,
ExternalName hostnames, and NodePort ranges.
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- NodePort
- LoadBalancer
- ExternalName
loadBalancers:
cidrs:
- 10.0.0.2/32
externalIPs:
cidrs:
- 10.20.0.0/16
externalNames:
hostnames:
- exp: ".*\\.example\\.com"
exact:
- internal.git.com
nodePorts:
ports:
- from: 30000
to: 32767
Service Types
The services.types field controls which Kubernetes Service types are allowed, denied, or audited by a rule.
Supported values are:
| Type | Description |
|---|
ClusterIP | Allows, denies, or audits Services of type ClusterIP. |
NodePort | Allows, denies, or audits Services of type NodePort. |
LoadBalancer | Allows, denies, or audits Services of type LoadBalancer. |
ExternalName | Allows, denies, or audits Services of type ExternalName. |
Allow selected types
Allow only ClusterIP Services:
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
With this rule, a ClusterIP Service is admitted:
apiVersion: v1
kind: Service
metadata:
name: internal-api
spec:
type: ClusterIP
ports:
- name: http
port: 8080
targetPort: 8080
A Service of another type, for example ExternalName, is denied because an allow-list exists for Service types and ExternalName is not listed:
apiVersion: v1
kind: Service
metadata:
name: external-api
spec:
type: ExternalName
externalName: internal.git.com
ports:
- name: http
port: 443
targetPort: 443
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: service type "ExternalName" at spec.type is not allowed by namespace rule: value did not match any allowed rule. Allowed service types: ClusterIP
Deny selected types
Deny LoadBalancer Services:
rules:
- enforce:
action: deny
services:
types:
- LoadBalancer
Allow ClusterIP and ExternalName, but deny ExternalName again for selected namespaces:
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- ExternalName
- namespaceSelector:
matchLabels:
external-services: blocked
enforce:
action: deny
services:
types:
- ExternalName
Because later matching allow or deny decisions win, namespaces labeled external-services=blocked cannot create ExternalName Services, while other matching namespaces can.
Combine types and value constraints
The services.types field is the Service capability gate. Constraint sections such as loadBalancers, externalIPs, externalNames, and nodePorts do not automatically allow a Service type by themselves. The externalIPs constraint is type-independent and applies to any allowed Service that specifies spec.externalIPs.
For example, this rule restricts LoadBalancer CIDRs, but it does not by itself allow LoadBalancer Services if another type allow-list exists that excludes LoadBalancer:
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- enforce:
action: allow
services:
loadBalancers:
cidrs:
- 10.0.0.2/32
In this example, a LoadBalancer Service is denied by the Service type allow-list because LoadBalancer is not included in services.types.
To allow and constrain LoadBalancer Services, configure both:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.0.2/32
LoadBalancer
LoadBalancer rules allow administrators to restrict the IPs and source ranges used by Services of type LoadBalancer.
LoadBalancer constraints are configured under enforce.services.loadBalancers.cidrs.
Capsule evaluates the following Service fields:
| Field | Description |
|---|
spec.loadBalancerIP | Explicit LoadBalancer IP requested by the Service. |
spec.loadBalancerSourceRanges[] | Source CIDR ranges allowed to access the LoadBalancer. |
Allow LoadBalancer Services only with a specific IP:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.0.2/32
This Service is admitted:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerIP: 10.0.0.2
ports:
- name: http
port: 80
targetPort: 8080
This Service is denied because the requested IP is outside the allowed CIDR:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerIP: 10.0.171.239
ports:
- name: http
port: 80
targetPort: 8080
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: loadBalancer CIDR "10.0.171.239" at spec.loadBalancerIP is not allowed by namespace rule: value did not match any allowed rule. Allowed CIDRs: 10.0.0.2/32
Allow a LoadBalancer IP range:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.1.0/24
The following Service is admitted because 10.0.1.44 is contained in 10.0.1.0/24:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerIP: 10.0.1.44
ports:
- name: http
port: 80
targetPort: 8080
Restrict loadBalancerSourceRanges:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.1.0/24
This Service is admitted because the requested source range is fully contained in the allowed CIDR:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerSourceRanges:
- 10.0.1.0/25
ports:
- name: http
port: 80
targetPort: 8080
This Service is denied because the requested source range is not fully contained in the allowed CIDR:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerSourceRanges:
- 10.0.1.0/23
ports:
- name: http
port: 80
targetPort: 8080
If any matching rule configures loadBalancers.cidrs, then a LoadBalancer Service must explicitly set at least one of:
spec.loadBalancerIPspec.loadBalancerSourceRanges
This is intentional. If CIDR restrictions are configured, Capsule requires the Service request to provide a value that can be evaluated.
For example, this Service is denied when loadBalancers.cidrs is configured:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
ports:
- name: http
port: 80
targetPort: 8080
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: loadBalancer service requires spec.loadBalancerIP or spec.loadBalancerSourceRanges because loadBalancer CIDR constraints are enforced by namespace rule
If no loadBalancers.cidrs constraint is configured, Capsule does not require these fields. In that case, a LoadBalancer Service can be admitted as long as the Service type itself is allowed.
Denying selected LoadBalancer CIDRs
You can also deny specific LoadBalancer CIDRs:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.0.0/8
- enforce:
action: deny
services:
loadBalancers:
cidrs:
- 10.0.66.0/24
A Service using 10.0.66.10 is denied because the later deny rule matches:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: loadBalancer CIDR "10.0.66.10" at spec.loadBalancerIP is denied by namespace rule: 10.0.66.10 is contained in 10.0.66.0/24
A later namespace-specific allow rule can override an earlier allow miss or deny decision:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.0.2/32
- namespaceSelector:
matchLabels:
environment: prod
enforce:
action: allow
services:
loadBalancers:
cidrs:
- 10.0.171.0/24
In namespaces labeled environment=prod, a Service using 10.0.171.239 is admitted. In other namespaces, it is denied because it does not match the default allowed CIDR.
External IPs
External IP rules allow administrators to restrict the addresses supplied in spec.externalIPs.
External IP constraints are configured under enforce.services.externalIPs.cidrs. They are admission rules only: Capsule does not allocate addresses, add entries to spec.externalIPs, or configure routing for them.
The constraint applies to any Service type that specifies spec.externalIPs. Individual addresses in a rule are treated as host CIDRs:
- an IPv4 address without a prefix is treated as
/32; - an IPv6 address without a prefix is treated as
/128.
Allow external IPs from selected networks:
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
externalIPs:
cidrs:
- 10.20.0.0/16
- 192.168.1.2
- 2001:db8::/32
This Service is admitted because both requested addresses match an allowed CIDR:
apiVersion: v1
kind: Service
metadata:
name: internal-api
spec:
type: ClusterIP
externalIPs:
- 10.20.1.44
- 192.168.1.2
ports:
- name: http
port: 8080
targetPort: 8080
Every entry in spec.externalIPs must satisfy the rule evaluation. The following Service is denied because 8.8.8.8 is outside the allowed CIDRs:
apiVersion: v1
kind: Service
metadata:
name: public-dns
spec:
type: ClusterIP
externalIPs:
- 10.20.1.44
- 8.8.8.8
ports:
- name: dns
protocol: UDP
port: 53
targetPort: 5353
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: external IP "8.8.8.8" at spec.externalIPs[1] is not allowed by namespace rule: value did not match any allowed rule. Allowed CIDRs: 10.20.0.0/16, 192.168.1.2, 2001:db8::/32
Unlike LoadBalancer CIDR and NodePort range constraints, an external IP rule does not require the Service to specify a value. A Service without spec.externalIPs is admitted if it satisfies the other applicable Service rules.
Denying selected external IPs
You can combine a broad allow-list with a later deny rule:
rules:
- enforce:
action: allow
services:
externalIPs:
cidrs:
- 10.20.0.0/16
- enforce:
action: deny
services:
externalIPs:
cidrs:
- 10.20.66.0/24
An address such as 10.20.1.44 is admitted, while 10.20.66.4 is denied because the later deny rule matches.
Denying all external IPs
For a deny rule, an empty cidrs array matches every value in spec.externalIPs:
rules:
- enforce:
action: deny
services:
externalIPs:
cidrs: []
The equivalent short form is:
rules:
- enforce:
action: deny
services:
externalIPs: {}
Both forms deny any Service that supplies one or more external IPs. Services that omit spec.externalIPs remain unaffected. An empty cidrs array in an allow or audit rule does not create an external IP restriction.
ExternalName
ExternalName rules allow administrators to restrict spec.externalName for Services of type ExternalName.
ExternalName constraints are configured under enforce.services.externalNames.hostnames.
Each hostname matcher uses the common match expression structure with exact, exp, and optional negate.
Allow selected ExternalName hostnames:
rules:
- enforce:
action: allow
services:
types:
- ExternalName
externalNames:
hostnames:
- exact:
- internal.git.com
- exp: ".*\\.example\\.com"
The following Services are admitted:
apiVersion: v1
kind: Service
metadata:
name: git
spec:
type: ExternalName
externalName: internal.git.com
ports:
- name: https
port: 443
targetPort: 443
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: ExternalName
externalName: api.example.com
ports:
- name: https
port: 443
targetPort: 443
A non-matching hostname is denied:
apiVersion: v1
kind: Service
metadata:
name: api
spec:
type: ExternalName
externalName: api.bad.com
ports:
- name: https
port: 443
targetPort: 443
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: externalName hostname "api.bad.com" at spec.externalName is not allowed by namespace rule: value did not match any allowed rule. Allowed hostnames: exact: internal.git.com, exp: .*\.example\.com
Use exact and exp together in the same matcher:
rules:
- enforce:
action: allow
services:
types:
- ExternalName
externalNames:
hostnames:
- exact:
- combined.internal.git.com
exp: "combined\\..*\\.example\\.com"
This matcher allows both:
combined.internal.git.com- hostnames matching
combined\\..*\\.example\\.com
Negation for ExternalName hostnames
negate: true inverts the final matcher result. This applies to both exact and exp.
Deny every ExternalName except trusted hostnames:
rules:
- enforce:
action: deny
services:
externalNames:
hostnames:
- exp: "trusted\\..*"
negate: true
- enforce:
action: allow
services:
types:
- ExternalName
externalNames:
hostnames:
- exp: "trusted\\..*"
With these rules:
trusted.api is admitted.api.example.com is denied by the negated deny rule.
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: externalName hostname "api.example.com" at spec.externalName is denied by namespace rule: "api.example.com" matched hostname rule not exp: trusted\..*
Important: when an allow-list exists for ExternalName hostnames, values excluded from a negated deny rule still need a matching allow rule. The deny rule prevents untrusted values, while the allow rule satisfies allow-list behavior for trusted values.
Namespace-specific ExternalName rules
You can use namespaceSelector to apply ExternalName restrictions only to selected namespaces:
rules:
- enforce:
action: allow
services:
types:
- ExternalName
externalNames:
hostnames:
- exp: ".*\\.example\\.com"
- namespaceSelector:
matchLabels:
external-policy: restricted
enforce:
action: deny
services:
externalNames:
hostnames:
- exact:
- blocked.example.com
In namespaces labeled external-policy=restricted, blocked.example.com is denied. Other hostnames matching .*\\.example\\.com remain allowed.
NodePort
NodePort rules allow administrators to restrict explicitly requested spec.ports[].nodePort values.
NodePort constraints are configured under enforce.services.nodePorts.ports.
Each port range contains:
| Field | Description |
|---|
from | First allowed or denied port in the range. |
to | Last allowed or denied port in the range. |
The from value must be lower than or equal to to. Equal values are valid and represent a single port.
Allow selected NodePort ranges:
rules:
- enforce:
action: allow
services:
types:
- NodePort
nodePorts:
ports:
- from: 30000
to: 30100
- from: 30500
to: 30500
This Service is admitted because 30080 is in the allowed range:
apiVersion: v1
kind: Service
metadata:
name: tenant-api
spec:
type: NodePort
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 30080
This Service is also admitted because 30500 matches the single-port range:
apiVersion: v1
kind: Service
metadata:
name: tenant-api-single
spec:
type: NodePort
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 30500
This Service is denied because 32080 is outside the allowed ranges:
apiVersion: v1
kind: Service
metadata:
name: tenant-api
spec:
type: NodePort
ports:
- name: http
port: 8080
targetPort: 8080
nodePort: 32080
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: nodePort "32080" at spec.ports[0].nodePort is not allowed by namespace rule: value did not match any allowed rule. Allowed ranges: 30000-30100, 30500
If any matching rule configures nodePorts.ports, then a NodePort Service must explicitly set spec.ports[].nodePort.
This is intentional. Kubernetes can allocate a node port automatically when the field is omitted, but the validating webhook cannot know the allocated value at admission time. To enforce configured port ranges reliably, Capsule requires the requested node port to be explicit.
The following Service is denied when nodePorts.ports is configured:
apiVersion: v1
kind: Service
metadata:
name: tenant-api
spec:
type: NodePort
ports:
- name: http
port: 8080
targetPort: 8080
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: service requires explicit spec.ports[*].nodePort because nodePort ranges are enforced by namespace rule
If no nodePorts.ports constraint is configured, Capsule does not require explicit nodePort values. In that case, a NodePort Service can be admitted as long as the Service type itself is allowed.
Denying selected NodePorts
You can allow a broad range and deny a specific port afterwards:
rules:
- enforce:
action: allow
services:
types:
- NodePort
nodePorts:
ports:
- from: 30000
to: 30100
- enforce:
action: deny
services:
nodePorts:
ports:
- from: 30090
to: 30090
A Service using 30080 is admitted. A Service using 30090 is denied because the later deny rule also matches.
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: nodePort "30090" at spec.ports[0].nodePort is denied by namespace rule: nodePort 30090 is within allowed range 30090
Although the detail says the port is within the matched range, the rule action is deny, so the request is rejected.
LoadBalancer Services and NodePorts
Kubernetes LoadBalancer Services may allocate node ports unless spec.allocateLoadBalancerNodePorts is explicitly set to false.
Therefore, NodePort range enforcement also applies to LoadBalancer Services when node port allocation is enabled.
This rule allows LoadBalancer Services, restricts the LoadBalancer IP, and restricts the allocated node port:
rules:
- enforce:
action: allow
services:
types:
- LoadBalancer
loadBalancers:
cidrs:
- 10.0.0.2/32
nodePorts:
ports:
- from: 30000
to: 30100
This Service is admitted because the LoadBalancer IP and node port are both allowed:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerIP: 10.0.0.2
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 30080
This Service is denied because the explicit node port is outside the allowed range:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerIP: 10.0.0.2
ports:
- name: http
port: 80
targetPort: 8080
nodePort: 32080
When nodePorts.ports is configured and LoadBalancer node port allocation is enabled, Capsule requires explicit spec.ports[].nodePort values:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
loadBalancerIP: 10.0.0.2
ports:
- name: http
port: 80
targetPort: 8080
Example rejection:
Error from server (Forbidden): error when creating "svc.yaml": admission webhook "services.validating.projectcapsule.dev" denied the request: service requires explicit spec.ports[*].nodePort because nodePort ranges are enforced by namespace rule
To avoid node port enforcement for a LoadBalancer Service, disable node port allocation explicitly:
apiVersion: v1
kind: Service
metadata:
name: public-api
spec:
type: LoadBalancer
allocateLoadBalancerNodePorts: false
loadBalancerIP: 10.0.0.2
ports:
- name: http
port: 80
targetPort: 8080
With allocateLoadBalancerNodePorts: false, Capsule does not require or validate spec.ports[].nodePort for that LoadBalancer Service. The Service must still satisfy any configured LoadBalancer CIDR rules.
Auditing Services
Use action: audit to observe Service usage without directly blocking the request. Audit rules emit Kubernetes events and return admission warnings, but they do not allow or deny the request.
Audit ExternalName usage:
rules:
- enforce:
action: audit
services:
types:
- ExternalName
externalNames:
hostnames:
- exp: "audit\\..*"
A matching Service is admitted in this audit-only example because no Service type or hostname allow-list is configured:
apiVersion: v1
kind: Service
metadata:
name: audited-external
spec:
type: ExternalName
externalName: audit.internal
ports:
- name: https
port: 443
targetPort: 443
If an allow-list is also configured, audit does not satisfy it:
rules:
- enforce:
action: audit
services:
externalNames:
hostnames:
- exp: "audit\\..*"
- enforce:
action: allow
services:
types:
- ExternalName
externalNames:
hostnames:
- exp: "allowed\\..*"
With these rules, audit.internal emits an audit event but is still denied because it does not match the allowed hostname rule.
Combining Service Rules
Service rules can be split across multiple rule blocks. This is useful when type permissions, external IP rules, LoadBalancer CIDR rules, hostname rules, and NodePort ranges should be managed independently.
For example:
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- ExternalName
- enforce:
action: allow
services:
externalNames:
hostnames:
- exp: ".*\\.example\\.com"
This configuration:
- allows
ClusterIP Services; - allows
ExternalName Services as a type; - allows only ExternalName hostnames matching
.*\\.example\\.com.
A Service of type ExternalName with externalName: api.example.com is admitted. A Service of type ExternalName with externalName: api.bad.com is denied by the hostname allow-list.
A later deny rule can override an earlier allow rule:
rules:
- enforce:
action: allow
services:
types:
- ExternalName
externalNames:
hostnames:
- exp: ".*\\.example\\.com"
- enforce:
action: deny
services:
externalNames:
hostnames:
- exact:
- blocked.example.com
Here, api.example.com is allowed, but blocked.example.com is denied because the later deny rule matches.
A later allow rule can override an earlier deny rule:
rules:
- enforce:
action: deny
services:
nodePorts:
ports:
- from: 30080
to: 30080
- namespaceSelector:
matchLabels:
allow-special-nodeport: "true"
enforce:
action: allow
services:
types:
- NodePort
nodePorts:
ports:
- from: 30080
to: 30080
In namespaces labeled allow-special-nodeport=true, a NodePort Service using 30080 is admitted because the namespace-specific allow rule matches later.
Ingress
Ingress enforcement is configured under spec.rules[].enforce.ingress.
Use Ingress Types to select Kubernetes Ingresses, OpenShift Routes,
and Gateway API resources, then configure Hostnames to allow,
deny, or audit their hostname values.
Ingress Types
The types list selects the resource kinds whose hostnames the rule evaluates.
Configure at least one type and one hostname expression together. A rule with
only types or only hostnames is rejected at admission.
Capsule supports the following resource types and hostname fields:
| Type | API | Evaluated fields |
|---|
Ingress | networking.k8s.io/v1 | spec.rules[].host and spec.tls[].hosts[] |
Route | route.openshift.io/v1 | spec.host |
Gateway | gateway.networking.k8s.io/v1 | spec.listeners[].hostname |
ListenerSet | gateway.networking.k8s.io/v1 | spec.listeners[].hostname |
HTTPRoute | gateway.networking.k8s.io/v1 | spec.hostnames[] |
TLSRoute | gateway.networking.k8s.io/v1 | spec.hostnames[] |
GRPCRoute | gateway.networking.k8s.io/v1 | spec.hostnames[] |
Hostnames
The hostnames list contains match expressions. Each entry supports:
| Field | Matching behavior |
|---|
exact | Match one of the listed hostname strings exactly. |
exp | Match a regular expression. Use ^ and $ to match the complete hostname. |
negate | Invert the entry’s match result. Defaults to false. |
An entry can use exact, exp, or both. When both are set, either match is
sufficient before applying negate. Multiple entries provide alternative
matches for the rule’s action.
Each hostname on a targeted resource is evaluated independently. The entire
request is denied if any hostname is denied or does not satisfy an active
allow-list. For an Ingress, this includes both routing hosts and TLS hosts, so
all values in spec.rules[].host and spec.tls[].hosts[] must satisfy the
policy.
Allow selected hostnames
The following rule allows one exact hostname and any single-label hostname
under example.com for Kubernetes Ingress and Gateway API HTTPRoute resources:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
ingress:
types:
- Ingress
- HTTPRoute
hostnames:
- exact:
- internal.example.com
- exp: "^[a-z0-9-]+\\.example\\.com$"
This Ingress is admitted because both its routing hostname and TLS hostname
match the allow-list:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: tenant-api
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tenant-api
port:
number: 8080
tls:
- hosts:
- api.example.com
secretName: tenant-api-tls
Changing either occurrence to api.example.net denies the request. A rejection
for the routing hostname includes the object path and configured allow-list:
ingress hostname "api.example.net" at spec.rules[0].host is not allowed by namespace rule: value did not match any allowed rule. Allowed hostnames: exact: internal.example.com, exp: ^[a-z0-9-]+\.example\.com$
An Ingress TLS entry is not exempt from enforcement. For example, the
following object is denied even though its routing hostname is allowed, because
legacy.example.net in spec.tls[0].hosts[0] is not allowed:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mixed-hostnames
spec:
rules:
- host: api.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: tenant-api
port:
number: 8080
tls:
- hosts:
- legacy.example.net
secretName: tenant-api-tls
Gateway API and OpenShift Route examples
A single rule can target several supported resource shapes:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
ingress:
types:
- Route
- Gateway
- ListenerSet
- HTTPRoute
- TLSRoute
- GRPCRoute
hostnames:
- exp: "^([a-z0-9-]+\\.)*apps\\.example\\.com$"
For an HTTPRoute, Capsule evaluates every entry in spec.hostnames:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: store
spec:
hostnames:
- store.apps.example.com
- checkout.apps.example.com
rules:
- backendRefs:
- name: store
port: 8080
Both values match the expression, so the request is admitted. If one hostname
does not match, Capsule denies the entire HTTPRoute.
For a Gateway or ListenerSet, Capsule evaluates the hostname of every
listener:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: tenant-gateway
spec:
gatewayClassName: shared
listeners:
- name: https
protocol: HTTPS
port: 443
hostname: gateway.apps.example.com
tls:
mode: Terminate
certificateRefs:
- name: gateway-tls
For an OpenShift Route, the same rule evaluates spec.host:
apiVersion: route.openshift.io/v1
kind: Route
metadata:
name: tenant-api
spec:
host: api.apps.example.com
to:
kind: Service
name: tenant-api
Deny hostnames and add exceptions
This Tenant allows a hostname family, denies a reserved hostname with a later
rule, and allows that hostname again in namespaces selected by the final rule:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
ingress:
types: [Ingress, HTTPRoute]
hostnames:
- exp: '^[a-z0-9-]+\.example\.com$'
- enforce:
action: deny
ingress:
types: [Ingress, HTTPRoute]
hostnames:
- exact: [admin.example.com]
- namespaceSelector:
matchLabels:
ingress-admin: "true"
enforce:
action: allow
ingress:
types: [Ingress, HTTPRoute]
hostnames:
- exact: [admin.example.com]
api.example.com is admitted in every namespace in this Tenant.
admin.example.com is admitted only in namespaces labeled
ingress-admin=true; the later deny rule blocks it in other namespaces.
Keep permission to change exception-selecting labels with the administrators
who manage the policy.
Negated matches
You can also use negation to deny every hostname outside a trusted suffix:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
ingress:
types:
- Ingress
hostnames:
- exp: "^([a-z0-9-]+\\.)*example\\.com$"
negate: true
This deny rule matches hostnames that do not match the expression. Because it
does not create an allow-list, matching example.com hostnames pass unless
another rule denies them.
Audit hostname usage
Use action: audit to observe selected hostnames without blocking them:
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: audit
ingress:
types:
- Ingress
- HTTPRoute
hostnames:
- exp: "^preview-.*\\.example\\.com$"
A matching hostname is admitted in this audit-only example, and Capsule emits a
Kubernetes event for it. If an allow-list is also configured and the hostname
does not match an allow rule, the request is still denied; the audit rule does
not grant access.
Missing hostnames
As soon as at least one allow or deny hostname rule targets a resource type,
every expected hostname field on that resource must contain a non-empty value.
Omitted, empty, and whitespace-only values are treated as missing.
Examples of missing values include:
- an
Ingress with no routing or TLS hostname, an Ingress rule without host,
or a TLS entry without hosts; - a
Route without spec.host; - a
Gateway or ListenerSet listener without hostname; - an
HTTPRoute, TLSRoute, or GRPCRoute without spec.hostnames.
For example, this Gateway is denied when an allow or deny hostname rule
targets Gateway:
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
name: hostless
spec:
gatewayClassName: shared
listeners:
- name: http
protocol: HTTP
port: 80
The rejection identifies the missing field:
hostname is required at spec.listeners[0].hostname because hostname rules target Gateway
If no ingress rule targets the resource type, Capsule does not apply this
hostname requirement.
Audit-only rules do not make hostnames mandatory. When an expected hostname is
missing, Capsule admits the request and emits an audit event that identifies the
empty field, for example:
empty hostname detected at spec.listeners[0].hostname for Gateway by audit namespace rule
If audit and allow or deny rules target the same resource type, Capsule emits
the empty-hostname audit event and enforces the non-audit rule, which denies the
request.
Reference
The following Tenant examples combine the policies in each group. Use the
configuration paths at the start of this chapter to combine them in one Tenant.
Network Policies
This complete Tenant blocks CIDR-based ingress and egress grants by default, permits approved
ranges in selected namespaces, and denies a reserved subnet in those namespaces.
Replace solar-owner with your owner identity.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: deny
network:
policies:
ingress:
cidrs:
- 0.0.0.0/0
- ::/0
egress:
cidrs:
- 0.0.0.0/0
- ::/0
- namespaceSelector:
matchLabels:
network-profile: approved-cidrs
enforce:
action: allow
network:
policies:
ingress:
cidrs:
- 10.20.0.0/16
- fd00:1234::/48
egress:
cidrs:
- 10.20.0.0/16
- fd00:1234::/48
- namespaceSelector:
matchLabels:
network-profile: approved-cidrs
enforce:
action: deny
network:
policies:
ingress:
cidrs:
- 10.20.66.0/24
egress:
cidrs:
- 10.20.66.0/24
| Namespace profile | NetworkPolicy grant in either direction | Result |
|---|
Without network-profile: approved-cidrs | Any CIDR granting addresses | Denied |
network-profile: approved-cidrs | 10.20.1.0/24 or fd00:1234::/64 | Allowed by the later allow rule |
network-profile: approved-cidrs | 10.20.66.0/24 | Denied by the final deny rule |
network-profile: approved-cidrs | 10.20.0.0/16 without exceptions | Denied: includes the reserved subnet |
network-profile: approved-cidrs | 10.20.0.0/16 except 10.20.66.0/24 | Allowed |
network-profile: approved-cidrs | 192.0.2.0/24 | Denied: outside the approved ranges |
| Any namespace in this Tenant | Ingress or egress rule with omitted peers | Denied |
| Any namespace in this Tenant | Selector-based peers or no grants in that direction | Passes the CIDR check |
Other applicable enforcement rules must also pass. Rules from this Tenant do not
apply to namespaces belonging to another Tenant.
Services
This complete Tenant combines type enforcement, external IP restrictions, LoadBalancer CIDR restrictions, ExternalName hostname restrictions, NodePort range restrictions, audit rules, and namespace-specific exceptions. Replace solar-owner with your owner identity.
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- NodePort
- LoadBalancer
- ExternalName
- enforce:
action: allow
services:
externalIPs:
cidrs:
- 10.20.0.0/16
- 192.168.1.2
- enforce:
action: allow
services:
loadBalancers:
cidrs:
- 10.0.0.2/32
- 10.0.1.0/24
- enforce:
action: allow
services:
externalNames:
hostnames:
- exact:
- internal.git.com
- exp: ".*\\.example\\.com"
- enforce:
action: allow
services:
nodePorts:
ports:
- from: 30000
to: 30100
- from: 30500
to: 30500
- enforce:
action: deny
services:
nodePorts:
ports:
- from: 30090
to: 30090
- enforce:
action: deny
services:
externalIPs:
cidrs:
- 10.20.66.0/24
- enforce:
action: deny
services:
loadBalancers:
cidrs:
- 10.0.66.0/24
- enforce:
action: audit
services:
externalNames:
hostnames:
- exp: "audit\\..*"
- namespaceSelector:
matchLabels:
environment: prod
enforce:
action: allow
services:
loadBalancers:
cidrs:
- 10.0.171.0/24
With this configuration:
ClusterIP, NodePort, LoadBalancer, and ExternalName Services are valid Service types.- External IPs must be contained in
10.20.0.0/16 or match 192.168.1.2. - External IPs in
10.20.66.0/24 are denied even though they are inside the broader allowed range. - LoadBalancer IPs must be contained in
10.0.0.2/32 or 10.0.1.0/24. - Namespaces labeled
environment=prod can also use LoadBalancer IPs in 10.0.171.0/24. - ExternalName hostnames must be
internal.git.com or match .*\\.example\\.com. - Explicit node ports must be in
30000-30100 or equal to 30500. - Node port
30090 is denied even though it is inside the broader allowed range. - ExternalName hostnames matching
audit\\..* emit audit events and warnings. - Audit matches do not allow values that fail the allow-list.
Ingress
This complete Tenant allows a hostname family, denies a reserved hostname,
audits preview hostnames, and grants a namespace-specific exception. Replace
solar-owner with your owner identity. It covers every resource kind listed
under Ingress Targets, including Gateway API and OpenShift resources when those
APIs are installed.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- enforce:
action: allow
ingress:
types: [Ingress, Route, Gateway, ListenerSet, HTTPRoute, TLSRoute, GRPCRoute]
hostnames:
- exact: [internal.example.com]
- exp: '^[a-z0-9-]+\.solar\.example\.com$'
- enforce:
action: deny
ingress:
types: [Ingress, Route, Gateway, ListenerSet, HTTPRoute, TLSRoute, GRPCRoute]
hostnames:
- exact: [admin.solar.example.com]
- enforce:
action: audit
ingress:
types: [Ingress, Route, Gateway, ListenerSet, HTTPRoute, TLSRoute, GRPCRoute]
hostnames:
- exp: '^preview-[a-z0-9-]+\.solar\.example\.com$'
- namespaceSelector:
matchLabels:
ingress-admin: "true"
enforce:
action: allow
ingress:
types: [Ingress, Route, Gateway, ListenerSet, HTTPRoute, TLSRoute, GRPCRoute]
hostnames:
- exact: [admin.solar.example.com]
Each routing, TLS, or listener hostname is checked independently:
| Hostname | Namespace | Result |
|---|
api.solar.example.com or internal.example.com | Any namespace in this Tenant | Allowed |
admin.solar.example.com | Without ingress-admin: "true" | Denied by the later deny rule |
admin.solar.example.com | With ingress-admin: "true" | Allowed by the final exception |
preview-api.solar.example.com | Any namespace in this Tenant | Allowed and audited |
api.example.net | Any namespace in this Tenant | Denied because it does not match the allow-list |
| A required hostname is missing | Any namespace in this Tenant | Denied because non-audit hostname rules target the resource type |
Every hostname on a resource must pass. For example, an Ingress with an allowed
routing host and a disallowed TLS host is denied. If you narrow the types
lists, omitted resource kinds remain unaffected by these rules.
2.4 - Storage
Default PersistentVolume isolation and additional volume access through namespace rules.
Storage enforcement is configured under spec.rules[].enforce.storage.
Persistent Volumes covers default Tenant isolation and
additional volume access for PVs without a Tenant label.
Rules use the shared namespace selection,
audiences,
conditions, and
actions and order.
See Reference for a complete Tenant example.
Rule Composition
Actions and order
Storage rules use the existing ordered action evaluation, but affect only
additional access to PVs without a Tenant label:
| Action | Effect on additional access |
|---|
allow | Grants access when the selector, audience, namespace selection, and all conditions match. |
deny | Rejects the exception when it is the last matching allow/deny rule. This is the default action. |
audit | Emits a Kubernetes event for a match; never grants access. |
The last matching allow or deny wins. If none grants access, the existing
missing-Tenant-label rejection remains. An audit-only rule therefore does not
permit an unlabeled PV. Other applicable admission checks must still pass after
a storage rule allows access.
Same-Tenant volume access remains available without these rules. A matching
rule cannot authorize another Tenant’s labeled PV or a PV whose Tenant label
has an empty value. See Ownership and label protection.
Admission scope and limits
- Checks apply to PVC creation and updates of an unbound PVC with an explicit
spec.volumeName, including the PATCH that sets it during a restore. - The referenced PV must exist. A PV being deleted cannot receive an additional
grant.
- PVCs without
spec.volumeName do not use these additional rules. They do not
change dynamic provisioning or how Kubernetes selects a PV. - Already-bound PVC updates keep their existing behavior. These rules do not
reauthorize or revoke an established binding.
- The grant authorizes the PVC admission request. It does not rewrite the PV’s
claimRef, restore labels, move data, or complete binding. RBAC, StorageClass
restrictions, other admission policies, and Kubernetes binding requirements
continue to apply.
PVC selector mutation runs only on creation. All updates preserve the existing
selector, including updates while the claim is still Pending. The PV ownership
check still applies when an unbound PVC is updated to reference a volume. See
PVC selectors and dynamic provisioning.
Persistent Volumes
Capsule isolates PersistentVolumes (PVs) between Tenants by default. Storage rules
let administrators grant additional access to selected PVs that do not yet carry
Capsule’s Tenant ownership label. Configure these additional grants under
spec.rules[].enforce.storage.volumes.
This supports workflows that patch a destination PersistentVolumeClaim (PVC)
before changing the restored PV’s claim reference from a temporary claim to the
destination claim. The default isolation
described below continues to apply.
Default PersistentVolume isolation
Tenant owners can create PVCs using the
StorageClasses available to their Tenant.
When a PV’s spec.claimRef points to a PVC in a Tenant namespace, Capsule’s PV
controller labels the volume with capsule.clastix.io/tenant: <tenant-name>.
The controller resolves the Tenant from the claim’s namespace; PVs themselves
are cluster-scoped. This behavior does not require any storage.volumes rules.
For example, a PV claimed in solar-production, a namespace belonging to the
solar Tenant, has the following relevant fields. This is an excerpt of the
existing PV, not a complete volume definition:
apiVersion: v1
kind: PersistentVolume
metadata:
name: solar-data
labels:
capsule.clastix.io/tenant: solar
spec:
storageClassName: standard
persistentVolumeReclaimPolicy: Retain
claimRef:
apiVersion: v1
kind: PersistentVolumeClaim
namespace: solar-production
name: data
Reusing retained volumes
What happens after a PVC is deleted depends on the PV’s reclaim policy. With
Retain, the volume and its data remain for manual reclamation; it is not
automatically ready for another claim. Follow the
Kubernetes reclamation procedure
before reusing the storage.
Once the PV is available for reuse, Capsule permits claims from namespaces
belonging to solar to reference it. Kubernetes must still be able to bind the
claim: the requested capacity, access modes, StorageClass, and other binding
requirements must be satisfied. Deleting the old PVC does not remove the PV’s
Tenant label or authorize a different Tenant to use it.
A claim from green-energy, a namespace belonging to another Tenant named
green, cannot reference solar-data:
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data
namespace: green-energy
spec:
storageClassName: standard
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 3Gi
volumeName: solar-data
Capsule rejects that request with a cross-Tenant mount error:
admission webhook "pvc.validating.projectcapsule.dev" denied the request: Preventing a cross-tenant mount for PersistentVolume solar-data
PVC selectors and dynamic provisioning
On PVC creation, Capsule adds the owning Tenant requirement to an existing
spec.selector, or creates a selector when spec.volumeName is set. It preserves
unrelated selector requirements and replaces conflicting requirements for
capsule.clastix.io/tenant with the correct Tenant value.
A new PVC with neither spec.selector nor spec.volumeName receives no Tenant
selector, preserving dynamic provisioning. On update, Capsule leaves the
selector unchanged, even while the PVC is Pending: Kubernetes treats the
selector as immutable after creation. This allows a restore component to set
spec.volumeName without Capsule adding a selector change that Kubernetes
would reject.
For an explicit spec.volumeName, Capsule also checks the referenced PV’s
Tenant label during PVC admission. A PV labeled for the same Tenant passes this
ownership check; another Tenant’s PV is rejected. Without an additional grant,
a PV with no Tenant label is also rejected. The rules below extend this last
case while retaining the default ownership checks.
Additional volume access
Each entry in storage.volumes has a required Kubernetes label selector and an
optional name for admission and audit messages. The selector matches labels on
the PV named by the PVC’s spec.volumeName.
rules:
- namespaceSelector:
matchLabels:
storage-profile: shared-imports
enforce:
action: allow
storage:
volumes:
- name: approved-imports
selector:
matchLabels:
storage.example.com/pool: imports
matchExpressions:
- key: storage.example.com/quarantined
operator: DoesNotExist
Place this rules block under Tenant.spec. It grants additional access to PVs
with the imports pool label and no quarantine label, only from selected
namespaces. Without an audience, the rule applies to every caller in that scope.
- All requirements in one selector must match. Entries in
volumes are
alternatives: any matching entry matches that enforcement rule. selector: {} explicitly matches every eligible PV, including a PV with no
labels. Use conditions and an audience to scope a restore exception.- Omitting
selector, or setting it to null, is invalid. An omitted or empty
volumes list grants no additional access. - A rule supports at most 64 volume entries. Optional names must be unique
within the list and use DNS labels of up to 63 characters.
Ownership and label protection
Capsule checks the PV’s capsule.clastix.io/tenant label before evaluating any
additional grant:
| PV ownership label | Volume access decision |
|---|
| Matches the destination Tenant | Existing access is retained, even if no storage rule matches or a storage rule says deny. |
| Names another Tenant | Denied; no selector or CEL condition can override ownership. |
| Key exists with an empty value | Denied; this is not treated as an absent label. |
| Key is absent | Requires an applicable matching allow rule. |
Selectors only read PV labels. They never add, remove, or overwrite PV labels,
and are never copied into PVC.spec.selector. Labels supplied on the PVC cannot
satisfy a PV selector. Capsule’s existing PVC Tenant selector enforcement remains
in place.
Labels used for authorization must be controlled by administrators or trusted
storage components. Tenant actors must not receive RBAC permissions to edit PVs
or the rules granting access. A name prefix or a label alone does not establish
Tenant ownership.
Conditions
Use enforce.conditions to inspect both the PVC being admitted and the referenced
PV. There is no separate conditions field on a volume entry.
| CEL variable | Value during additional volume access evaluation |
|---|
object | The incoming PVC, including its destination namespace and spec.volumeName. |
request | Admission metadata, including operation and authenticated caller. |
volume | A read-only snapshot of the existing PV referenced by spec.volumeName. |
Check optional fields with has(...), for example
volume != null && has(volume.spec.claimRef). Outside additional volume access
evaluation, volume is null, including in metadata conditions on a PVC. Keep
conditions that depend on volume in a separate storage enforcement block when
other policies need independent conditions.
All conditions must be true. A false condition skips the enforcement block; it
does not grant access. Evaluation errors reject admission unless another
condition in that block is false, following the normal
condition evaluation rules.
Reference
Restore through a temporary namespace
For a restore workflow using temporary claims in openshift-adp, the destination
PVC may need to reference a PV whose claimRef still names a temporary claim.
PVs are cluster-scoped; the namespace in spec.claimRef belongs to that PVC.
If that PV has no Tenant label, the default Capsule ownership check rejects the
PVC patch. An additional storage rule can authorize this handoff without
relaxing access to another Tenant’s labeled PVs.
The following Tenant grants the exception in namespaces labeled
storage-profile: restore. It allows service accounts from openshift-adp to
update a PVC only when the PV’s temporary claim is in that namespace and its
name starts with the destination namespace followed by -.
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
owners:
- kind: User
name: solar-owner
rules:
- namespaceSelector:
matchLabels:
storage-profile: restore
audience:
- kind: Group
name: system:serviceaccounts:openshift-adp
enforce:
action: allow
conditions:
- name: restore-handoff
expression: >-
request.operation == 'UPDATE' &&
volume != null &&
has(volume.spec.claimRef) &&
volume.spec.claimRef.namespace == 'openshift-adp' &&
volume.spec.claimRef.name.startsWith(object.metadata.namespace + '-')
storage:
volumes:
- name: temporary-restore-volume
selector: {}
Replace the owner, staging namespace, and claim naming condition with values
from your restore workflow. This example grants the audience to all service
accounts in openshift-adp. To scope it to one restoring component, use an
audience with kind: ServiceAccount and its full
system:serviceaccount:<namespace>:<name> username. Match the identity making
the PVC patch, which may differ from the user who submitted a NonAdminRestore.
The prefix check is a naming convention, not proof of ownership. The trusted
restore component remains responsible for handing the correct volume to the
destination claim. Use the actual temporary claim naming scheme; Capsule does
not derive it from a NonAdminRestore object.
If the temporary PV carries an administrator-controlled pool label, replace
selector: {} with a matchLabels selector to narrow the grant further. Capsule
does not add that label on your behalf.
After the PVC patch is admitted, the restore component can finish the PV
handoff. Capsule’s existing PV labeling behavior still applies when the claim
reference points to a Tenant namespace. The rule itself does not change labels
or claim references.
| Request or PV state | Result with this example |
|---|
| Trusted restore caller, selected namespace, matching temporary claim, no Tenant label | Additional access is allowed. |
| Tenant owner without the restore audience | No additional access. |
Namespace without storage-profile: restore | No additional access. |
| PVC creation rather than update | No additional access; the condition requires UPDATE. |
| Missing claim reference or a different temporary namespace/name prefix | No additional access. |
| PV labeled for another Tenant | Denied regardless of the rule. |
PV already labeled for solar | Existing access is retained; other checks still apply. |