Merge pull request #104588 from liggitt/podsecurity-benchmark

PodSecurity: benchmark and optimize privileged namespace evaluations
This commit is contained in:
Kubernetes Prow Robot 2021-09-22 16:17:10 -07:00 committed by GitHub
commit dce069ce22
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 900 additions and 30 deletions

View File

@ -17,14 +17,27 @@ limitations under the License.
package podsecurity
import (
"context"
"io/ioutil"
"testing"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authentication/user"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
featuregatetesting "k8s.io/component-base/featuregate/testing"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/batch"
"k8s.io/kubernetes/pkg/apis/core"
v1 "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/features"
podsecurityadmission "k8s.io/pod-security-admission/admission"
"sigs.k8s.io/yaml"
)
func TestConvert(t *testing.T) {
@ -58,3 +71,113 @@ func TestConvert(t *testing.T) {
}
}
}
func BenchmarkVerifyPod(b *testing.B) {
defer featuregatetesting.SetFeatureGateDuringTest(b, utilfeature.DefaultFeatureGate, features.PodSecurity, true)()
p, err := newPlugin(nil)
if err != nil {
b.Fatal(err)
}
p.InspectFeatureGates(utilfeature.DefaultFeatureGate)
enforceImplicitPrivilegedNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "enforce-implicit", Labels: map[string]string{}}}
enforcePrivilegedNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "enforce-privileged", Labels: map[string]string{"pod-security.kubernetes.io/enforce": "privileged"}}}
enforceBaselineNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "enforce-baseline", Labels: map[string]string{"pod-security.kubernetes.io/enforce": "baseline"}}}
enforceRestrictedNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "enforce-restricted", Labels: map[string]string{"pod-security.kubernetes.io/enforce": "restricted"}}}
warnBaselineNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "warn-baseline", Labels: map[string]string{"pod-security.kubernetes.io/warn": "baseline"}}}
warnRestrictedNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "warn-restricted", Labels: map[string]string{"pod-security.kubernetes.io/warn": "restricted"}}}
enforceWarnAuditBaseline := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "enforce-warn-audit-baseline", Labels: map[string]string{"pod-security.kubernetes.io/enforce": "baseline", "pod-security.kubernetes.io/warn": "baseline", "pod-security.kubernetes.io/audit": "baseline"}}}
warnBaselineAuditRestrictedNamespace := &corev1.Namespace{ObjectMeta: metav1.ObjectMeta{Name: "warn-baseline-audit-restricted", Labels: map[string]string{"pod-security.kubernetes.io/warn": "baseline", "pod-security.kubernetes.io/audit": "restricted"}}}
c := fake.NewSimpleClientset(
enforceImplicitPrivilegedNamespace,
enforcePrivilegedNamespace,
enforceBaselineNamespace,
enforceRestrictedNamespace,
warnBaselineNamespace,
warnRestrictedNamespace,
enforceWarnAuditBaseline,
warnBaselineAuditRestrictedNamespace,
)
p.SetExternalKubeClientSet(c)
informerFactory := informers.NewSharedInformerFactory(c, 0)
p.SetExternalKubeInformerFactory(informerFactory)
stopCh := make(chan struct{})
defer close(stopCh)
informerFactory.Start(stopCh)
informerFactory.WaitForCacheSync(stopCh)
if err := p.ValidateInitialization(); err != nil {
b.Fatal(err)
}
corePod := &core.Pod{}
v1Pod := &corev1.Pod{}
data, err := ioutil.ReadFile("testdata/pod.yaml")
if err != nil {
b.Fatal(err)
}
if err := yaml.Unmarshal(data, v1Pod); err != nil {
b.Fatal(err)
}
if err := v1.Convert_v1_Pod_To_core_Pod(v1Pod, corePod, nil); err != nil {
b.Fatal(err)
}
appsDeployment := &apps.Deployment{
ObjectMeta: metav1.ObjectMeta{Name: "mydeployment"},
Spec: apps.DeploymentSpec{
Template: core.PodTemplateSpec{
ObjectMeta: corePod.ObjectMeta,
Spec: corePod.Spec,
},
},
}
namespaces := []string{
"enforce-implicit", "enforce-privileged", "enforce-baseline", "enforce-restricted",
"warn-baseline", "warn-restricted",
"enforce-warn-audit-baseline", "warn-baseline-audit-restricted",
}
for _, namespace := range namespaces {
b.Run(namespace+"_pod", func(b *testing.B) {
ctx := context.Background()
attrs := admission.NewAttributesRecord(
corePod.DeepCopy(), nil,
schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Pod"},
namespace, "mypod",
schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"},
"",
admission.Create, &metav1.CreateOptions{}, false,
&user.DefaultInfo{Name: "myuser"},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := p.Validate(ctx, attrs, nil); err != nil {
b.Fatal(err)
}
}
})
b.Run(namespace+"_deployment", func(b *testing.B) {
ctx := context.Background()
attrs := admission.NewAttributesRecord(
appsDeployment.DeepCopy(), nil,
schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"},
namespace, "mydeployment",
schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"},
"",
admission.Create, &metav1.CreateOptions{}, false,
&user.DefaultInfo{Name: "myuser"},
)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := p.Validate(ctx, attrs, nil); err != nil {
b.Fatal(err)
}
}
})
}
}

