From 4b0eeeb618449f355803e32dc1e448b33bf4570b Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Mon, 28 Jul 2025 16:53:23 -0400 Subject: [PATCH] Make pod-security-admission honor emulation version --- .../security/podsecurity/admission.go | 46 ++++++++++++---- .../security/podsecurity/admission_test.go | 6 +- .../admission/admission_test.go | 2 +- .../cmd/webhook/server/server.go | 2 +- .../check_hostProbesAndhostLifecycle_test.go | 55 +++++++++++++++++++ .../pod-security-admission/policy/registry.go | 8 ++- .../policy/registry_test.go | 8 +-- .../k8s.io/pod-security-admission/test/run.go | 6 +- test/e2e/framework/pod/utils.go | 2 +- 9 files changed, 115 insertions(+), 20 deletions(-) diff --git a/plugin/pkg/admission/security/podsecurity/admission.go b/plugin/pkg/admission/security/podsecurity/admission.go index e4b55cb4908..879c1a6f7e7 100644 --- a/plugin/pkg/admission/security/podsecurity/admission.go +++ b/plugin/pkg/admission/security/podsecurity/admission.go @@ -43,6 +43,7 @@ import ( "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" corev1listers "k8s.io/client-go/listers/core/v1" + "k8s.io/component-base/compatibility" "k8s.io/component-base/featuregate" "k8s.io/component-base/metrics/legacyregistry" "k8s.io/kubernetes/pkg/api/legacyscheme" @@ -72,6 +73,9 @@ type Plugin struct { inspectedFeatureGates bool + inspectedEffectiveVersion bool + emulationVersion *podsecurityadmissionapi.Version + client kubernetes.Interface namespaceLister corev1listers.NamespaceLister podLister corev1listers.PodLister @@ -104,16 +108,10 @@ func newPlugin(reader io.Reader) (*Plugin, error) { return nil, err } - evaluator, err := policy.NewEvaluator(policy.DefaultChecks()) - if err != nil { - return nil, fmt.Errorf("could not create PodSecurityRegistry: %w", err) - } - return &Plugin{ Handler: admission.NewHandler(admission.Create, admission.Update), delegate: &podsecurityadmission.Admission{ Configuration: config, - Evaluator: evaluator, Metrics: getDefaultRecorder(), PodSpecExtractor: podsecurityadmission.DefaultPodSpecExtractor{}, }, @@ -146,12 +144,37 @@ func (p *Plugin) updateDelegate() { if p.client == nil { return } - p.delegate.PodLister = podsecurityadmission.PodListerFromInformer(p.podLister) - p.delegate.NamespaceGetter = podsecurityadmission.NamespaceGetterFromListerAndClient(p.namespaceLister, p.client) + if !p.inspectedEffectiveVersion { + return + } + if p.delegate.PodLister == nil { + p.delegate.PodLister = podsecurityadmission.PodListerFromInformer(p.podLister) + } + if p.delegate.NamespaceGetter == nil { + p.delegate.NamespaceGetter = podsecurityadmission.NamespaceGetterFromListerAndClient(p.namespaceLister, p.client) + } + if p.delegate.Evaluator == nil { + evaluator, err := policy.NewEvaluator(policy.DefaultChecks(), p.emulationVersion) + if err != nil { + panic(fmt.Errorf("could not create PodSecurityRegistry: %w", err)) + } + p.delegate.Evaluator = evaluator + } } -func (c *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { - c.inspectedFeatureGates = true +func (p *Plugin) InspectEffectiveVersion(version compatibility.EffectiveVersion) { + p.inspectedEffectiveVersion = true + binaryVersion := version.BinaryVersion() + emulationVersion := version.EmulationVersion() + binaryMajorMinor := podsecurityadmissionapi.MajorMinorVersion(int(binaryVersion.Major()), int(binaryVersion.Minor())) + emulationMajorMinor := podsecurityadmissionapi.MajorMinorVersion(int(emulationVersion.Major()), int(emulationVersion.Minor())) + if binaryMajorMinor != emulationMajorMinor { + p.emulationVersion = &emulationMajorMinor + } +} + +func (p *Plugin) InspectFeatureGates(featureGates featuregate.FeatureGate) { + p.inspectedFeatureGates = true policy.RelaxPolicyForUserNamespacePods(featureGates.Enabled(features.UserNamespacesPodSecurityStandards)) } @@ -160,6 +183,9 @@ func (p *Plugin) ValidateInitialization() error { if !p.inspectedFeatureGates { return fmt.Errorf("%s did not see feature gates", PluginName) } + if !p.inspectedEffectiveVersion { + return fmt.Errorf("%s did not see effective version", PluginName) + } if err := p.delegate.CompleteConfiguration(); err != nil { return fmt.Errorf("%s configuration error: %w", PluginName, err) } diff --git a/plugin/pkg/admission/security/podsecurity/admission_test.go b/plugin/pkg/admission/security/podsecurity/admission_test.go index ed4c4b92918..e4a6c253cf0 100644 --- a/plugin/pkg/admission/security/podsecurity/admission_test.go +++ b/plugin/pkg/admission/security/podsecurity/admission_test.go @@ -23,6 +23,8 @@ import ( "strings" "testing" + "sigs.k8s.io/yaml" + corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -30,6 +32,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apiserver/pkg/admission" "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/util/compatibility" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/apiserver/pkg/warning" "k8s.io/client-go/informers" @@ -40,7 +43,6 @@ import ( v1 "k8s.io/kubernetes/pkg/apis/core/v1" podsecurityadmission "k8s.io/pod-security-admission/admission" "k8s.io/utils/ptr" - "sigs.k8s.io/yaml" ) func TestConvert(t *testing.T) { @@ -81,6 +83,7 @@ func BenchmarkVerifyPod(b *testing.B) { b.Fatal(err) } + p.InspectEffectiveVersion(compatibility.DefaultBuildEffectiveVersion()) p.InspectFeatureGates(utilfeature.DefaultFeatureGate) enforceImplicitPrivilegedNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "enforce-implicit", Labels: map[string]string{}}} @@ -189,6 +192,7 @@ func BenchmarkVerifyNamespace(b *testing.B) { b.Fatal(err) } + p.InspectEffectiveVersion(compatibility.DefaultBuildEffectiveVersion()) p.InspectFeatureGates(utilfeature.DefaultFeatureGate) namespace := "enforce" diff --git a/staging/src/k8s.io/pod-security-admission/admission/admission_test.go b/staging/src/k8s.io/pod-security-admission/admission/admission_test.go index 868950ce5ee..38bd4dac80a 100644 --- a/staging/src/k8s.io/pod-security-admission/admission/admission_test.go +++ b/staging/src/k8s.io/pod-security-admission/admission/admission_test.go @@ -702,7 +702,7 @@ func TestValidatePodAndController(t *testing.T) { config.Exemptions.RuntimeClasses = []string{exemptRuntimeClass} config.Exemptions.Usernames = []string{exemptUser} - evaluator, err := policy.NewEvaluator(policy.DefaultChecks()) + evaluator, err := policy.NewEvaluator(policy.DefaultChecks(), nil) assert.NoError(t, err) type testCase struct { diff --git a/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go b/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go index c357ab231a7..3d71a23e3ec 100644 --- a/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go +++ b/staging/src/k8s.io/pod-security-admission/cmd/webhook/server/server.go @@ -281,7 +281,7 @@ func Setup(c *Config) (*Server, error) { namespaceInformer := s.informerFactory.Core().V1().Namespaces() namespaceLister := namespaceInformer.Lister() - evaluator, err := policy.NewEvaluator(policy.DefaultChecks()) + evaluator, err := policy.NewEvaluator(policy.DefaultChecks(), nil) if err != nil { return nil, fmt.Errorf("could not create PodSecurityRegistry: %w", err) } diff --git a/staging/src/k8s.io/pod-security-admission/policy/check_hostProbesAndhostLifecycle_test.go b/staging/src/k8s.io/pod-security-admission/policy/check_hostProbesAndhostLifecycle_test.go index 11e040680bd..f6d5034ecf1 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/check_hostProbesAndhostLifecycle_test.go +++ b/staging/src/k8s.io/pod-security-admission/policy/check_hostProbesAndhostLifecycle_test.go @@ -20,8 +20,63 @@ import ( "testing" corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/pod-security-admission/api" + "k8s.io/utils/ptr" ) +func TestHostProbesAndHostLifecycleEmulation(t *testing.T) { + testcases := []struct { + name string + emulateVersion *api.Version + hostCheckActive bool + }{ + { + name: "no emulation", + emulateVersion: nil, + hostCheckActive: true, + }, + { + name: "emulate 1.34", + emulateVersion: ptr.To(api.MajorMinorVersion(1, 34)), + hostCheckActive: true, + }, + { + name: "emulate 1.33", + emulateVersion: ptr.To(api.MajorMinorVersion(1, 33)), + hostCheckActive: false, + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + e, err := NewEvaluator(DefaultChecks(), tc.emulateVersion) + if err != nil { + t.Fatal(err) + } + + // pod that uses a probe with an explicit host + podMetadata := &metav1.ObjectMeta{} + podSpec := &corev1.PodSpec{Containers: []corev1.Container{{StartupProbe: &corev1.Probe{ProbeHandler: corev1.ProbeHandler{HTTPGet: &corev1.HTTPGetAction{Host: "localhost"}}}}}} + + // evaluating "version=latest" and "version=1.34" should only allow if the host check is not active + expectAllowed := tc.hostCheckActive == false + if result := AggregateCheckResults(e.EvaluatePod(api.LevelVersion{Level: api.LevelBaseline, Version: api.LatestVersion()}, podMetadata, podSpec)); result.Allowed != expectAllowed { + t.Fatalf("evaluating with 'latest' expected allowed=%v, got %v: %v", expectAllowed, result.Allowed, result.ForbiddenReasons) + } + if result := AggregateCheckResults(e.EvaluatePod(api.LevelVersion{Level: api.LevelBaseline, Version: api.MajorMinorVersion(1, 34)}, podMetadata, podSpec)); result.Allowed != expectAllowed { + t.Fatalf("evaluating with '1.34' expected allowed=%v, got %v: %v", expectAllowed, result.Allowed, result.ForbiddenReasons) + } + + // evaluating "version=1.33" should always allow + if result := AggregateCheckResults(e.EvaluatePod(api.LevelVersion{Level: api.LevelBaseline, Version: api.MajorMinorVersion(1, 33)}, podMetadata, podSpec)); !result.Allowed { + t.Fatalf("evaluating with '1.33' expected allowed=true, got %v: %v", result.Allowed, result.ForbiddenReasons) + } + }) + } + +} + func TestHostProbesAndHostLifecycle(t *testing.T) { tests := []struct { name string diff --git a/staging/src/k8s.io/pod-security-admission/policy/registry.go b/staging/src/k8s.io/pod-security-admission/policy/registry.go index 4b91bef8875..1648ff020cc 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/registry.go +++ b/staging/src/k8s.io/pod-security-admission/policy/registry.go @@ -46,7 +46,7 @@ type checkRegistry struct { // 2. Check.Level must be either Baseline or Restricted // 3. Checks must have a non-empty set of versions, sorted in a strictly increasing order // 4. Check.Versions cannot include 'latest' -func NewEvaluator(checks []Check) (Evaluator, error) { +func NewEvaluator(checks []Check, emulationVersion *api.Version) (*checkRegistry, error) { if err := validateChecks(checks); err != nil { return nil, err } @@ -55,6 +55,12 @@ func NewEvaluator(checks []Check) (Evaluator, error) { restrictedChecks: map[api.Version][]CheckPodFn{}, } populate(r, checks) + + // lower the max version if we're emulating an older minor + if emulationVersion != nil && (*emulationVersion).Older(r.maxVersion) { + r.maxVersion = *emulationVersion + } + return r, nil } diff --git a/staging/src/k8s.io/pod-security-admission/policy/registry_test.go b/staging/src/k8s.io/pod-security-admission/policy/registry_test.go index 039b83b25b8..ee6c078bec2 100644 --- a/staging/src/k8s.io/pod-security-admission/policy/registry_test.go +++ b/staging/src/k8s.io/pod-security-admission/policy/registry_test.go @@ -43,7 +43,7 @@ func TestCheckRegistry(t *testing.T) { multiOverride.Versions[1].OverrideCheckIDs = []CheckID{"d"} checks = append(checks, multiOverride) - reg, err := NewEvaluator(checks) + reg, err := NewEvaluator(checks, nil) require.NoError(t, err) levelCases := []registryTestCase{ @@ -76,7 +76,7 @@ func TestCheckRegistry_NoBaseline(t *testing.T) { withOverrides(generateCheck("h", api.LevelRestricted, []string{"v1.0"}), []CheckID{"b"}), } - reg, err := NewEvaluator(checks) + reg, err := NewEvaluator(checks, nil) require.NoError(t, err) levelCases := []registryTestCase{ @@ -103,7 +103,7 @@ func TestCheckRegistry_NoRestricted(t *testing.T) { generateCheck("d", api.LevelBaseline, []string{"v1.11", "v1.15", "v1.20"}), } - reg, err := NewEvaluator(checks) + reg, err := NewEvaluator(checks, nil) require.NoError(t, err) levelCases := []registryTestCase{ @@ -124,7 +124,7 @@ func TestCheckRegistry_NoRestricted(t *testing.T) { } func TestCheckRegistry_Empty(t *testing.T) { - reg, err := NewEvaluator(nil) + reg, err := NewEvaluator(nil, nil) require.NoError(t, err) levelCases := []registryTestCase{ diff --git a/staging/src/k8s.io/pod-security-admission/test/run.go b/staging/src/k8s.io/pod-security-admission/test/run.go index 8e3451e419e..c6eb117afcb 100644 --- a/staging/src/k8s.io/pod-security-admission/test/run.go +++ b/staging/src/k8s.io/pod-security-admission/test/run.go @@ -53,6 +53,10 @@ type Options struct { // If unset, all testcases are run. Features featuregate.FeatureGate + // EmulationVersion optionally indicates a different minor version is being emulated. + // This can lower the effective "latest" version. + EmulationVersion *api.Version + // CreateNamespace is an optional stub for creating a namespace with the given name and labels. // Returning an error fails the test. // If nil, DefaultCreateNamespace is used. @@ -194,7 +198,7 @@ func Run(t *testing.T, opts Options) { if len(opts.Checks) == 0 { opts.Checks = policy.DefaultChecks() } - _, err = policy.NewEvaluator(opts.Checks) + _, err = policy.NewEvaluator(opts.Checks, opts.EmulationVersion) if err != nil { t.Fatalf("invalid checks: %v", err) } diff --git a/test/e2e/framework/pod/utils.go b/test/e2e/framework/pod/utils.go index b1fd25bed6d..74d42507a52 100644 --- a/test/e2e/framework/pod/utils.go +++ b/test/e2e/framework/pod/utils.go @@ -178,7 +178,7 @@ func GetRestrictedContainerSecurityContext() *v1.SecurityContext { } } -var psaEvaluator, _ = psapolicy.NewEvaluator(psapolicy.DefaultChecks()) +var psaEvaluator, _ = psapolicy.NewEvaluator(psapolicy.DefaultChecks(), nil) // MustMixinRestrictedPodSecurity makes the given pod compliant with the restricted pod security level. // If doing so would overwrite existing non-conformant configuration, a test failure is triggered.