Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
ab1eb8e
feat(policy-evaluator): Add HostCapabilitiesAllowList{}
viccuad Apr 8, 2026
c275e0a
feat(policy-evaluator): Consume HostCapabilitiesAllowList{} in Evalua…
viccuad Apr 9, 2026
e413ea1
test(policy-evaluator): Add can_access_host_capability test to evalua…
viccuad Apr 9, 2026
e6c7a5f
test(policy-evaluator): Add new test_host_capabilities_allow_list
viccuad Apr 9, 2026
77aafba
chore(policy-evaluator): Add stubs for PolicyGroupEvaluator{}
viccuad Apr 9, 2026
5b7423f
chore(kwctl): Add stubs for Evaluator{}
viccuad Apr 9, 2026
f0879be
feat(policy-evaluator): Check permissions on host_callback()
viccuad Apr 9, 2026
74926ef
test(policy-evaluator): Add unit tests to callback.rs
viccuad Apr 13, 2026
c96eba9
build(policy-evaluator): Bump version to 0.32.0
viccuad Apr 9, 2026
ab4cc6d
chore(kwctl): Wrap PolicyEvaluator in Box to fix large_enum_variant w…
viccuad Apr 10, 2026
fd54517
feat(policy-server): Support `hostCapabilities` field in policies.yml…
viccuad Apr 10, 2026
c3060a6
chore(policy-server): Stub host_capabilities_allow_list for policy gr…
viccuad Apr 10, 2026
c6d9d7f
test(policy-server): Adapt testcases
viccuad Apr 13, 2026
792b3c9
test(policy-server): Add 2 integration testcases for host_capabilities
viccuad Apr 13, 2026
a4a816f
ci: Allow runs on private forks
viccuad Apr 14, 2026
5d4115f
refactor(test): Add TryFrom to HostCapabilitiesAllowList
viccuad Apr 15, 2026
d38aa8f
refactor(test): Use testcase names in EvaluationContext
viccuad Apr 15, 2026
c2e5076
feat(controller): Add namespacedPoliciesCapabilities to PolicyServerS…
viccuad Apr 14, 2026
eb47db1
feat(controller): Validate spec.namespacedPoliciesCapabilities
viccuad Apr 14, 2026
11caa9c
feat(controller): Add HostCapabilities field to policyServerConfigEnt…
viccuad Apr 14, 2026
523c84d
chore(controller): Stub policygroup members config struct
viccuad Apr 14, 2026
c539880
feat(helm-charts): Update PolicyServer CRD with NamespacedPoliciesCap…
viccuad Apr 14, 2026
35b7a47
feat(helm-charts): Add new .Values.policyServer.namespacedPoliciesCap…
viccuad Apr 14, 2026
3e065d1
refactor(policy-evaluator): Check capability gating once
viccuad Apr 15, 2026
5a8b972
refactor(policy-evaluator): Use if-return to check for callback binding
viccuad Apr 15, 2026
9087b92
feat(controller): propagate host capabilities to policy group members
flavio Apr 16, 2026
3d7c385
feat(policy-evaluator): add host capabilities support to group policies
flavio Apr 16, 2026
e562da0
feat(policy-server): honor per-member host capabilities in group poli…
flavio Apr 16, 2026
58b8bed
feat(kwctl): add --allowed-host-capabilities flag to run/bench
flavio Apr 16, 2026
3258f87
refactor(policy-evaluator): remove default trait from HostCapabilitie…
flavio Apr 16, 2026
a00dedc
refactor(policy-evaluator): improve HostCapabilitiesAllowList design
flavio Apr 16, 2026
3d9f63f
refactor(policy-evaluator): drop pattern field from HostCapabilitiesP…
flavio Apr 16, 2026
ba6254a
test(controller): assert namespaced group members get empty host caps…
flavio Apr 16, 2026
8c7e7c4
docs(kwctl): Regenerate kwctl cli docs
viccuad Apr 16, 2026
c6b36fe
refactor(policy-evaluator): rename HostCapabilitiesAllowList to HostC…
flavio Apr 16, 2026
18ed32b
refactor(kwctl): move comment to proper location
flavio Apr 16, 2026
5a5e1b2
refactor(policy-evaluator): re-add Default trait to HostCapabilities …
flavio Apr 16, 2026
3d686a6
refactor(policy-evaluator): replace allow_all/deny_all constructors w…
flavio Apr 16, 2026
5c68b3e
test(policy-evaluator): DRY host_capabilities tests with rstest
flavio Apr 16, 2026
8d1b6b8
refactor(rust): get rid of some useless clone
flavio Apr 16, 2026
cdefbc7
test(controller): expand the test case covering ConfigMap generation
flavio Apr 16, 2026
e844c80
refactor(rust): reshuffle imports
flavio Apr 16, 2026
ffe076e
refactor(controller): rename variable
flavio Apr 16, 2026
2674a08
test(controller): extend test coverage
flavio Apr 20, 2026
e9486a7
test(controller): DRY controller tests
flavio Apr 20, 2026
ddce9fd
chart: fix namespacedPoliciesCapabilities schema validation
flavio Apr 20, 2026
d9a714a
chore: remove wrong comment
flavio Apr 20, 2026
16b47c6
feat(policy-evaluator): validate host capabilities
flavio Apr 20, 2026
73b1cad
refactor(policy-evaluator): simplify HostCapabilities::new
flavio Apr 20, 2026
ec5e72b
refactor(policy-evaluator): Use strip.suffix, fixes clippy
viccuad Apr 20, 2026
6ad04c4
feat(kwctl): handle host capabilities at annotation time
flavio Apr 20, 2026
e79091f
chore(policy-evaluator): Add field to test fixtures
viccuad Apr 20, 2026
f21a9a7
refactor(controller): Default to "*" if namespacedPoliciesCapabilitie…
viccuad Apr 22, 2026
9129dca
test(controller): Add testcases for namespaced and clusterwide policies
viccuad Apr 22, 2026
a4430c8
chore(docs,chart): Refresh generated CRD files and docs
viccuad Apr 23, 2026
92f2322
docs: Correct PolicyServer godoc and regenerate docs
viccuad Apr 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/build-kwctl.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ jobs:
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
if: ${{ !inputs.build_only }}
Expand Down Expand Up @@ -197,6 +198,7 @@ jobs:
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down Expand Up @@ -286,6 +288,7 @@ jobs:
permissions:
id-token: write
attestations: write
contents: read
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
Expand Down
25 changes: 24 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