View File

@ -0,0 +1,676 @@
# this pod fixture is used for benchmarks and should be kept updated to pass the latest restricted policy
apiVersion: v1
kind: Pod
metadata:
annotations:
scheduler.alpha.kubernetes.io/critical-pod: ""
seccomp.security.alpha.kubernetes.io/pod: runtime/default
creationTimestamp: "2021-08-20T14:35:04Z"
generateName: kube-dns-76dbc85bd5-
labels:
k8s-app: kube-dns
pod-template-hash: 76dbc85bd5
managedFields:
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:metadata:
f:annotations:
.: {}
f:scheduler.alpha.kubernetes.io/critical-pod: {}
f:seccomp.security.alpha.kubernetes.io/pod: {}
f:generateName: {}
f:labels:
.: {}
f:k8s-app: {}
f:pod-template-hash: {}
f:ownerReferences:
.: {}
k:{"uid":"901a2f14-52d5-468b-af25-6587b60f2887"}:
.: {}
f:apiVersion: {}
f:blockOwnerDeletion: {}
f:controller: {}
f:kind: {}
f:name: {}
f:uid: {}
f:spec:
f:affinity:
.: {}
f:podAntiAffinity:
.: {}
f:preferredDuringSchedulingIgnoredDuringExecution: {}
f:containers:
k:{"name":"dnsmasq"}:
.: {}
f:args: {}
f:image: image-name:tag-name
f:imagePullPolicy: {}
f:livenessProbe:
.: {}
f:failureThreshold: {}
f:httpGet:
.: {}
f:path: {}
f:port: {}
f:scheme: {}
f:initialDelaySeconds: {}
f:periodSeconds: {}
f:successThreshold: {}
f:timeoutSeconds: {}
f:name: {}
f:ports:
.: {}
k:{"containerPort":53,"protocol":"TCP"}:
.: {}
f:containerPort: {}
f:name: {}
f:protocol: {}
k:{"containerPort":53,"protocol":"UDP"}:
.: {}
f:containerPort: {}
f:name: {}
f:protocol: {}
f:resources:
.: {}
f:requests:
.: {}
f:cpu: {}
f:memory: {}
f:securityContext:
.: {}
f:capabilities:
.: {}
f:add: {}
f:drop: {}
f:terminationMessagePath: {}
f:terminationMessagePolicy: {}
f:volumeMounts:
.: {}
k:{"mountPath":"/etc/k8s/dns/dnsmasq-nanny"}:
.: {}
f:mountPath: {}
f:name: {}
k:{"name":"kubedns"}:
.: {}
f:args: {}
f:env:
.: {}
k:{"name":"PROMETHEUS_PORT"}:
.: {}
f:name: {}
f:value: {}
f:image: image-name:tag-name
f:imagePullPolicy: {}
f:livenessProbe:
.: {}
f:failureThreshold: {}
f:httpGet:
.: {}
f:path: {}
f:port: {}
f:scheme: {}
f:initialDelaySeconds: {}
f:periodSeconds: {}
f:successThreshold: {}
f:timeoutSeconds: {}
f:name: {}
f:ports:
.: {}
k:{"containerPort":10053,"protocol":"TCP"}:
.: {}
f:containerPort: {}
f:name: {}
f:protocol: {}
k:{"containerPort":10053,"protocol":"UDP"}:
.: {}
f:containerPort: {}
f:name: {}
f:protocol: {}
k:{"containerPort":10055,"protocol":"TCP"}:
.: {}
f:containerPort: {}
f:name: {}
f:protocol: {}
f:readinessProbe:
.: {}
f:failureThreshold: {}
f:httpGet:
.: {}
f:path: {}
f:port: {}
f:scheme: {}
f:initialDelaySeconds: {}
f:periodSeconds: {}
f:successThreshold: {}
f:timeoutSeconds: {}
f:resources:
.: {}
f:limits:
.: {}
f:memory: {}
f:requests:
.: {}
f:cpu: {}
f:memory: {}
f:securityContext:
.: {}
f:allowPrivilegeEscalation: {}
f:readOnlyRootFilesystem: {}
f:runAsGroup: {}
f:runAsUser: {}
f:terminationMessagePath: {}
f:terminationMessagePolicy: {}
f:volumeMounts:
.: {}
k:{"mountPath":"/kube-dns-config"}:
.: {}
f:mountPath: {}
f:name: {}
k:{"name":"prometheus-to-sd"}:
.: {}
f:command: {}
f:env:
.: {}
k:{"name":"POD_NAME"}:
.: {}
f:name: {}
f:valueFrom:
.: {}
f:fieldRef:
.: {}
f:apiVersion: {}
f:fieldPath: {}
k:{"name":"POD_NAMESPACE"}:
.: {}
f:name: {}
f:valueFrom:
.: {}
f:fieldRef:
.: {}
f:apiVersion: {}
f:fieldPath: {}
f:image: image-name:tag-name
f:imagePullPolicy: {}
f:name: {}
f:resources: {}
f:securityContext:
.: {}
f:allowPrivilegeEscalation: {}
f:readOnlyRootFilesystem: {}
f:runAsGroup: {}
f:runAsUser: {}
f:terminationMessagePath: {}
f:terminationMessagePolicy: {}
k:{"name":"sidecar"}:
.: {}
f:args: {}
f:image: image-name:tag-name
f:imagePullPolicy: {}
f:livenessProbe:
.: {}
f:failureThreshold: {}
f:httpGet:
.: {}
f:path: {}
f:port: {}
f:scheme: {}
f:initialDelaySeconds: {}
f:periodSeconds: {}
f:successThreshold: {}
f:timeoutSeconds: {}
f:name: {}
f:ports:
.: {}
k:{"containerPort":10054,"protocol":"TCP"}:
.: {}
f:containerPort: {}
f:name: {}
f:protocol: {}
f:resources:
.: {}
f:requests:
.: {}
f:cpu: {}
f:memory: {}
f:securityContext:
.: {}
f:allowPrivilegeEscalation: {}
f:readOnlyRootFilesystem: {}
f:runAsGroup: {}
f:runAsUser: {}
f:terminationMessagePath: {}
f:terminationMessagePolicy: {}
f:dnsPolicy: {}
f:enableServiceLinks: {}
f:nodeSelector:
.: {}
f:kubernetes.io/os: {}
f:priorityClassName: {}
f:restartPolicy: {}
f:schedulerName: {}
f:securityContext:
.: {}
f:fsGroup: {}
f:supplementalGroups: {}
f:serviceAccount: {}
f:serviceAccountName: {}
f:terminationGracePeriodSeconds: {}
f:tolerations: {}
f:volumes:
.: {}
k:{"name":"kube-dns-config"}:
.: {}
f:configMap:
.: {}
f:defaultMode: {}
f:name: {}
f:optional: {}
f:name: {}
manager: kube-controller-manager
operation: Update
time: "2021-08-20T14:35:04Z"
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:status:
f:conditions:
.: {}
k:{"type":"PodScheduled"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:message: {}
f:reason: {}
f:status: {}
f:type: {}
manager: kube-scheduler
operation: Update
time: "2021-08-20T14:35:04Z"
- apiVersion: v1
fieldsType: FieldsV1
fieldsV1:
f:status:
f:conditions:
k:{"type":"ContainersReady"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:status: {}
f:type: {}
k:{"type":"Initialized"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:status: {}
f:type: {}
k:{"type":"Ready"}:
.: {}
f:lastProbeTime: {}
f:lastTransitionTime: {}
f:status: {}
f:type: {}
f:containerStatuses: {}
f:hostIP: {}
f:phase: {}
f:podIP: {}
f:podIPs:
.: {}
k:{"ip":"10..10.10"}:
.: {}
f:ip: {}
f:startTime: {}
manager: kubelet
operation: Update
time: "2021-08-20T14:36:10Z"
name: kube-dns-76dbc85bd5-zl5tr
namespace: kube-system
ownerReferences:
- apiVersion: apps/v1
blockOwnerDeletion: true
controller: true
kind: ReplicaSet
name: kube-dns-76dbc85bd5
uid: 901a2f14-52d5-468b-af25-6587b60f2887
resourceVersion: "1391"
uid: e98f0f22-0937-4495-8211-d5633e50fb8d
spec:
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- podAffinityTerm:
labelSelector:
matchExpressions:
- key: k8s-app
operator: In
values:
- kube-dns
topologyKey: kubernetes.io/hostname
weight: 100
containers:
- args:
- --domain=cluster.local.
- --dns-port=10053
- --config-dir=/kube-dns-config
- --v=2
env:
- name: PROMETHEUS_PORT
value: "10055"
image: image-name:tag-name
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthcheck/kubedns
port: 10054
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: kubedns
ports:
- containerPort: 10053
name: dns-local
protocol: UDP
- containerPort: 10053
name: dns-tcp-local
protocol: TCP
- containerPort: 10055
name: metrics
protocol: TCP
readinessProbe:
failureThreshold: 3
httpGet:
path: /readiness
port: 8081
scheme: HTTP
initialDelaySeconds: 3
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
resources:
limits:
memory: 210Mi
requests:
cpu: 100m
memory: 70Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsGroup: 1001
runAsUser: 1001
runAsNonRoot: true
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /kube-dns-config
name: kube-dns-config
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-s8rz5
readOnly: true
- args:
- -v=2
- -logtostderr
- -configDir=/etc/k8s/dns/dnsmasq-nanny
- -restartDnsmasq=true
- --
- -k
- --cache-size=1000
- --no-negcache
- --dns-forward-max=1500
- --log-facility=-
- --server=/cluster.local/127.0.0.1#10053
- --server=/in-addr.arpa/127.0.0.1#10053
- --server=/ip6.arpa/127.0.0.1#10053
image: image-name:tag-name
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5
httpGet:
path: /healthcheck/dnsmasq
port: 10054
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: dnsmasq
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
resources:
requests:
cpu: 150m
memory: 20Mi
securityContext:
allowPrivilegeEscalation: false
runAsNonRoot: true
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /etc/k8s/dns/dnsmasq-nanny
name: kube-dns-config
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-s8rz5
readOnly: true
- args:
- --v=2
- --logtostderr
- --probe=kubedns,127.0.0.1:10053,kubernetes.default.svc.cluster.local,5,SRV
- --probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,SRV
image: image-name:tag-name
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5
httpGet:
path: /metrics
port: 10054
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 10
successThreshold: 1
timeoutSeconds: 5
name: sidecar
ports:
- containerPort: 10054
name: metrics
protocol: TCP
resources:
requests:
cpu: 10m
memory: 20Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsGroup: 1001
runAsUser: 1001
runAsNonRoot: true
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-s8rz5
readOnly: true
- command:
- /monitor
- --stackdriver-prefix=container.googleapis.com/internal/addons
- --api-override=https://test-monitoring.sandbox.googleapis.com/
- --pod-id=$(POD_NAME)
- --namespace-id=$(POD_NAMESPACE)
- --v=2
env:
- name: POD_NAME
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.name
- name: POD_NAMESPACE
valueFrom:
fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
image: image-name:tag-name
imagePullPolicy: IfNotPresent
name: prometheus-to-sd
resources: {}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsGroup: 1001
runAsUser: 1001
runAsNonRoot: true
capabilities:
add:
- NET_BIND_SERVICE
drop:
- ALL
terminationMessagePath: /dev/termination-log
terminationMessagePolicy: File
volumeMounts:
- mountPath: /var/run/secrets/kubernetes.io/serviceaccount
name: kube-api-access-s8rz5
readOnly: true
dnsPolicy: Default
enableServiceLinks: true
nodeName: mynode
nodeSelector:
kubernetes.io/os: linux
preemptionPolicy: PreemptLowerPriority
priority: 2000000000
priorityClassName: system-cluster-critical
restartPolicy: Always
schedulerName: default-scheduler
securityContext:
fsGroup: 65534
seccompProfile:
type: RuntimeDefault
supplementalGroups:
- 65534
serviceAccount: kube-dns
serviceAccountName: kube-dns
terminationGracePeriodSeconds: 30
tolerations:
- key: CriticalAddonsOnly
operator: Exists
- effect: NoExecute
key: node.kubernetes.io/not-ready
operator: Exists
tolerationSeconds: 300
- effect: NoExecute
key: node.kubernetes.io/unreachable
operator: Exists
tolerationSeconds: 300
volumes:
- configMap:
defaultMode: 420
name: kube-dns
optional: true
name: kube-dns-config
- name: kube-api-access-s8rz5
projected:
defaultMode: 420
sources:
- serviceAccountToken:
expirationSeconds: 3607
path: token
- configMap:
items:
- key: ca.crt
path: ca.crt
name: kube-root-ca.crt
- downwardAPI:
items:
- fieldRef:
apiVersion: v1
fieldPath: metadata.namespace
path: namespace
status:
conditions:
- lastProbeTime: null
lastTransitionTime: "2021-08-20T14:35:31Z"
status: "True"
type: Initialized
- lastProbeTime: null
lastTransitionTime: "2021-08-20T14:36:10Z"
status: "True"
type: Ready
- lastProbeTime: null
lastTransitionTime: "2021-08-20T14:36:10Z"
status: "True"
type: ContainersReady
- lastProbeTime: null
lastTransitionTime: "2021-08-20T14:35:31Z"
status: "True"
type: PodScheduled
containerStatuses:
- containerID: containerd://f21ec303caca266fa4b81ebe6c210b5aa2b8ea6a262d8038db2c4f57db127187
image: image-name:tag-name
imageID: imageid@sha256:8e2a7eaa7e6b1ede58d6361d0058a391260a46f0290b7f0368b709494e9e36bf
lastState: {}
name: dnsmasq
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2021-08-20T14:36:03Z"
- containerID: containerd://bf3db3f330364ba2af3763a3c0b0bcd137f0556a73fffd0e0dbda61035b696a9
image: image-name:tag-name
imageID: imageid@sha256:50a1d17afe48a4ae15c9321d8c16d8f1302358c92971884722514c4ed7315ca3
lastState: {}
name: kubedns
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2021-08-20T14:35:52Z"
- containerID: containerd://733304e5217f2c9827736e1226188b11488fd476d0b9f647bd098fe9db89460e
image: image-name:tag-name
imageID: imageid@sha256:aca8ef8aa7fae83e1f8583ed78dd4d11f655b9f22a0a76bda5edce6d8965bdf2
lastState: {}
name: prometheus-to-sd
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2021-08-20T14:36:09Z"
- containerID: containerd://4639ada29f769008d3b21eef48cd061534dfd7875b42d5103179d4f0258667e9
image: image-name:tag-name
imageID: imageid@sha256:3bb5033aefb3e3dee259ab3d357d38d16eacf9cf2e1542ad577e3796410033ca
lastState: {}
name: sidecar
ready: true
restartCount: 0
started: true
state:
running:
startedAt: "2021-08-20T14:36:06Z"
hostIP: 10.128.0.48
phase: Running
podIP: 10..10.10
podIPs:
- ip: 10..10.10
qosClass: Burstable
startTime: "2021-08-20T14:35:31Z"

View File

@ -188,14 +188,20 @@ func (a *Admission) ValidateConfiguration() error {
return nil
}
var (
namespacesResource = corev1.Resource("namespaces")
podsResource = corev1.Resource("pods")
)
// Validate admits an API request.
// The objects in admission attributes are expected to be external v1 objects that we care about.
// The returned response may be shared and must not be mutated.
func (a *Admission) Validate(ctx context.Context, attrs Attributes) *admissionv1.AdmissionResponse {
var response *admissionv1.AdmissionResponse
switch attrs.GetResource().GroupResource() {
case corev1.Resource("namespaces"):
case namespacesResource:
response = a.ValidateNamespace(ctx, attrs)
case corev1.Resource("pods"):
case podsResource:
response = a.ValidatePod(ctx, attrs)
default:
response = a.ValidatePodController(ctx, attrs)
@ -206,10 +212,13 @@ func (a *Admission) Validate(ctx context.Context, attrs Attributes) *admissionv1
return response
}
// ValidateNamespace evaluates a namespace create or update request to ensure the pod security labels are valid,
// and checks existing pods in the namespace for violations of the new policy when updating the enforce level on a namespace.
// The returned response may be shared between evaluations and must not be mutated.
func (a *Admission) ValidateNamespace(ctx context.Context, attrs Attributes) *admissionv1.AdmissionResponse {
// short-circuit on subresources
if attrs.GetSubresource() != "" {
return allowedResponse()
return sharedAllowedResponse()
}
obj, err := attrs.GetObject()
if err != nil {
@ -230,7 +239,7 @@ func (a *Admission) ValidateNamespace(ctx context.Context, attrs Attributes) *ad
if newErr != nil {
return invalidResponse(newErr.Error())
}
return allowedResponse()
return sharedAllowedResponse()
case admissionv1.Update:
// if update, check if policy labels changed
@ -257,24 +266,24 @@ func (a *Admission) ValidateNamespace(ctx context.Context, attrs Attributes) *ad
// * if the new enforce is the same version and level was relaxed
// * for exempt namespaces
if newPolicy.Enforce == oldPolicy.Enforce {
return allowedResponse()
return sharedAllowedResponse()
}
if newPolicy.Enforce.Level == api.LevelPrivileged {
return allowedResponse()
return sharedAllowedResponse()
}
if newPolicy.Enforce.Version == oldPolicy.Enforce.Version &&
api.CompareLevels(newPolicy.Enforce.Level, oldPolicy.Enforce.Level) < 1 {
return allowedResponse()
return sharedAllowedResponse()
}
if a.exemptNamespace(attrs.GetNamespace()) {
return allowedResponse()
return sharedAllowedResponse()
}
response := allowedResponse()
response.Warnings = a.EvaluatePodsInNamespace(ctx, namespace.Name, newPolicy.Enforce)
return response
default:
return allowedResponse()
return sharedAllowedResponse()
}
}
@ -292,14 +301,27 @@ var ignoredPodSubresources = map[string]bool{
"status": true,
}
// ValidatePod evaluates a pod create or update request against the effective policy for the namespace.
// The returned response may be shared between evaluations and must not be mutated.
func (a *Admission) ValidatePod(ctx context.Context, attrs Attributes) *admissionv1.AdmissionResponse {
// short-circuit on ignored subresources
if ignoredPodSubresources[attrs.GetSubresource()] {
return allowedResponse()
return sharedAllowedResponse()
}
// short-circuit on exempt namespaces and users
if a.exemptNamespace(attrs.GetNamespace()) || a.exemptUser(attrs.GetUserName()) {
return allowedResponse()
return sharedAllowedResponse()
}
// short-circuit on privileged enforce+audit+warn namespaces
namespace, err := a.NamespaceGetter.GetNamespace(ctx, attrs.GetNamespace())
if err != nil {
klog.ErrorS(err, "failed to fetch pod namespace", "namespace", attrs.GetNamespace())
return internalErrorResponse(fmt.Sprintf("failed to lookup namespace %s", attrs.GetNamespace()))
}
nsPolicy, nsPolicyErr := a.PolicyToEvaluate(namespace.Labels)
if nsPolicyErr == nil && nsPolicy.Enforce.Level == api.LevelPrivileged && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged {
return sharedAllowedResponse()
}
obj, err := attrs.GetObject()
@ -325,20 +347,33 @@ func (a *Admission) ValidatePod(ctx context.Context, attrs Attributes) *admissio
}
if !isSignificantPodUpdate(pod, oldPod) {
// Nothing we care about changed, so always allow the update.
return allowedResponse()
return sharedAllowedResponse()
}
}
return a.EvaluatePod(ctx, attrs.GetNamespace(), &pod.ObjectMeta, &pod.Spec, true)
return a.EvaluatePod(ctx, nsPolicy, nsPolicyErr, &pod.ObjectMeta, &pod.Spec, true)
}
// ValidatePodController evaluates a pod controller create or update request against the effective policy for the namespace.
// The returned response may be shared between evaluations and must not be mutated.
func (a *Admission) ValidatePodController(ctx context.Context, attrs Attributes) *admissionv1.AdmissionResponse {
// short-circuit on subresources
if attrs.GetSubresource() != "" {
return allowedResponse()
return sharedAllowedResponse()
}
// short-circuit on exempt namespaces and users
if a.exemptNamespace(attrs.GetNamespace()) || a.exemptUser(attrs.GetUserName()) {
return allowedResponse()
return sharedAllowedResponse()
}
// short-circuit on privileged audit+warn namespaces
namespace, err := a.NamespaceGetter.GetNamespace(ctx, attrs.GetNamespace())
if err != nil {
klog.ErrorS(err, "failed to fetch pod namespace", "namespace", attrs.GetNamespace())
return internalErrorResponse(fmt.Sprintf("failed to lookup namespace %s", attrs.GetNamespace()))
}
nsPolicy, nsPolicyErr := a.PolicyToEvaluate(namespace.Labels)
if nsPolicyErr == nil && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged {
return sharedAllowedResponse()
}
obj, err := attrs.GetObject()
@ -353,30 +388,24 @@ func (a *Admission) ValidatePodController(ctx context.Context, attrs Attributes)
}
if podMetadata == nil && podSpec == nil {
// if a controller with an optional pod spec does not contain a pod spec, skip validation
return allowedResponse()
return sharedAllowedResponse()
}
return a.EvaluatePod(ctx, attrs.GetNamespace(), podMetadata, podSpec, false)
return a.EvaluatePod(ctx, nsPolicy, nsPolicyErr, podMetadata, podSpec, false)
}
// EvaluatePod looks up the policy for the pods namespace, and checks it against the given pod(-like) object.
// EvaluatePod evaluates the given policy against the given pod(-like) object.
// The enforce policy is only checked if enforce=true.
func (a *Admission) EvaluatePod(ctx context.Context, namespaceName string, podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec, enforce bool) *admissionv1.AdmissionResponse {
// The returned response may be shared between evaluations and must not be mutated.
func (a *Admission) EvaluatePod(ctx context.Context, nsPolicy api.Policy, nsPolicyErr error, podMetadata *metav1.ObjectMeta, podSpec *corev1.PodSpec, enforce bool) *admissionv1.AdmissionResponse {
// short-circuit on exempt runtimeclass
if a.exemptRuntimeClass(podSpec.RuntimeClassName) {
return allowedResponse()
}
namespace, err := a.NamespaceGetter.GetNamespace(ctx, namespaceName)
if err != nil {
klog.ErrorS(err, "failed to fetch pod namespace", "namespace", namespaceName)
return internalErrorResponse(fmt.Sprintf("failed to lookup namespace %s", namespaceName))
return sharedAllowedResponse()
}
auditAnnotations := map[string]string{}
nsPolicy, err := a.PolicyToEvaluate(namespace.Labels)
if err != nil {
klog.V(2).InfoS("failed to parse PodSecurity namespace labels", "err", err)
auditAnnotations["error"] = fmt.Sprintf("Failed to parse policy: %v", err)
if nsPolicyErr != nil {
klog.V(2).InfoS("failed to parse PodSecurity namespace labels", "err", nsPolicyErr)
auditAnnotations["error"] = fmt.Sprintf("Failed to parse policy: %v", nsPolicyErr)
}
// TODO: log nsPolicy evaluation with context (op, resource, namespace, name) for the request.
@ -456,6 +485,12 @@ func (a *Admission) PolicyToEvaluate(labels map[string]string) (api.Policy, erro
return api.PolicyToEvaluate(labels, a.defaultPolicy)
}
var _sharedAllowedResponse = allowedResponse()
func sharedAllowedResponse() *admissionv1.AdmissionResponse {
return _sharedAllowedResponse
}
// allowedResponse is the response used when the admission decision is allow.
func allowedResponse() *admissionv1.AdmissionResponse {
return &admissionv1.AdmissionResponse{Allowed: true}

View File

@ -0,0 +1,36 @@
/*
Copyright 2021 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package admission
import (
"fmt"
"os"
"reflect"
"testing"
)
func TestMain(m *testing.M) {
sharedCopy := sharedAllowedResponse().DeepCopy()
rc := m.Run()
if !reflect.DeepEqual(sharedCopy, sharedAllowedResponse()) {
fmt.Println("sharedAllowedReponse mutated")
rc = 1
}
os.Exit(rc)
}