Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
12 changes: 12 additions & 0 deletions pkg/detection/mocks/policy_set.go

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

1 change: 1 addition & 0 deletions pkg/detection/policy_set.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type PolicySet interface {

Exists(id string) bool
UpsertPolicy(*storage.Policy) error
UpsertCompiledPolicy(compiled CompiledPolicy)
RemovePolicy(policyID string)
}

Expand Down
6 changes: 6 additions & 0 deletions pkg/detection/policy_set_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,12 @@ func (p *setImpl) UpsertPolicy(policy *storage.Policy) error {
return nil
}

// UpsertCompiledPolicy adds or updates an already-compiled policy in the set,
// avoiding the recompilation that UpsertPolicy would perform.
func (p *setImpl) UpsertCompiledPolicy(compiled CompiledPolicy) {
p.policyIDToCompiled.Set(compiled.Policy().GetId(), compiled)
}

// RemovePolicy removes a policy from the set.
func (p *setImpl) RemovePolicy(policyID string) {
p.policyIDToCompiled.Delete(policyID)
Expand Down
41 changes: 39 additions & 2 deletions sensor/admission-control/manager/evaluate_deploytime.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"github.com/stackrox/rox/pkg/booleanpolicy/policyfields"
"github.com/stackrox/rox/pkg/detection/deploytime"
"github.com/stackrox/rox/pkg/enforcers"
"github.com/stackrox/rox/pkg/images/types"
"github.com/stackrox/rox/pkg/kubernetes"
"github.com/stackrox/rox/pkg/namespaces"
"github.com/stackrox/rox/pkg/protoconv/resources"
Expand Down Expand Up @@ -161,6 +162,34 @@ func (m *manager) evaluateAdmissionRequest(s *state, req *admission.AdmissionReq
}
}

// Fast path: evaluate deployment-spec-only policies (no image fetches required).
fastPathImages := toPlaceholderImages(deployment)
fastPathAlerts, err := s.fastPathDeployDetector.Detect(context.Background(), detectionCtx, booleanpolicy.EnhancedDeployment{
Deployment: deployment,
Images: fastPathImages,
})
if err != nil {
observeAdmissionReview(reviewResultError, time.Since(start))
return nil, errors.Wrap(err, "running StackRox detection")
}
if len(fastPathAlerts) > 0 {
if !pointer.BoolDeref(req.DryRun, false) {
go m.filterAndPutAttemptedAlertsOnChan(req.Operation, fastPathAlerts...)
}
slowPathCount := len(s.slowPathDeployDetector.PolicySet().GetCompiledPolicies())
log.Debugf("Violated policies (fast path): %d, rejecting %s request on %s/%s [%s]", len(fastPathAlerts), req.Operation, req.Namespace, req.Name, req.Kind)
observeAdmissionReview(reviewResultDenied, time.Since(start))
return fail(req.UID, message(fastPathAlerts, !s.GetClusterConfig().GetAdmissionControllerConfig().GetDisableBypass(), slowPathCount)), nil
}

// Slow path: skip entirely if there are no policies that require image enrichment data.
if len(s.slowPathDeployDetector.PolicySet().GetCompiledPolicies()) == 0 {
log.Debugf("No policies violated, allowing %s request on %s/%s [%s]", req.Operation, req.Namespace, req.Name, req.Kind)
observeAdmissionReview(reviewResultAllowed, time.Since(start))
return pass(req.UID), nil
}

