mirror of
https://github.com/projectcapsule/capsule.git
synced 2026-08-20 21:16:49 +00:00
* fix(controller): decode old object for delete requests Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: modernize golang Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix(config): remove usergroups default Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * fix(config): remove usergroups default Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * sec(ghsa-2ww6-hf35-mfjm): intercept namespace subresource Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * chore: conflicts Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> * feat(api): add rulestatus api Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com> --------- Signed-off-by: Oliver Bähler <oliverbaehler@hotmail.com>
335 lines
7.5 KiB
Go
335 lines
7.5 KiB
Go
// Copyright 2020-2026 Project Capsule Authors
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package pod
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"sort"
|
|
"strings"
|
|
|
|
corev1 "k8s.io/api/core/v1"
|
|
"k8s.io/client-go/tools/events"
|
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
|
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
|
|
|
|
capsulev1beta2 "github.com/projectcapsule/capsule/api/v1beta2"
|
|
"github.com/projectcapsule/capsule/internal/cache"
|
|
"github.com/projectcapsule/capsule/pkg/api"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/configuration"
|
|
evt "github.com/projectcapsule/capsule/pkg/runtime/events"
|
|
"github.com/projectcapsule/capsule/pkg/runtime/handlers"
|
|
)
|
|
|
|
type registryHandler struct {
|
|
configuration configuration.Configuration
|
|
cache *cache.RegistryRuleSetCache
|
|
}
|
|
|
|
func ContainerRegistry(configuration configuration.Configuration, cache *cache.RegistryRuleSetCache) handlers.TypedHandlerWithTenantWithRuleset[*corev1.Pod] {
|
|
return ®istryHandler{
|
|
configuration: configuration,
|
|
cache: cache,
|
|
}
|
|
}
|
|
|
|
func (h *registryHandler) OnCreate(
|
|
c client.Client,
|
|
pod *corev1.Pod,
|
|
decoder admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
tnt *capsulev1beta2.Tenant,
|
|
rule *capsulev1beta2.NamespaceRuleBody,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
return h.validate(req, pod, tnt, recorder, rule)
|
|
}
|
|
}
|
|
|
|
func (h *registryHandler) OnUpdate(
|
|
c client.Client,
|
|
old *corev1.Pod,
|
|
pod *corev1.Pod,
|
|
decoder admission.Decoder,
|
|
recorder events.EventRecorder,
|
|
tnt *capsulev1beta2.Tenant,
|
|
rule *capsulev1beta2.NamespaceRuleBody,
|
|
) handlers.Func {
|
|
return func(ctx context.Context, req admission.Request) *admission.Response {
|
|
return h.validate(req, pod, tnt, recorder, rule)
|
|
}
|
|
}
|
|
|
|
func (h *registryHandler) OnDelete(
|
|
client.Client,
|
|
*corev1.Pod,
|
|
admission.Decoder,
|
|
events.EventRecorder,
|
|
*capsulev1beta2.Tenant,
|
|
*capsulev1beta2.NamespaceRuleBody,
|
|
) handlers.Func {
|
|
return func(context.Context, admission.Request) *admission.Response {
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func (h *registryHandler) validate(
|
|
req admission.Request,
|
|
pod *corev1.Pod,
|
|
tnt *capsulev1beta2.Tenant,
|
|
recorder events.EventRecorder,
|
|
rule *capsulev1beta2.NamespaceRuleBody,
|
|
) *admission.Response {
|
|
if rule == nil || len(rule.Enforce.Registries) == 0 {
|
|
resp := admission.Allowed("no registry rules")
|
|
|
|
return &resp
|
|
}
|
|
|
|
rs, _, err := h.cache.GetOrBuild(rule.Enforce.Registries)
|
|
if err != nil {
|
|
resp := admission.Errored(http.StatusInternalServerError, err)
|
|
|
|
return &resp
|
|
}
|
|
|
|
if rs == nil {
|
|
resp := admission.Allowed("no registry rules")
|
|
|
|
return &resp
|
|
}
|
|
|
|
if rs.HasImages {
|
|
if resp := h.validateContainers(req, pod, tnt, recorder, rs); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
if rs.HasVolumes {
|
|
if resp := h.validateVolumes(req, pod, tnt, recorder, rs); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *registryHandler) validateContainers(
|
|
req admission.Request,
|
|
pod *corev1.Pod,
|
|
tnt *capsulev1beta2.Tenant,
|
|
recorder events.EventRecorder,
|
|
rs *cache.RuleSet,
|
|
) *admission.Response {
|
|
for i := range pod.Spec.InitContainers {
|
|
c := pod.Spec.InitContainers[i]
|
|
if resp := h.verifyOCIReference(recorder, req, tnt, pod, rs, api.ValidateImages, c.Image, c.ImagePullPolicy, fmt.Sprintf("initContainers[%d]", i)); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
for i := range pod.Spec.EphemeralContainers {
|
|
c := pod.Spec.EphemeralContainers[i]
|
|
if resp := h.verifyOCIReference(recorder, req, tnt, pod, rs, api.ValidateImages, c.Image, c.ImagePullPolicy, fmt.Sprintf("ephemeralContainers[%d]", i)); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
for i := range pod.Spec.Containers {
|
|
c := pod.Spec.Containers[i]
|
|
if resp := h.verifyOCIReference(recorder, req, tnt, pod, rs, api.ValidateImages, c.Image, c.ImagePullPolicy, fmt.Sprintf("containers[%d]", i)); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (h *registryHandler) validateVolumes(
|
|
req admission.Request,
|
|
pod *corev1.Pod,
|
|
tnt *capsulev1beta2.Tenant,
|
|
recorder events.EventRecorder,
|
|
rs *cache.RuleSet,
|
|
) *admission.Response {
|
|
for i := range pod.Spec.Volumes {
|
|
v := pod.Spec.Volumes[i]
|
|
if v.Image == nil {
|
|
continue
|
|
}
|
|
|
|
ref := strings.TrimSpace(v.Image.Reference)
|
|
if ref == "" {
|
|
resp := admission.Denied(fmt.Sprintf("volume %q has empty image.reference", v.Name))
|
|
|
|
return &resp
|
|
}
|
|
|
|
if resp := h.verifyOCIReference(
|
|
recorder, req, tnt, pod,
|
|
rs, api.ValidateVolumes,
|
|
ref, v.Image.PullPolicy,
|
|
fmt.Sprintf("volumes[%d](%s)", i, v.Name),
|
|
); resp != nil {
|
|
return resp
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type resolvedRegistryConfig struct {
|
|
allowed bool
|
|
allowedPolicy map[corev1.PullPolicy]struct{} // nil => no restriction
|
|
}
|
|
|
|
func resolveRegistryConfig(
|
|
rules []cache.CompiledRule,
|
|
ref string,
|
|
target api.RegistryValidationTarget,
|
|
) resolvedRegistryConfig {
|
|
var res resolvedRegistryConfig
|
|
|
|
for i := range rules {
|
|
r := rules[i]
|
|
|
|
switch target {
|
|
case api.ValidateImages:
|
|
if !r.ValidateImages { // adjust field name
|
|
continue
|
|
}
|
|
case api.ValidateVolumes:
|
|
if !r.ValidateVolumes { // adjust field name
|
|
continue
|
|
}
|
|
}
|
|
|
|
if !r.RE.MatchString(ref) { // adjust field name
|
|
continue
|
|
}
|
|
|
|
res.allowed = true
|
|
|
|
// only override pullpolicy restriction when explicitly set by a later matching rule
|
|
if len(r.AllowedPolicy) > 0 { // adjust field name
|
|
res.allowedPolicy = r.AllowedPolicy
|
|
}
|
|
}
|
|
|
|
return res
|
|
}
|
|
|
|
func (h *registryHandler) verifyOCIReference(
|
|
recorder events.EventRecorder,
|
|
req admission.Request,
|
|
tnt *capsulev1beta2.Tenant,
|
|
pod *corev1.Pod,
|
|
rs *cache.RuleSet,
|
|
target api.RegistryValidationTarget,
|
|
reference string,
|
|
pullPolicy corev1.PullPolicy,
|
|
where string,
|
|
) *admission.Response {
|
|
ref := strings.TrimSpace(reference)
|
|
if ref == "" {
|
|
msg := fmt.Sprintf("%s has empty reference", where)
|
|
|
|
resp := admission.Denied(msg)
|
|
|
|
recorder.Eventf(
|
|
pod,
|
|
tnt,
|
|
corev1.EventTypeWarning,
|
|
evt.ReasonForbiddenContainerRegistry,
|
|
evt.ActionValidationDenied,
|
|
msg,
|
|
)
|
|
|
|
return &resp
|
|
}
|
|
|
|
// Match rules against the FULL OCI reference string.
|
|
// This avoids relying on parsing logic and supports nested paths, digests, etc.
|
|
cfg := resolveRegistryConfig(rs.Compiled, ref, target)
|
|
if !cfg.allowed {
|
|
msg := fmt.Sprintf("%s reference %q is not allowed", where, ref)
|
|
|
|
resp := admission.Denied(msg)
|
|
|
|
recorder.Eventf(
|
|
pod,
|
|
tnt,
|
|
corev1.EventTypeWarning,
|
|
evt.ReasonForbiddenContainerRegistry,
|
|
evt.ActionValidationDenied,
|
|
msg,
|
|
)
|
|
|
|
return &resp
|
|
}
|
|
|
|
// No defaulting: enforce only if restricted; empty pullPolicy is rejected under restriction.
|
|
if cfg.allowedPolicy != nil {
|
|
allowed := formatAllowedPullPolicies(cfg.allowedPolicy)
|
|
|
|
if pullPolicy == "" {
|
|
msg := fmt.Sprintf(
|
|
"%s reference %q must explicitly set pullPolicy (allowed: %s)",
|
|
where, ref, allowed,
|
|
)
|
|
|
|
resp := admission.Denied(msg)
|
|
|
|
recorder.Eventf(
|
|
pod,
|
|
tnt,
|
|
corev1.EventTypeWarning,
|
|
evt.ReasonForbiddenPullPolicy,
|
|
evt.ActionValidationDenied,
|
|
msg,
|
|
)
|
|
|
|
return &resp
|
|
}
|
|
|
|
if _, ok := cfg.allowedPolicy[pullPolicy]; !ok {
|
|
msg := fmt.Sprintf(
|
|
"%s reference %q uses pullPolicy=%s which is not allowed (allowed: %s)",
|
|
where, ref, pullPolicy, allowed,
|
|
)
|
|
|
|
resp := admission.Denied(msg)
|
|
|
|
recorder.Eventf(
|
|
pod,
|
|
tnt,
|
|
corev1.EventTypeWarning,
|
|
evt.ReasonForbiddenPullPolicy,
|
|
evt.ActionValidationDenied,
|
|
msg,
|
|
)
|
|
|
|
return &resp
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func formatAllowedPullPolicies(policies map[corev1.PullPolicy]struct{}) string {
|
|
if len(policies) == 0 {
|
|
return ""
|
|
}
|
|
|
|
out := make([]string, 0, len(policies))
|
|
for p := range policies {
|
|
out = append(out, string(p))
|
|
}
|
|
|
|
sort.Strings(out)
|
|
|
|
return strings.Join(out, ", ")
|
|
}
|