37 changes: 22 additions & 15 deletions api/policies/v1/factories.go
Original file line number Diff line number Diff line change
Expand Up @@ -479,13 +479,14 @@ func (f *ClusterAdmissionPolicyGroupFactory) Build() *ClusterAdmissionPolicyGrou
}

type PolicyServerBuilder struct {
name string
minAvailable *intstr.IntOrString
maxUnavailable *intstr.IntOrString
imagePullSecret string
limits corev1.ResourceList
requests corev1.ResourceList
sigstoreTrustConfigMap string
name string
minAvailable *intstr.IntOrString
maxUnavailable *intstr.IntOrString
imagePullSecret string
limits corev1.ResourceList
requests corev1.ResourceList
sigstoreTrustConfigMap string
namespacedPoliciesCapabilities []string
}

func NewPolicyServerFactory() *PolicyServerBuilder {
Expand Down Expand Up @@ -529,6 +530,11 @@ func (f *PolicyServerBuilder) WithRequests(requests corev1.ResourceList) *Policy
return f
}

func (f *PolicyServerBuilder) WithNamespacedPoliciesCapabilities(capabilities []string) *PolicyServerBuilder {
f.namespacedPoliciesCapabilities = capabilities
return f
}

func (f *PolicyServerBuilder) Build() *PolicyServer {
policyServer := PolicyServer{
ObjectMeta: metav1.ObjectMeta{
Expand All @@ -544,14 +550,15 @@ func (f *PolicyServerBuilder) Build() *PolicyServer {
},
},
Spec: PolicyServerSpec{
Image: policyServerRepository() + ":" + policyServerVersion(),
Replicas: 1,
MinAvailable: f.minAvailable,
MaxUnavailable: f.maxUnavailable,
ImagePullSecret: f.imagePullSecret,
Limits: f.limits,
Requests: f.requests,
SigstoreTrustConfig: f.sigstoreTrustConfigMap,
Image: policyServerRepository() + ":" + policyServerVersion(),
Replicas: 1,
MinAvailable: f.minAvailable,
MaxUnavailable: f.maxUnavailable,
ImagePullSecret: f.imagePullSecret,
Limits: f.limits,
Requests: f.requests,
SigstoreTrustConfig: f.sigstoreTrustConfigMap,
NamespacedPoliciesCapabilities: f.namespacedPoliciesCapabilities,
Env: []corev1.EnvVar{
{
Name: "KUBEWARDEN_LOG_LEVEL",
Expand Down
11 changes: 11 additions & 0 deletions api/policies/v1/policyserver_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,17 @@ type PolicyServerSpec struct {
// remain unchanged, but new pods that reference it cannot be created.
// +optional
PriorityClassName string `json:"priorityClassName,omitempty"`

// NamespacedPoliciesCapabilities lists host capability API calls allowed
// for namespaced policies running on this PolicyServer. When not set,
// no host capabilities are granted to namespaced policies.
// Supported wildcard patterns:
// - "*": allow all host capabilities
// - "category/*": allow all capabilities in a category (e.g. "oci/*")
// - "category/version/*": allow all capabilities of a specific version (e.g. "oci/v1/*")
// - Specific capability paths (e.g. "oci/v1/verify", "net/v1/dns_lookup_host")
// +optional
NamespacedPoliciesCapabilities []string `json:"namespacedPoliciesCapabilities,omitempty"`
}

type ReconciliationTransitionReason string
Expand Down
119 changes: 119 additions & 0 deletions api/policies/v1/policyserver_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ package v1
import (
"context"
"fmt"
"maps"
"slices"
"strings"

corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
Expand All @@ -35,6 +38,46 @@ import (
"github.com/kubewarden/kubewarden-controller/internal/constants"
)

// capabilityNode is a node in the host-capability path tree.
// Leaf nodes (complete, addressable operations) have a nil value.
// Intermediate nodes carry a non-nil map of named children.
type capabilityNode map[string]capabilityNode

// capabilityTree is the authoritative tree of all recognised host capability
// paths. It mirrors the namespaces and operations handled by the policy-server
// callback (crates/policy-server/src/evaluation/callback.rs).
//
//nolint:gochecknoglobals // effectively a constant, not used anywhere else
var capabilityTree = capabilityNode{
"oci": {
"v1": {
"verify": nil,
"manifest_digest": nil,
"oci_manifest": nil,
"oci_manifest_config": nil,
},
"v2": {
"verify": nil,
},
},
"net": {
"v1": {
"dns_lookup_host": nil,
},
},
"crypto": {
"v1": {
"is_certificate_trusted": nil,
},
},
"kubernetes": {
"list_resources_by_namespace": nil,
"list_resources_all": nil,
"get_resource": nil,
"can_i": nil,
},
}

// SetupWebhookWithManager registers the PolicyServer webhook with the controller manager.
func (ps *PolicyServer) SetupWebhookWithManager(mgr ctrl.Manager, deploymentsNamespace string) error {
logger := mgr.GetLogger().WithName("policyserver-webhook")
Expand Down Expand Up @@ -131,6 +174,7 @@ func (v *policyServerValidator) validate(ctx context.Context, policyServer *Poli
}

allErrs = append(allErrs, validateLimitsAndRequests(policyServer.Spec.Limits, policyServer.Spec.Requests)...)
allErrs = append(allErrs, validateNamespacedPoliciesCapabilities(policyServer.Spec.NamespacedPoliciesCapabilities)...)

if len(allErrs) == 0 {
return nil
Expand Down Expand Up @@ -208,3 +252,78 @@ func validateLimitsAndRequests(limits, requests corev1.ResourceList) field.Error

return allErrs
}

// validateNamespacedPoliciesCapabilities validates each capability pattern
// against the authoritative capability tree.
//
// Valid formats:
// - "*" (allow all capabilities)
// - "category/*" (e.g. "oci/*", "kubernetes/*")
// - "category/sub/*" (e.g. "oci/v1/*")
// - full path (e.g. "oci/v1/verify", "kubernetes/can_i")
//
// Every segment is validated against the tree, so unknown categories,
// unknown versions, and unknown operations are all rejected with an error
// listing the valid options at that level.
func validateNamespacedPoliciesCapabilities(capabilities []string) field.ErrorList {
var allErrs field.ErrorList
fieldPath := field.NewPath("spec").Child("namespacedPoliciesCapabilities")

for i, pattern := range capabilities {
if err := validateSingleCapability(pattern, fieldPath.Index(i)); err != nil {
allErrs = append(allErrs, err)
}
}

return allErrs
}

// validateSingleCapability validates one capability pattern against the capability tree.
func validateSingleCapability(pattern string, path *field.Path) *field.Error {
if pattern == "" {
return field.Invalid(path, pattern, "capability must not be empty")
}
if pattern == "*" {
return nil
}

parts := strings.Split(pattern, "/")
node := capabilityTree

for i, part := range parts {
// Wildcard handling: "*" is only valid as the final segment.
if strings.Contains(part, "*") {
if part != "*" || i != len(parts)-1 {
return field.Invalid(path, pattern,
"wildcard \"*\" is only allowed as the last path segment (e.g. \"oci/*\" or \"oci/v1/*\")")
}
// Valid wildcard termination; parent node is already confirmed.
return nil
}

child, found := node[part]
if !found {
return field.Invalid(path, pattern,
fmt.Sprintf("unknown segment %q, valid options at this level are: %s",
part, strings.Join(slices.Sorted(maps.Keys(node)), ", ")))
}

if child == nil {
// Leaf reached, path must end here.
if i != len(parts)-1 {
return field.Invalid(path, pattern,
fmt.Sprintf("%q is a complete capability path and cannot have further segments",
strings.Join(parts[:i+1], "/")))
}
return nil
}

node = child
}

// Consumed all parts but stopped at an intermediate node: the path is
// incomplete. Guide the user toward the wildcard form.
return field.Invalid(path, pattern,
fmt.Sprintf("%q is not a complete capability path; use %q to allow all capabilities under it",
pattern, pattern+"/*"))
}
Loading