// Fetch images and evaluate slow path policies. If there are no modified images, skip the scan.
var fetchImgCtx context.Context
if timeoutSecs := s.GetClusterConfig().GetAdmissionControllerConfig().GetTimeoutSeconds(); timeoutSecs > 1 && hasModifiedImages(s, deployment, req) {
var cancel context.CancelFunc
Expand All @@ -169,7 +198,7 @@ func (m *manager) evaluateAdmissionRequest(s *state, req *admission.AdmissionReq
}

getAlertsFunc := func(dep *storage.Deployment, imgs []*storage.Image) ([]*storage.Alert, error) {
return s.deploytimeDetector.Detect(context.Background(), detectionCtx, booleanpolicy.EnhancedDeployment{
return s.slowPathDeployDetector.Detect(context.Background(), detectionCtx, booleanpolicy.EnhancedDeployment{
Deployment: dep,
Images: imgs,
})
Expand All @@ -193,5 +222,13 @@ func (m *manager) evaluateAdmissionRequest(s *state, req *admission.AdmissionReq

log.Debugf("Violated policies: %d, rejecting %s request on %s/%s [%s]", len(alerts), req.Operation, req.Namespace, req.Name, req.Kind)
observeAdmissionReview(reviewResultDenied, time.Since(start))
return fail(req.UID, message(alerts, !s.GetClusterConfig().GetAdmissionControllerConfig().GetDisableBypass())), nil
return fail(req.UID, message(alerts, !s.GetClusterConfig().GetAdmissionControllerConfig().GetDisableBypass(), 0)), nil
}

func toPlaceholderImages(deployment *storage.Deployment) []*storage.Image {
images := make([]*storage.Image, len(deployment.GetContainers()))
for i, c := range deployment.GetContainers() {
images[i] = types.ToImage(c.GetImage())
}
return images
}
12 changes: 6 additions & 6 deletions sensor/admission-control/manager/evaluate_runtime.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func (m *manager) evaluateRuntimeAdmissionRequest(s *state, req *admission.Admis
if sendAlerts {
go m.filterAndPutAttemptedAlertsOnChan(req.Operation, alerts...)
}
return fail(req.UID, message(alerts, false)), nil
return fail(req.UID, message(alerts, false, 0)), nil
}

if sendAlerts {
Expand All @@ -107,8 +107,8 @@ func (m *manager) evaluatePodEvent(s *state, req *admission.AdmissionRequest, ev
// Fast path: skip image fetches when no k8s event policies require deploy-time fields.
// Note: This webhook handles user-initiated commands (exec, port-forward) and
// lacks the requirement for burst resilience at scale, so this optimization is intentionally kept simple
if len(s.slowPathDetector.PolicySet().GetCompiledPolicies()) == 0 {
alerts, err := s.fastPathDetector.DetectForDeploymentAndKubeEvent(context.Background(),
if len(s.slowPathK8sEventDetector.PolicySet().GetCompiledPolicies()) == 0 {
alerts, err := s.fastPathK8sEventDetector.DetectForDeploymentAndKubeEvent(context.Background(),
booleanpolicy.EnhancedDeployment{
Deployment: deployment,
Images: make([]*storage.Image, len(deployment.GetContainers())),
Expand Down Expand Up @@ -151,7 +151,7 @@ func (m *manager) evaluatePodEvent(s *state, req *admission.AdmissionRequest, ev
go m.waitForDeploymentAndDetect(s, event)
}

alerts, err := s.fastPathDetector.DetectForDeploymentAndKubeEvent(context.Background(), booleanpolicy.EnhancedDeployment{}, event)
alerts, err := s.fastPathK8sEventDetector.DetectForDeploymentAndKubeEvent(context.Background(), booleanpolicy.EnhancedDeployment{}, event)
if err != nil {
return nil, false, errors.Wrap(err, "runtime detection without deployment enrichment")
}
Expand All @@ -160,7 +160,7 @@ func (m *manager) evaluatePodEvent(s *state, req *admission.AdmissionRequest, ev

func (m *manager) waitForDeploymentAndDetect(s *state, event *storage.KubernetesEvent) {
// No policies containing deploy-time fields; skip background image fetches and detection.
if len(s.slowPathDetector.PolicySet().GetCompiledPolicies()) == 0 {
if len(s.slowPathK8sEventDetector.PolicySet().GetCompiledPolicies()) == 0 {
return
}

Expand Down Expand Up @@ -199,7 +199,7 @@ func (m *manager) waitForDeploymentAndDetect(s *state, event *storage.Kubernetes
Deployment: dep,
Images: imgs,
}
return s.slowPathDetector.DetectForDeploymentAndKubeEvent(context.Background(), enhancedDeployment, event)
return s.slowPathK8sEventDetector.DetectForDeploymentAndKubeEvent(context.Background(), enhancedDeployment, event)
}

alerts, err := m.kickOffImgScansAndDetect(fetchImgCtx, s, getAlertsFunc, deployment)
Expand Down
42 changes: 30 additions & 12 deletions sensor/admission-control/manager/manager_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,19 @@ var (

type state struct {
*sensor.AdmissionControlSettings
deploytimeDetector deploytime.Detector

// fastPathDeployDetector evaluates deploy policies that only reference
// deployment spec fields (privileged, capabilities, labels, etc.) and
// can produce a review response without requiring image enrichment data.
fastPathDeployDetector deploytime.Detector

// slowPathDeployDetector evaluates deploy policies that require image
// enrichment data (scan results, image metadata, signatures).
slowPathDeployDetector deploytime.Detector

allK8sEventPoliciesDetector runtime.Detector
fastPathDetector runtime.Detector
slowPathDetector runtime.Detector
slowPathK8sEventDetector runtime.Detector
fastPathK8sEventDetector runtime.Detector

bypassForUsers, bypassForGroups set.FrozenStringSet
enforcedOps map[admission.Operation]struct{}
Expand Down Expand Up @@ -269,17 +277,24 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting
enforceOnCreates := newSettings.GetClusterConfig().GetAdmissionControllerConfig().GetEnabled()
enforceOnUpdates := newSettings.GetClusterConfig().GetAdmissionControllerConfig().GetEnforceOnUpdates()

// TODO(ROX-33188): Wire cluster and namespace label providers.
deployTimePolicySet := detection.NewPolicySet(nil, nil)
fastPathPolicies := detection.NewPolicySet(nil, nil)
slowPathPolicies := detection.NewPolicySet(nil, nil)
if enforceOnCreates || enforceOnUpdates {
for _, policy := range newSettings.GetEnforcedDeployTimePolicies().GetPolicies() {
if policyfields.AlertsOnMissingEnrichment(policy) &&
!newSettings.GetClusterConfig().GetAdmissionControllerConfig().GetScanInline() {
log.Warn(errors.ImageScanUnavailableMsg(policy))
continue
}
if err := deployTimePolicySet.UpsertPolicy(policy); err != nil {
log.Errorf("Unable to upsert policy %q (%s), will not be able to enforce", policy.GetName(), policy.GetId())
compiled, err := detection.CompilePolicy(policy, nil, nil)
if err != nil {
log.Errorf("Unable to compile policy %q (%s): %v", policy.GetName(), policy.GetId(), err)
continue
}
if compiled.RequiresImageEnrichment() {
slowPathPolicies.UpsertCompiledPolicy(compiled)
} else {
fastPathPolicies.UpsertCompiledPolicy(compiled)
}
}
}
Expand All @@ -296,10 +311,11 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting
oldState := m.currentState()
newState := &state{
AdmissionControlSettings: newSettings,
deploytimeDetector: deploytime.NewDetector(deployTimePolicySet),
fastPathDeployDetector: deploytime.NewDetector(fastPathPolicies),
slowPathDeployDetector: deploytime.NewDetector(slowPathPolicies),
allK8sEventPoliciesDetector: runtime.NewDetector(allK8sEventPolicySet),
slowPathDetector: runtime.NewDetector(k8sEventPoliciesWithDeployFields),
fastPathDetector: runtime.NewDetector(k8sEventPoliciesWithoutDeployFields),
slowPathK8sEventDetector: runtime.NewDetector(k8sEventPoliciesWithDeployFields),
fastPathK8sEventDetector: runtime.NewDetector(k8sEventPoliciesWithoutDeployFields),
bypassForUsers: allowAlwaysUsers,
bypassForGroups: allowAlwaysGroups,
enforcedOps: enforcedOperations,
Expand Down Expand Up @@ -346,10 +362,12 @@ func (m *manager) ProcessNewSettings(newSettings *sensor.AdmissionControlSetting
}
}
log.Infof("Applied new admission control settings "+
"(enforcing on %d deploy-time policies; "+
"(enforcing on %d deploy-time policies: %d deployment metadata only, %d image enrichment data required; "+
"detecting on %d run-time policies; "+
"enforcing on %d run-time policies).",
len(deployTimePolicySet.GetCompiledPolicies()),
len(fastPathPolicies.GetCompiledPolicies())+len(slowPathPolicies.GetCompiledPolicies()),
len(fastPathPolicies.GetCompiledPolicies()),
len(slowPathPolicies.GetCompiledPolicies()),
len(allK8sEventPolicySet.GetCompiledPolicies()),
enforceablePolicies)

Expand Down
9 changes: 6 additions & 3 deletions sensor/admission-control/manager/responses.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@ Policy: {{.Policy.Name}}
- {{.Message}}
{{- end}}

{{ end -}}
{{- if gt .UnevaluatedPolicyCount 0}}
{{.UnevaluatedPolicyCount}} additional {{if eq .UnevaluatedPolicyCount 1}}policy depends{{else}}policies depend{{end}} on image enrichment results and will be evaluated only after the above violations are addressed.
{{ end -}}
{{- if .BypassAnnotationKey}}
In case of emergency, add the annotation {"{{.BypassAnnotationKey}}": "ticket-1234"} to your deployment with an updated ticket number
Expand Down Expand Up @@ -62,12 +65,12 @@ func fail(uid types.UID, message string) *admission.AdmissionResponse {
}
}

func message(alerts []*storage.Alert, addBypassMsg bool) string {

func message(alerts []*storage.Alert, addBypassMsg bool, unevaluatedPolicyCount int) string {
// We add a line break at the beginning to look nicer in kubectl
msgHeader := "\nThe attempted operation violated one or more enforced policies, described below:\n\n"
data := map[string]interface{}{
"Alerts": alerts,
"Alerts": alerts,
"UnevaluatedPolicyCount": unevaluatedPolicyCount,
}

if addBypassMsg {
Expand Down
3 changes: 3 additions & 0 deletions sensor/common/admissioncontroller/settings_manager_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ func (p *settingsManager) UpdatePolicies(policies []*storage.Policy) {
if isEnforcedDeployTimePolicy(policy) {
deploytimePolicies = append(deploytimePolicies, policy.CloneVT())
}
// Audit log event policies share field types (KubeEvent) with K8s webhook
// policies, so ContainsOneOf alone is insufficient to distinguish them.
if pkgPolicies.AppliesAtRunTime(policy) &&
policy.GetEventSource() == storage.EventSource_DEPLOYMENT_EVENT &&
booleanpolicy.ContainsOneOf(policy, booleanpolicy.KubeEvent) {
runtimePolicies = append(runtimePolicies, policy.CloneVT())
}
Expand Down
Loading