Skip to content

Commit 2745017

Browse files
authored
Merge pull request #1693 from kubewarden/feat/allowed-host-cap
feat: Add host-capabilities whitelist
2 parents 5eacbd8 + 92f2322 commit 2745017

54 files changed

Lines changed: 3005 additions & 428 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/build-kwctl.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,7 @@ jobs:
8888
permissions:
8989
id-token: write
9090
attestations: write
91+
contents: read
9192
steps:
9293
- uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1
9394
if: ${{ !inputs.build_only }}
@@ -197,6 +198,7 @@ jobs:
197198
permissions:
198199
id-token: write
199200
attestations: write
201+
contents: read
200202
steps:
201203
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
202204
with:
@@ -286,6 +288,7 @@ jobs:
286288
permissions:
287289
id-token: write
288290
attestations: write
291+
contents: read
289292
steps:
290293
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
291294
with:

Cargo.lock

Lines changed: 24 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

api/policies/v1/factories.go

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -479,13 +479,14 @@ func (f *ClusterAdmissionPolicyGroupFactory) Build() *ClusterAdmissionPolicyGrou
479479
}
480480

481481
type PolicyServerBuilder struct {
482-
name string
483-
minAvailable *intstr.IntOrString
484-
maxUnavailable *intstr.IntOrString
485-
imagePullSecret string
486-
limits corev1.ResourceList
487-
requests corev1.ResourceList
488-
sigstoreTrustConfigMap string
482+
name string
483+
minAvailable *intstr.IntOrString
484+
maxUnavailable *intstr.IntOrString
485+
imagePullSecret string
486+
limits corev1.ResourceList
487+
requests corev1.ResourceList
488+
sigstoreTrustConfigMap string
489+
namespacedPoliciesCapabilities []string
489490
}
490491

491492
func NewPolicyServerFactory() *PolicyServerBuilder {
@@ -529,6 +530,11 @@ func (f *PolicyServerBuilder) WithRequests(requests corev1.ResourceList) *Policy
529530
return f
530531
}
531532

