Make pod-security-admission honor emulation version

This commit is contained in:
Jordan Liggitt
2025-07-28 16:53:23 -04:00
parent 7f4ee652ea
commit 4b0eeeb618
9 changed files with 115 additions and 20 deletions

View File

@@ -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)
}

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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
}

View File

@@ -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{

View File

@@ -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)
}

View File

@@ -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.