Enforcement
Namespace rules can enforce admission behavior for selected resources in Tenant namespaces. Each enforce block can define an action and one or more matchers.
Rules are evaluated in declaration order. If multiple allow or deny rules match the same request, the last matching allow or deny rule wins. If at least one allow rule is configured for a workload 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, but all matching audit rules emit Kubernetes events and add admission warnings.
Action
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 | Emits a Kubernetes event and returns an admission warning when it matches. It does not allow or deny the request. |
If action is omitted, Capsule treats the rule as deny.
Allow-list behavior is evaluated per workload 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.
Match expressions
Several workload rule types use a common match expression 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 | A list of exact values. The matcher succeeds when the evaluated value equals one of the listed values. |
exp | A regular expression matched against the evaluated value. |
negate | Negates the final match result. This applies to both exact and exp. |
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/.*.
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:
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.
Workloads
Enforcement for workloads mainly targets Pods and their associated resources.
Workload enforcement is configured under spec.rules[].enforce.workloads. Each rule can define an action, optional workload targets, and one or more workload matchers such as registry match expressions, scheduler match expressions, or QoS classes.
QoS Classes
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:
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:
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.
Scheduler Names
Scheduler enforcement allows administrators to allow, deny, or audit Pods based on spec.schedulerName.
Scheduler rules are configured under enforce.workloads.schedulers. Each scheduler matcher uses the common match expression structure with exact, exp, and optional negate.
Capsule evaluates spec.schedulerName during Pod create and update admission. If spec.schedulerName is empty or omitted, scheduler enforcement does not match it and does not normalize it to default-scheduler.
Allow only selected explicit schedulers:
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: allow
workloads:
schedulers:
- exact:
- tenant-scheduler
- batch-scheduler
A Pod using one of the listed schedulers is admitted:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-by-tenant
spec:
schedulerName: tenant-scheduler
containers:
- name: shell
image: harbor/platform/debian:latest
command: ["sleep", "infinity"]
A Pod using another explicit scheduler is denied:
apiVersion: v1
kind: Pod
metadata:
name: scheduled-by-other
spec:
schedulerName: other-scheduler
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: scheduler "other-scheduler" at spec.schedulerName is not allowed by namespace rule
Use a regular expression to allow a scheduler family:
rules:
- enforce:
action: allow
workloads:
schedulers:
- exp: "tenant-[a-z0-9-]+"
Use exact and exp together to allow a fixed list plus a pattern:
rules:
- enforce:
action: allow
workloads:
schedulers:
- exact:
- default-scheduler
- batch-scheduler
exp: "tenant-[a-z0-9-]+"
This matcher allows default-scheduler, batch-scheduler, and scheduler names matching tenant-[a-z0-9-]+.
Deny a known unsafe scheduler:
rules:
- enforce:
action: deny
workloads:
schedulers:
- exact:
- unsafe-scheduler
Use negate: true to deny every explicit scheduler except a trusted set:
rules:
- enforce:
action: deny
workloads:
schedulers:
- exact:
- default-scheduler
- tenant-scheduler
negate: true
Because negate applies to exact, this rule matches any explicit scheduler name except default-scheduler and tenant-scheduler.
Audit usage of a custom scheduler:
rules:
- enforce:
action: audit
workloads:
schedulers:
- exact:
- custom-scheduler
A matching Pod is admitted in this audit-only example, but Capsule emits an audit event and returns an admission warning. If a scheduler allow-list is also configured and the scheduler name is not allowed, the Pod is denied while the audit event is still emitted.
OCI 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 workload-level targets field under enforce.workloads.targets controls which Pod image references are validated.
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:
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.
Registry exact match examples
Use exact when you want to allow or deny a fixed set of complete image references:
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:
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/*.
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.
---
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:
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.0is 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:latestis denied because it does not matchtrusted/.*, so the negated matcher evaluates to true.
Negation also applies to exact values:
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:
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.
Targets
The targets field defines which parts of a workload a rule applies to.
Targets are configured under enforce.workloads.targets and are authoritative for target-aware workload enforcement. Registry entries do not define their own validation targets.
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
registries:
- exp: "harbor/customer/.*"
If targets is omitted or empty, the rule applies to all workload targets supported by the matching hook.
Supported workload targets are:
| Target | Description |
|---|---|
pod/initcontainers | Applies to images used by spec.initContainers. |
pod/containers | Applies to images used by spec.containers. |
pod/ephemeralcontainers | Applies to images used by spec.ephemeralContainers. |
pod/volumes | Applies to image volumes under spec.volumes[].image. |
Targets are currently used only by a subset of workload hooks. For example, the registry enforcement hook uses targets to decide which Pod image references are validated. Other hooks may ignore targets until they explicitly support target-aware enforcement.
Examples:
rules:
- enforce:
action: deny
workloads:
targets:
- pod/initcontainers
registries:
- exp: "harbor/init-only/.*"
This rule denies matching images only when they are used by initContainers. The same image reference is not denied when used by regular containers, ephemeral containers, or image volumes unless another rule matches those targets.
rules:
- enforce:
action: deny
workloads:
targets:
- pod/containers
- pod/ephemeralcontainers
registries:
- exp: "debug/.*"
This rule applies to regular containers and ephemeral containers, but not to init containers or image volume
Services
Service enforcement allows administrators to allow, deny, or audit Kubernetes Service resources in Tenant namespaces.
Service rules are configured under spec.rules[].enforce.services. Each rule can define an action, a list of allowed or denied Service types, and optional type-specific constraints for LoadBalancer, ExternalName, and NodePort Services.
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- NodePort
- LoadBalancer
- ExternalName
loadBalancers:
cidrs:
- 10.0.0.2/32
externalNames:
hostnames:
- exp: ".*\\.example\\.com"
exact:
- internal.git.com
nodePorts:
ports:
- from: 30000
to: 32767
Service enforcement follows the same action and precedence model as other namespace rules:
allowcreates an allow-list for the evaluated Service value.denydenies matching values.auditemits events and admission warnings but does not allow or deny the request.- If multiple
allowordenyrules match the same value, the last matching allow or deny rule wins. - If at least one
allowrule exists for a Service matcher and no allow or deny rule matches the evaluated value, Capsule denies the request. - Audit rules never satisfy allow-list behavior.
Service rules are evaluated during Service create and update admission.
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 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 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.
Important caveats for services.types
The services.types field is the Service capability gate. Type-specific sections such as loadBalancers, externalNames, and nodePorts do not automatically allow a Service type by themselves.
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
Required LoadBalancer fields when CIDRs are configured
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.
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.apiis admitted.api.example.comis 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
Required explicit nodePort when ranges are configured
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.
Advanced
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, 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
ClusterIPServices; - allows
ExternalNameServices 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.
Service Rule Caveats
Service enforcement is intentionally explicit. Keep the following behavior in mind:
| 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. |
| Type-specific constraints create allow-lists for their values | If loadBalancers.cidrs, externalNames.hostnames, or nodePorts.ports is configured with action: allow, non-matching values are denied. |
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. |
| Audit does not allow | A matching audit rule emits events and warnings but does not satisfy an allow-list. |
| Last matching allow or deny wins | Later matching allow or deny rules override earlier matching allow or deny rules. |
| Negation applies to the whole matcher | negate: true inverts the result of both exact and exp. |
| Namespace selectors affect projected rules | Rules with namespaceSelector only apply to namespaces matching the selector. |
Complete Service Enforcement Example
The following example combines type enforcement, LoadBalancer CIDR restrictions, ExternalName hostname restrictions, NodePort range restrictions, audit rules, and namespace-specific exceptions:
---
apiVersion: capsule.clastix.io/v1beta2
kind: Tenant
metadata:
name: solar
spec:
...
rules:
- enforce:
action: allow
services:
types:
- ClusterIP
- NodePort
- LoadBalancer
- ExternalName
- 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:
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, andExternalNameServices are valid Service types.- LoadBalancer IPs must be contained in
10.0.0.2/32or10.0.1.0/24. - Namespaces labeled
environment=prodcan also use LoadBalancer IPs in10.0.171.0/24. - ExternalName hostnames must be
internal.git.comor match.*\\.example\\.com. - Explicit node ports must be in
30000-30100or equal to30500. - Node port
30090is 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.