533+
func (f *PolicyServerBuilder) WithNamespacedPoliciesCapabilities(capabilities []string) *PolicyServerBuilder {
534+
f.namespacedPoliciesCapabilities = capabilities
535+
return f
536+
}
537+
532538
func (f *PolicyServerBuilder) Build() *PolicyServer {
533539
policyServer := PolicyServer{
534540
ObjectMeta: metav1.ObjectMeta{
@@ -544,14 +550,15 @@ func (f *PolicyServerBuilder) Build() *PolicyServer {
544550
},
545551
},
546552
Spec: PolicyServerSpec{
547-
Image: policyServerRepository() + ":" + policyServerVersion(),
548-
Replicas: 1,
549-
MinAvailable: f.minAvailable,
550-
MaxUnavailable: f.maxUnavailable,
551-
ImagePullSecret: f.imagePullSecret,
552-
Limits: f.limits,
553-
Requests: f.requests,
554-
SigstoreTrustConfig: f.sigstoreTrustConfigMap,
553+
Image: policyServerRepository() + ":" + policyServerVersion(),
554+
Replicas: 1,
555+
MinAvailable: f.minAvailable,
556+
MaxUnavailable: f.maxUnavailable,
557+
ImagePullSecret: f.imagePullSecret,
558+
Limits: f.limits,
559+
Requests: f.requests,
560+
SigstoreTrustConfig: f.sigstoreTrustConfigMap,
561+
NamespacedPoliciesCapabilities: f.namespacedPoliciesCapabilities,
555562
Env: []corev1.EnvVar{
556563
{
557564
Name: "KUBEWARDEN_LOG_LEVEL",

api/policies/v1/policyserver_types.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,6 +139,17 @@ type PolicyServerSpec struct {
139139
// remain unchanged, but new pods that reference it cannot be created.
140140
// +optional
141141
PriorityClassName string `json:"priorityClassName,omitempty"`
142+
143+
// NamespacedPoliciesCapabilities lists host capability API calls allowed
144+
// for namespaced policies running on this PolicyServer. When not set,
145+
// no host capabilities are granted to namespaced policies.
146+
// Supported wildcard patterns:
147+
// - "*": allow all host capabilities
148+
// - "category/*": allow all capabilities in a category (e.g. "oci/*")
149+
// - "category/version/*": allow all capabilities of a specific version (e.g. "oci/v1/*")
150+
// - Specific capability paths (e.g. "oci/v1/verify", "net/v1/dns_lookup_host")
151+
// +optional
152+
NamespacedPoliciesCapabilities []string `json:"namespacedPoliciesCapabilities,omitempty"`
142153
}
143154

144155
type ReconciliationTransitionReason string

api/policies/v1/policyserver_webhook.go

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ package v1
1919
import (
2020
"context"
2121
"fmt"
22+
"maps"
23+
"slices"
24+
"strings"
2225

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

41+
// capabilityNode is a node in the host-capability path tree.
42+
// Leaf nodes (complete, addressable operations) have a nil value.
43+
// Intermediate nodes carry a non-nil map of named children.
44+
type capabilityNode map[string]capabilityNode
45+
46+
// capabilityTree is the authoritative tree of all recognised host capability
47+
// paths. It mirrors the namespaces and operations handled by the policy-server
48+
// callback (crates/policy-server/src/evaluation/callback.rs).
49+
//
50+
//nolint:gochecknoglobals // effectively a constant, not used anywhere else
51+
var capabilityTree = capabilityNode{
52+
"oci": {
53+
"v1": {
54+
"verify": nil,
55+
"manifest_digest": nil,
56+
"oci_manifest": nil,
57+
"oci_manifest_config": nil,
58+
},
59+
"v2": {
60+
"verify": nil,
61+
},
62+
},
63+
"net": {
64+
"v1": {
65+
"dns_lookup_host": nil,
66+
},
67+
},
68+
"crypto": {
69+
"v1": {
70+
"is_certificate_trusted": nil,
71+
},
72+
},
73+
"kubernetes": {
74+
"list_resources_by_namespace": nil,
75+
"list_resources_all": nil,
76+
"get_resource": nil,
77+
"can_i": nil,
78+
},
79+
}
80+
3881
// SetupWebhookWithManager registers the PolicyServer webhook with the controller manager.
3982
func (ps *PolicyServer) SetupWebhookWithManager(mgr ctrl.Manager, deploymentsNamespace string) error {
4083
logger := mgr.GetLogger().WithName("policyserver-webhook")
@@ -131,6 +174,7 @@ func (v *policyServerValidator) validate(ctx context.Context, policyServer *Poli
131174
}
132175

133176
allErrs = append(allErrs, validateLimitsAndRequests(policyServer.Spec.Limits, policyServer.Spec.Requests)...)
177+
allErrs = append(allErrs, validateNamespacedPoliciesCapabilities(policyServer.Spec.NamespacedPoliciesCapabilities)...)
134178

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

209253
return allErrs
210254
}
255+
256+
// validateNamespacedPoliciesCapabilities validates each capability pattern
257+
// against the authoritative capability tree.
258+
//
259+
// Valid formats:
260+
// - "*" (allow all capabilities)
261+
// - "category/*" (e.g. "oci/*", "kubernetes/*")
262+
// - "category/sub/*" (e.g. "oci/v1/*")
263+
// - full path (e.g. "oci/v1/verify", "kubernetes/can_i")
264+
//
265+
// Every segment is validated against the tree, so unknown categories,
266+
// unknown versions, and unknown operations are all rejected with an error
267+
// listing the valid options at that level.
268+
func validateNamespacedPoliciesCapabilities(capabilities []string) field.ErrorList {
269+
var allErrs field.ErrorList
270+
fieldPath := field.NewPath("spec").Child("namespacedPoliciesCapabilities")
271+
272+
for i, pattern := range capabilities {
273+
if err := validateSingleCapability(pattern, fieldPath.Index(i)); err != nil {
274+
allErrs = append(allErrs, err)
275+
}
276+
}
277+
278+
return allErrs
279+
}
280+
281+
// validateSingleCapability validates one capability pattern against the capability tree.
282+
func validateSingleCapability(pattern string, path *field.Path) *field.Error {
283+
if pattern == "" {
284+
return field.Invalid(path, pattern, "capability must not be empty")
285+
}
286+
if pattern == "*" {
287+
return nil
288+
}
289+
290+
parts := strings.Split(pattern, "/")
291+
node := capabilityTree
292+
293+
for i, part := range parts {
294+
// Wildcard handling: "*" is only valid as the final segment.
295+
if strings.Contains(part, "*") {
296+
if part != "*" || i != len(parts)-1 {
297+
return field.Invalid(path, pattern,
298+
"wildcard \"*\" is only allowed as the last path segment (e.g. \"oci/*\" or \"oci/v1/*\")")
299+
}
300+
// Valid wildcard termination; parent node is already confirmed.
301+
return nil
302+
}
303+
304+
child, found := node[part]
305+
if !found {
306+
return field.Invalid(path, pattern,
307+
fmt.Sprintf("unknown segment %q, valid options at this level are: %s",
308+
part, strings.Join(slices.Sorted(maps.Keys(node)), ", ")))
309+
}
310+
311+
if child == nil {
312+
// Leaf reached, path must end here.
313+
if i != len(parts)-1 {
314+
return field.Invalid(path, pattern,
315+
fmt.Sprintf("%q is a complete capability path and cannot have further segments",
316+
strings.Join(parts[:i+1], "/")))
317+
}
318+
return nil
319+
}
320+
321+
node = child
322+
}
323+
324+
// Consumed all parts but stopped at an intermediate node: the path is
325+
// incomplete. Guide the user toward the wildcard form.
326+
return field.Invalid(path, pattern,
327+
fmt.Sprintf("%q is not a complete capability path; use %q to allow all capabilities under it",
328+
pattern, pattern+"/*"))
329+
}

0 commit comments

Comments
 (0)