mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-25 20:53:33 +00:00
PodSecurity: admission: admission library
Co-authored-by: Jordan Liggitt <liggitt@google.com>
This commit is contained in:
parent
29f5ebf1fe
commit
02a6187757
549
staging/src/k8s.io/pod-security-admission/admission/admission.go
Normal file
549
staging/src/k8s.io/pod-security-admission/admission/admission.go
Normal file
@ -0,0 +1,549 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"reflect"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
|
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"
|
||||||
|
admissionapi "k8s.io/pod-security-admission/admission/api"
|
||||||
|
"k8s.io/pod-security-admission/admission/api/validation"
|
||||||
|
"k8s.io/pod-security-admission/api"
|
||||||
|
"k8s.io/pod-security-admission/metrics"
|
||||||
|
"k8s.io/pod-security-admission/policy"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
namespaceMaxPodsToCheck = 3000
|
||||||
|
namespacePodCheckTimeout = 1 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Admission implements the core admission logic for the Pod Security Admission controller.
|
||||||
|
// The admission logic can be
|
||||||
|
type Admission struct {
|
||||||
|
Configuration *admissionapi.PodSecurityConfiguration
|
||||||
|
|
||||||
|
// Getting policy checks per level/version
|
||||||
|
Evaluator policy.Evaluator
|
||||||
|
|
||||||
|
// Metrics
|
||||||
|
Metrics metrics.EvaluationRecorder
|
||||||
|
|
||||||
|
// Arbitrary object --> PodSpec
|
||||||
|
PodSpecExtractor PodSpecExtractor
|
||||||
|
|
||||||
|
// API connections
|
||||||
|
NamespaceGetter NamespaceGetter
|
||||||
|
PodLister PodLister
|
||||||
|
|
||||||
|
defaultPolicy api.Policy
|
||||||
|
}
|
||||||
|
|
||||||
|
type NamespaceGetter interface {
|
||||||
|
GetNamespace(ctx context.Context, name string) (*corev1.Namespace, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type PodLister interface {
|
||||||
|
ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodSpecExtractor extracts a PodSpec from pod-controller resources that embed a PodSpec.
|
||||||
|
// This interface can be extended to enforce policy on CRDs for custom pod-controllers.
|
||||||
|
type PodSpecExtractor interface {
|
||||||
|
// HasPodSpec returns true if the given resource type MAY contain an extractable PodSpec.
|
||||||
|
HasPodSpec(schema.GroupResource) bool
|
||||||
|
// ExtractPodSpec returns a pod spec and metadata to evaluate from the object.
|
||||||
|
// An error returned here does not block admission of the pod-spec-containing object and is not returned to the user.
|
||||||
|
// If the object has no pod spec, return `nil, nil, nil`.
|
||||||
|
ExtractPodSpec(runtime.Object) (*metav1.ObjectMeta, *corev1.PodSpec, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var defaultPodSpecResources = map[schema.GroupResource]bool{
|
||||||
|
corev1.Resource("pods"): true,
|
||||||
|
corev1.Resource("replicationcontrollers"): true,
|
||||||
|
corev1.Resource("podtemplates"): true,
|
||||||
|
appsv1.Resource("replicasets"): true,
|
||||||
|
appsv1.Resource("deployments"): true,
|
||||||
|
appsv1.Resource("statefulsets"): true,
|
||||||
|
appsv1.Resource("daemonsets"): true,
|
||||||
|
batchv1.Resource("jobs"): true,
|
||||||
|
batchv1.Resource("cronjobs"): true,
|
||||||
|
}
|
||||||
|
|
||||||
|
type DefaultPodSpecExtractor struct{}
|
||||||
|
|
||||||
|
func (DefaultPodSpecExtractor) HasPodSpec(gr schema.GroupResource) bool {
|
||||||
|
return defaultPodSpecResources[gr]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DefaultPodSpecExtractor) ExtractPodSpec(obj runtime.Object) (*metav1.ObjectMeta, *corev1.PodSpec, error) {
|
||||||
|
switch o := obj.(type) {
|
||||||
|
case *corev1.Pod:
|
||||||
|
return &o.ObjectMeta, &o.Spec, nil
|
||||||
|
case *corev1.PodTemplate:
|
||||||
|
return extractPodSpecFromTemplate(&o.Template)
|
||||||
|
case *corev1.ReplicationController:
|
||||||
|
return extractPodSpecFromTemplate(o.Spec.Template)
|
||||||
|
case *appsv1.ReplicaSet:
|
||||||
|
return extractPodSpecFromTemplate(&o.Spec.Template)
|
||||||
|
case *appsv1.Deployment:
|
||||||
|
return extractPodSpecFromTemplate(&o.Spec.Template)
|
||||||
|
case *appsv1.DaemonSet:
|
||||||
|
return extractPodSpecFromTemplate(&o.Spec.Template)
|
||||||
|
case *appsv1.StatefulSet:
|
||||||
|
return extractPodSpecFromTemplate(&o.Spec.Template)
|
||||||
|
case *batchv1.Job:
|
||||||
|
return extractPodSpecFromTemplate(&o.Spec.Template)
|
||||||
|
case *batchv1.CronJob:
|
||||||
|
return extractPodSpecFromTemplate(&o.Spec.JobTemplate.Spec.Template)
|
||||||
|
default:
|
||||||
|
return nil, nil, fmt.Errorf("unexpected object type: %s", obj.GetObjectKind().GroupVersionKind().String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (DefaultPodSpecExtractor) PodSpecResources() []schema.GroupResource {
|
||||||
|
retval := make([]schema.GroupResource, 0, len(defaultPodSpecResources))
|
||||||
|
for r := range defaultPodSpecResources {
|
||||||
|
retval = append(retval, r)
|
||||||
|
}
|
||||||
|
return retval
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractPodSpecFromTemplate(template *corev1.PodTemplateSpec) (*metav1.ObjectMeta, *corev1.PodSpec, error) {
|
||||||
|
if template == nil {
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
return &template.ObjectMeta, &template.Spec, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CompleteConfiguration() sets up default or derived configuration.
|
||||||
|
func (a *Admission) CompleteConfiguration() error {
|
||||||
|
if a.Configuration != nil {
|
||||||
|
if p, err := admissionapi.ToPolicy(a.Configuration.Defaults); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
a.defaultPolicy = p
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.PodSpecExtractor == nil {
|
||||||
|
a.PodSpecExtractor = &DefaultPodSpecExtractor{}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidateConfiguration() ensures all required fields are set with valid values.
|
||||||
|
func (a *Admission) ValidateConfiguration() error {
|
||||||
|
if a.Configuration == nil {
|
||||||
|
return fmt.Errorf("configuration required")
|
||||||
|
} else if errs := validation.ValidatePodSecurityConfiguration(a.Configuration); len(errs) > 0 {
|
||||||
|
return errs.ToAggregate()
|
||||||
|
} else {
|
||||||
|
if p, err := admissionapi.ToPolicy(a.Configuration.Defaults); err != nil {
|
||||||
|
return err
|
||||||
|
} else if !reflect.DeepEqual(p, a.defaultPolicy) {
|
||||||
|
return fmt.Errorf("default policy does not match; CompleteConfiguration() was not called before ValidateConfiguration()")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO: check metrics is non-nil?
|
||||||
|
if a.PodSpecExtractor == nil {
|
||||||
|
return fmt.Errorf("PodSpecExtractor required")
|
||||||
|
}
|
||||||
|
if a.Evaluator == nil {
|
||||||
|
return fmt.Errorf("Evaluator required")
|
||||||
|
}
|
||||||
|
if a.NamespaceGetter == nil {
|
||||||
|
return fmt.Errorf("NamespaceGetter required")
|
||||||
|
}
|
||||||
|
if a.PodLister == nil {
|
||||||
|
return fmt.Errorf("PodLister required")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate admits an API request.
|
||||||
|
// The objects in admission attributes are expected to be external v1 objects that we care about.
|
||||||
|
func (a *Admission) Validate(ctx context.Context, attrs admission.Attributes) admissionv1.AdmissionResponse {
|
||||||
|
var response admissionv1.AdmissionResponse
|
||||||
|
switch attrs.GetResource().GroupResource() {
|
||||||
|
case corev1.Resource("namespaces"):
|
||||||
|
response = a.ValidateNamespace(ctx, attrs)
|
||||||
|
case corev1.Resource("pods"):
|
||||||
|
response = a.ValidatePod(ctx, attrs)
|
||||||
|
default:
|
||||||
|
response = a.ValidatePodController(ctx, attrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: record metrics.
|
||||||
|
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admission) ValidateNamespace(ctx context.Context, attrs admission.Attributes) admissionv1.AdmissionResponse {
|
||||||
|
// short-circuit on subresources
|
||||||
|
if attrs.GetSubresource() != "" {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
namespace, ok := attrs.GetObject().(*corev1.Namespace)
|
||||||
|
if !ok {
|
||||||
|
klog.InfoS("failed to assert namespace type", "type", reflect.TypeOf(attrs.GetObject()))
|
||||||
|
return internalErrorResponse("failed to decode namespace")
|
||||||
|
}
|
||||||
|
|
||||||
|
newPolicy, newErr := a.PolicyToEvaluate(namespace.Labels)
|
||||||
|
|
||||||
|
switch attrs.GetOperation() {
|
||||||
|
case admission.Create:
|
||||||
|
// require valid labels on create
|
||||||
|
if newErr != nil {
|
||||||
|
return invalidResponse(newErr.Error())
|
||||||
|
}
|
||||||
|
return allowedResponse()
|
||||||
|
|
||||||
|
case admission.Update:
|
||||||
|
// if update, check if policy labels changed
|
||||||
|
oldNamespace, ok := attrs.GetOldObject().(*corev1.Namespace)
|
||||||
|
if !ok {
|
||||||
|
klog.InfoS("failed to assert old namespace type", "type", reflect.TypeOf(attrs.GetOldObject()))
|
||||||
|
return internalErrorResponse("failed to decode old namespace")
|
||||||
|
}
|
||||||
|
oldPolicy, oldErr := a.PolicyToEvaluate(oldNamespace.Labels)
|
||||||
|
|
||||||
|
// require valid labels on update if they have changed
|
||||||
|
if newErr != nil && (oldErr == nil || newErr.Error() != oldErr.Error()) {
|
||||||
|
return invalidResponse(newErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip dry-running pods:
|
||||||
|
// * if the enforce policy is unchanged
|
||||||
|
// * if the new enforce policy is privileged
|
||||||
|
// * if the new enforce is the same version and level was relaxed
|
||||||
|
// * for exempt namespaces
|
||||||
|
if newPolicy.Enforce == oldPolicy.Enforce {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
if newPolicy.Enforce.Level == api.LevelPrivileged {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
if newPolicy.Enforce.Version == oldPolicy.Enforce.Version &&
|
||||||
|
api.CompareLevels(newPolicy.Enforce.Level, oldPolicy.Enforce.Level) < 1 {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
if a.exemptNamespace(attrs.GetNamespace()) {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
response := allowedResponse()
|
||||||
|
response.Warnings = a.EvaluatePodsInNamespace(ctx, namespace.Name, newPolicy.Enforce)
|
||||||
|
return response
|
||||||
|
|
||||||
|
default:
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignoredPodSubresources is a set of ignored Pod subresources.
|
||||||
|
// Any other subresource is expected to be a *v1.Pod type and is evaluated.
|
||||||
|
// This ensures a version skewed webhook fails safe and denies an unknown pod subresource that allows modifying the pod spec.
|
||||||
|
var ignoredPodSubresources = map[string]bool{
|
||||||
|
"exec": true,
|
||||||
|
"attach": true,
|
||||||
|
"binding": true,
|
||||||
|
"eviction": true,
|
||||||
|
"log": true,
|
||||||
|
"portforward": true,
|
||||||
|
"proxy": true,
|
||||||
|
"status": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admission) ValidatePod(ctx context.Context, attrs admission.Attributes) admissionv1.AdmissionResponse {
|
||||||
|
// short-circuit on ignored subresources
|
||||||
|
if ignoredPodSubresources[attrs.GetSubresource()] {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
// short-circuit on exempt namespaces and users
|
||||||
|
if a.exemptNamespace(attrs.GetNamespace()) || a.exemptUser(attrs.GetUserInfo().GetName()) {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
pod, ok := attrs.GetObject().(*corev1.Pod)
|
||||||
|
if !ok {
|
||||||
|
klog.InfoS("failed to assert pod type", "type", reflect.TypeOf(attrs.GetObject()))
|
||||||
|
return internalErrorResponse("failed to decode pod")
|
||||||
|
}
|
||||||
|
enforce := true
|
||||||
|
if attrs.GetOperation() == admission.Update {
|
||||||
|
oldPod, ok := attrs.GetOldObject().(*corev1.Pod)
|
||||||
|
if !ok {
|
||||||
|
klog.InfoS("failed to assert old pod type", "type", reflect.TypeOf(attrs.GetOldObject()))
|
||||||
|
return internalErrorResponse("failed to decode old pod")
|
||||||
|
}
|
||||||
|
if !isSignificantPodUpdate(pod, oldPod) {
|
||||||
|
// Nothing we care about changed, so always allow the update.
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return a.EvaluatePod(ctx, attrs.GetNamespace(), &pod.ObjectMeta, &pod.Spec, enforce)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admission) ValidatePodController(ctx context.Context, attrs admission.Attributes) admissionv1.AdmissionResponse {
|
||||||
|
// short-circuit on subresources
|
||||||
|
if attrs.GetSubresource() != "" {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
// short-circuit on exempt namespaces and users
|
||||||
|
if a.exemptNamespace(attrs.GetNamespace()) || a.exemptUser(attrs.GetUserInfo().GetName()) {
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
podMetadata, podSpec, err := a.PodSpecExtractor.ExtractPodSpec(attrs.GetObject())
|
||||||
|
if err != nil {
|
||||||
|
klog.ErrorS(err, "failed to extract pod spec")
|
||||||
|
return internalErrorResponse("failed to extract pod template")
|
||||||
|
}
|
||||||
|
if podMetadata == nil && podSpec == nil {
|
||||||
|
// if a controller with an optional pod spec does not contain a pod spec, skip validation
|
||||||
|
return allowedResponse()
|
||||||
|
}
|
||||||
|
return a.EvaluatePod(ctx, attrs.GetNamespace(), podMetadata, podSpec, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EvaluatePod looks up the policy for the pods namespace, and checks it 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 {
|
||||||
|
// 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))
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
response := allowedResponse()
|
||||||
|
if enforce {
|
||||||
|
if result := policy.AggregateCheckResults(a.Evaluator.EvaluatePod(nsPolicy.Enforce, podMetadata, podSpec)); !result.Allowed {
|
||||||
|
response = forbiddenResponse(result.ForbiddenDetail())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reuse previous evaluation if audit level+version is the same as enforce level+version
|
||||||
|
if result := policy.AggregateCheckResults(a.Evaluator.EvaluatePod(nsPolicy.Audit, podMetadata, podSpec)); !result.Allowed {
|
||||||
|
auditAnnotations["audit"] = result.ForbiddenDetail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: reuse previous evaluation if warn level+version is the same as audit or enforce level+version
|
||||||
|
if result := policy.AggregateCheckResults(a.Evaluator.EvaluatePod(nsPolicy.Warn, podMetadata, podSpec)); !result.Allowed {
|
||||||
|
// TODO: Craft a better user-facing warning message
|
||||||
|
response.Warnings = append(response.Warnings, fmt.Sprintf("Pod violates PodSecurity profile %s: %s", nsPolicy.Warn.String(), result.ForbiddenDetail()))
|
||||||
|
}
|
||||||
|
|
||||||
|
response.AuditAnnotations = auditAnnotations
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admission) EvaluatePodsInNamespace(ctx context.Context, namespace string, enforce api.LevelVersion) []string {
|
||||||
|
timeout := namespacePodCheckTimeout
|
||||||
|
if deadline, ok := ctx.Deadline(); ok {
|
||||||
|
timeRemaining := time.Duration(0.9 * float64(time.Until(deadline))) // Leave a little time to respond.
|
||||||
|
if timeout > timeRemaining {
|
||||||
|
timeout = timeRemaining
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deadline := time.Now().Add(timeout)
|
||||||
|
ctx, cancel := context.WithDeadline(ctx, deadline)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
pods, err := a.PodLister.ListPods(ctx, namespace)
|
||||||
|
if err != nil {
|
||||||
|
klog.ErrorS(err, "Failed to list pods", "namespace", namespace)
|
||||||
|
return []string{"Failed to list pods"}
|
||||||
|
}
|
||||||
|
|
||||||
|
var warnings []string
|
||||||
|
if len(pods) > namespaceMaxPodsToCheck {
|
||||||
|
warnings = append(warnings, fmt.Sprintf("Large namespace: only checking the first %d of %d pods", namespaceMaxPodsToCheck, len(pods)))
|
||||||
|
pods = pods[0:namespaceMaxPodsToCheck]
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, pod := range pods {
|
||||||
|
// short-circuit on exempt runtimeclass
|
||||||
|
if a.exemptRuntimeClass(pod.Spec.RuntimeClassName) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := policy.AggregateCheckResults(a.Evaluator.EvaluatePod(enforce, &pod.ObjectMeta, &pod.Spec))
|
||||||
|
if !r.Allowed {
|
||||||
|
// TODO: consider aggregating results (e.g. multiple pods failed for the same reasons)
|
||||||
|
warnings = append(warnings, fmt.Sprintf("%s: %s", pod.Name, r.ForbiddenReason()))
|
||||||
|
}
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
return append(warnings, fmt.Sprintf("Timeout reached after checking %d pods", i+1))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warnings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admission) PolicyToEvaluate(labels map[string]string) (api.Policy, error) {
|
||||||
|
return api.PolicyToEvaluate(labels, a.defaultPolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
// allowResponse is the response used when the admission decision is allow.
|
||||||
|
func allowedResponse() admissionv1.AdmissionResponse {
|
||||||
|
return admissionv1.AdmissionResponse{Allowed: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
// forbiddenResponse is the response used when the admission decision is deny for policy violations.
|
||||||
|
func forbiddenResponse(msg string) admissionv1.AdmissionResponse {
|
||||||
|
return admissionv1.AdmissionResponse{
|
||||||
|
Allowed: false,
|
||||||
|
Result: &metav1.Status{
|
||||||
|
Status: metav1.StatusFailure,
|
||||||
|
Reason: metav1.StatusReasonForbidden,
|
||||||
|
Message: msg,
|
||||||
|
Code: http.StatusForbidden,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// invalidResponse is the response used for namespace requests when namespace labels are invalid.
|
||||||
|
func invalidResponse(msg string) admissionv1.AdmissionResponse {
|
||||||
|
return admissionv1.AdmissionResponse{
|
||||||
|
Allowed: false,
|
||||||
|
Result: &metav1.Status{
|
||||||
|
Status: metav1.StatusFailure,
|
||||||
|
Reason: metav1.StatusReasonInvalid,
|
||||||
|
Message: msg,
|
||||||
|
Code: 422,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// internalErrorResponse is the response used for unexpected errors
|
||||||
|
func internalErrorResponse(msg string) admissionv1.AdmissionResponse {
|
||||||
|
return admissionv1.AdmissionResponse{
|
||||||
|
Allowed: false,
|
||||||
|
Result: &metav1.Status{
|
||||||
|
Status: metav1.StatusFailure,
|
||||||
|
Reason: metav1.StatusReasonInternalError,
|
||||||
|
Message: msg,
|
||||||
|
Code: http.StatusInternalServerError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSignificantPodUpdate determines whether a pod update should trigger a policy evaluation.
|
||||||
|
// Relevant mutable pod fields as of 1.21 are image and seccomp annotations:
|
||||||
|
// * https://github.com/kubernetes/kubernetes/blob/release-1.21/pkg/apis/core/validation/validation.go#L3947-L3949
|
||||||
|
func isSignificantPodUpdate(pod, oldPod *corev1.Pod) bool {
|
||||||
|
if pod.Annotations[corev1.SeccompPodAnnotationKey] != oldPod.Annotations[corev1.SeccompPodAnnotationKey] {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(pod.Spec.Containers) != len(oldPod.Spec.Containers) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if len(pod.Spec.InitContainers) != len(oldPod.Spec.InitContainers) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for i := 0; i < len(pod.Spec.Containers); i++ {
|
||||||
|
if isSignificantContainerUpdate(&pod.Spec.Containers[i], &oldPod.Spec.Containers[i], pod.Annotations, oldPod.Annotations) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := 0; i < len(pod.Spec.InitContainers); i++ {
|
||||||
|
if isSignificantContainerUpdate(&pod.Spec.InitContainers[i], &oldPod.Spec.InitContainers[i], pod.Annotations, oldPod.Annotations) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range pod.Spec.EphemeralContainers {
|
||||||
|
var oldC *corev1.Container
|
||||||
|
for i, oc := range oldPod.Spec.EphemeralContainers {
|
||||||
|
if oc.Name == c.Name {
|
||||||
|
oldC = (*corev1.Container)(&oldPod.Spec.EphemeralContainers[i].EphemeralContainerCommon)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if oldC == nil {
|
||||||
|
return true // EphemeralContainer added
|
||||||
|
}
|
||||||
|
if isSignificantContainerUpdate((*corev1.Container)(&c.EphemeralContainerCommon), oldC, pod.Annotations, oldPod.Annotations) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// isSignificantContainerUpdate determines whether a container update should trigger a policy evaluation.
|
||||||
|
func isSignificantContainerUpdate(container, oldContainer *corev1.Container, annotations, oldAnnotations map[string]string) bool {
|
||||||
|
if container.Image != oldContainer.Image {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
seccompKey := corev1.SeccompContainerAnnotationKeyPrefix + container.Name
|
||||||
|
return annotations[seccompKey] != oldAnnotations[seccompKey]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Admission) exemptNamespace(namespace string) bool {
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO: consider optimizing to O(1) lookup
|
||||||
|
return containsString(namespace, a.Configuration.Exemptions.Namespaces)
|
||||||
|
}
|
||||||
|
func (a *Admission) exemptUser(username string) bool {
|
||||||
|
if len(username) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO: consider optimizing to O(1) lookup
|
||||||
|
return containsString(username, a.Configuration.Exemptions.Usernames)
|
||||||
|
}
|
||||||
|
func (a *Admission) exemptRuntimeClass(runtimeClass *string) bool {
|
||||||
|
if runtimeClass == nil || len(*runtimeClass) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// TODO: consider optimizing to O(1) lookup
|
||||||
|
return containsString(*runtimeClass, a.Configuration.Exemptions.RuntimeClasses)
|
||||||
|
}
|
||||||
|
func containsString(needle string, haystack []string) bool {
|
||||||
|
for _, s := range haystack {
|
||||||
|
if s == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
@ -0,0 +1,465 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"context"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
|
appsv1 "k8s.io/api/apps/v1"
|
||||||
|
batchv1 "k8s.io/api/batch/v1"
|
||||||
|
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"
|
||||||
|
admissionapi "k8s.io/pod-security-admission/admission/api"
|
||||||
|
"k8s.io/pod-security-admission/api"
|
||||||
|
"k8s.io/pod-security-admission/policy"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultExtractPodSpec(t *testing.T) {
|
||||||
|
metadata := metav1.ObjectMeta{
|
||||||
|
Name: "foo-pod",
|
||||||
|
}
|
||||||
|
spec := corev1.PodSpec{
|
||||||
|
Containers: []corev1.Container{{
|
||||||
|
Name: "foo-container",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
objects := []runtime.Object{
|
||||||
|
&corev1.Pod{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
&corev1.PodTemplate{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-template"},
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&corev1.ReplicationController{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-rc"},
|
||||||
|
Spec: corev1.ReplicationControllerSpec{
|
||||||
|
Template: &corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&appsv1.ReplicaSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-rs"},
|
||||||
|
Spec: appsv1.ReplicaSetSpec{
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&appsv1.Deployment{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-deployment"},
|
||||||
|
Spec: appsv1.DeploymentSpec{
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&appsv1.StatefulSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-ss"},
|
||||||
|
Spec: appsv1.StatefulSetSpec{
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&appsv1.DaemonSet{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-ds"},
|
||||||
|
Spec: appsv1.DaemonSetSpec{
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&batchv1.Job{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-job"},
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&batchv1.CronJob{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "foo-cronjob"},
|
||||||
|
Spec: batchv1.CronJobSpec{
|
||||||
|
JobTemplate: batchv1.JobTemplateSpec{
|
||||||
|
Spec: batchv1.JobSpec{
|
||||||
|
Template: corev1.PodTemplateSpec{
|
||||||
|
ObjectMeta: metadata,
|
||||||
|
Spec: spec,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
extractor := &DefaultPodSpecExtractor{}
|
||||||
|
for _, obj := range objects {
|
||||||
|
name := obj.(metav1.Object).GetName()
|
||||||
|
actualMetadata, actualSpec, err := extractor.ExtractPodSpec(obj)
|
||||||
|
assert.NoError(t, err, name)
|
||||||
|
assert.Equal(t, &metadata, actualMetadata, "%s: Metadata mismatch", name)
|
||||||
|
assert.Equal(t, &spec, actualSpec, "%s: PodSpec mismatch", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
service := &corev1.Service{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "foo-svc",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_, _, err := extractor.ExtractPodSpec(service)
|
||||||
|
assert.Error(t, err, "service should not have an extractable pod spec")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDefaultHasPodSpec(t *testing.T) {
|
||||||
|
podLikeResources := []schema.GroupResource{
|
||||||
|
corev1.Resource("pods"),
|
||||||
|
corev1.Resource("replicationcontrollers"),
|
||||||
|
corev1.Resource("podtemplates"),
|
||||||
|
appsv1.Resource("replicasets"),
|
||||||
|
appsv1.Resource("deployments"),
|
||||||
|
appsv1.Resource("statefulsets"),
|
||||||
|
appsv1.Resource("daemonsets"),
|
||||||
|
batchv1.Resource("jobs"),
|
||||||
|
batchv1.Resource("cronjobs"),
|
||||||
|
}
|
||||||
|
extractor := &DefaultPodSpecExtractor{}
|
||||||
|
for _, gr := range podLikeResources {
|
||||||
|
assert.True(t, extractor.HasPodSpec(gr), gr.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
nonPodResources := []schema.GroupResource{
|
||||||
|
corev1.Resource("services"),
|
||||||
|
admissionv1.Resource("admissionreviews"),
|
||||||
|
appsv1.Resource("foobars"),
|
||||||
|
}
|
||||||
|
for _, gr := range nonPodResources {
|
||||||
|
assert.False(t, extractor.HasPodSpec(gr), gr.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testEvaluator struct {
|
||||||
|
lv api.LevelVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testEvaluator) EvaluatePod(lv api.LevelVersion, meta *metav1.ObjectMeta, spec *corev1.PodSpec) []policy.CheckResult {
|
||||||
|
t.lv = lv
|
||||||
|
if meta.Annotations["error"] != "" {
|
||||||
|
return []policy.CheckResult{{Allowed: false, ForbiddenReason: meta.Annotations["error"]}}
|
||||||
|
} else {
|
||||||
|
return []policy.CheckResult{{Allowed: true}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testPodLister struct {
|
||||||
|
called bool
|
||||||
|
pods []*corev1.Pod
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *testPodLister) ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error) {
|
||||||
|
t.called = true
|
||||||
|
return t.pods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidateNamespace(t *testing.T) {
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
exemptNamespaces []string
|
||||||
|
exemptRuntimeClasses []string
|
||||||
|
// override default policy
|
||||||
|
defaultPolicy *api.Policy
|
||||||
|
// request subresource
|
||||||
|
subresource string
|
||||||
|
// labels for the new namespace
|
||||||
|
newLabels map[string]string
|
||||||
|
// labels for the old namespace (only used if update=true)
|
||||||
|
oldLabels map[string]string
|
||||||
|
// list of pods to return
|
||||||
|
pods []*corev1.Pod
|
||||||
|
|
||||||
|
expectAllowed bool
|
||||||
|
expectError string
|
||||||
|
expectListPods bool
|
||||||
|
expectEvaluate api.LevelVersion
|
||||||
|
expectWarnings []string
|
||||||
|
}{
|
||||||
|
// creation tests, just validate labels
|
||||||
|
{
|
||||||
|
name: "create privileged",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelPrivileged), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create baseline",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline)},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create restricted",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline)},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create malformed level",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: "unknown"},
|
||||||
|
expectAllowed: false,
|
||||||
|
expectError: `must be one of privileged, baseline, restricted`,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "create malformed version",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelPrivileged), api.EnforceVersionLabel: "unknown"},
|
||||||
|
expectAllowed: false,
|
||||||
|
expectError: `must be "latest" or "v1.x"`,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// update tests that don't tighten effective policy, no pod list/evaluate
|
||||||
|
{
|
||||||
|
name: "update no-op",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update no-op malformed level",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: "unknown"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: "unknown"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update no-op malformed version",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline), api.EnforceVersionLabel: "unknown"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline), api.EnforceVersionLabel: "unknown"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update relax level identical version",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update relax level explicit latest",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline), api.EnforceVersionLabel: "latest"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "latest"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update relax level implicit latest",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline)},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted)},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update to explicit privileged",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelPrivileged)},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update to implicit privileged",
|
||||||
|
newLabels: map[string]string{},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update exempt to restricted",
|
||||||
|
exemptNamespaces: []string{"test"},
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
oldLabels: map[string]string{},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// update tests that introduce labels errors
|
||||||
|
{
|
||||||
|
name: "update malformed level",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: "unknown"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: false,
|
||||||
|
expectError: `must be one of privileged, baseline, restricted`,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update malformed version",
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelPrivileged), api.EnforceVersionLabel: "unknown"},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
expectAllowed: false,
|
||||||
|
expectError: `must be "latest" or "v1.x"`,
|
||||||
|
expectListPods: false,
|
||||||
|
},
|
||||||
|
|
||||||
|
// update tests that tighten effective policy
|
||||||
|
{
|
||||||
|
name: "update to implicit restricted",
|
||||||
|
newLabels: map[string]string{},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline), api.EnforceVersionLabel: "v1.0"},
|
||||||
|
defaultPolicy: &api.Policy{Enforce: api.LevelVersion{Level: api.LevelRestricted, Version: api.LatestVersion()}},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: true,
|
||||||
|
expectEvaluate: api.LevelVersion{Level: api.LevelRestricted, Version: api.LatestVersion()},
|
||||||
|
expectWarnings: []string{"noruntimeclasspod: message", "runtimeclass1pod: message", "runtimeclass2pod: message"},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "update with runtimeclass exempt pods",
|
||||||
|
exemptRuntimeClasses: []string{"runtimeclass1"},
|
||||||
|
newLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelRestricted)},
|
||||||
|
oldLabels: map[string]string{api.EnforceLevelLabel: string(api.LevelBaseline)},
|
||||||
|
expectAllowed: true,
|
||||||
|
expectListPods: true,
|
||||||
|
expectEvaluate: api.LevelVersion{Level: api.LevelRestricted, Version: api.LatestVersion()},
|
||||||
|
expectWarnings: []string{"noruntimeclasspod: message", "runtimeclass2pod: message"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// TODO: test for aggregating pods with identical warnings
|
||||||
|
// TODO: test for bounding evalution time with a warning
|
||||||
|
// TODO: test for bounding pod count with a warning
|
||||||
|
// TODO: test for prioritizing evaluating pods from unique controllers
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
newObject := &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
Labels: tc.newLabels,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
var operation = admission.Create
|
||||||
|
var oldObject runtime.Object
|
||||||
|
if tc.oldLabels != nil {
|
||||||
|
operation = admission.Update
|
||||||
|
oldObject = &corev1.Namespace{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "test",
|
||||||
|
Labels: tc.oldLabels,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
attrs := admission.NewAttributesRecord(
|
||||||
|
newObject,
|
||||||
|
oldObject,
|
||||||
|
schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Namespace"},
|
||||||
|
newObject.Name,
|
||||||
|
newObject.Name,
|
||||||
|
schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"},
|
||||||
|
tc.subresource,
|
||||||
|
operation,
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
defaultPolicy := api.Policy{
|
||||||
|
Enforce: api.LevelVersion{
|
||||||
|
Level: api.LevelPrivileged,
|
||||||
|
Version: api.LatestVersion(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if tc.defaultPolicy != nil {
|
||||||
|
defaultPolicy = *tc.defaultPolicy
|
||||||
|
}
|
||||||
|
|
||||||
|
pods := tc.pods
|
||||||
|
if pods == nil {
|
||||||
|
pods = []*corev1.Pod{
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "noruntimeclasspod", Annotations: map[string]string{"error": "message"}},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "runtimeclass1pod", Annotations: map[string]string{"error": "message"}},
|
||||||
|
Spec: corev1.PodSpec{RuntimeClassName: pointer.String("runtimeclass1")},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "runtimeclass2pod", Annotations: map[string]string{"error": "message"}},
|
||||||
|
Spec: corev1.PodSpec{RuntimeClassName: pointer.String("runtimeclass2")},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
podLister := &testPodLister{pods: pods}
|
||||||
|
evaluator := &testEvaluator{}
|
||||||
|
a := &Admission{
|
||||||
|
PodLister: podLister,
|
||||||
|
Evaluator: evaluator,
|
||||||
|
Configuration: &admissionapi.PodSecurityConfiguration{
|
||||||
|
Exemptions: admissionapi.PodSecurityExemptions{
|
||||||
|
Namespaces: tc.exemptNamespaces,
|
||||||
|
RuntimeClasses: tc.exemptRuntimeClasses,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultPolicy: defaultPolicy,
|
||||||
|
}
|
||||||
|
result := a.ValidateNamespace(context.TODO(), attrs)
|
||||||
|
if result.Allowed != tc.expectAllowed {
|
||||||
|
t.Errorf("expected allowed=%v, got %v", tc.expectAllowed, result.Allowed)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultError := ""
|
||||||
|
if result.Result != nil {
|
||||||
|
resultError = result.Result.Message
|
||||||
|
}
|
||||||
|
if (len(resultError) > 0) != (len(tc.expectError) > 0) {
|
||||||
|
t.Errorf("expected error=%v, got %v", tc.expectError, resultError)
|
||||||
|
}
|
||||||
|
if len(tc.expectError) > 0 && !strings.Contains(resultError, tc.expectError) {
|
||||||
|
t.Errorf("expected error containing '%s', got %s", tc.expectError, resultError)
|
||||||
|
}
|
||||||
|
if podLister.called != tc.expectListPods {
|
||||||
|
t.Errorf("expected getPods=%v, got %v", tc.expectListPods, podLister.called)
|
||||||
|
}
|
||||||
|
if evaluator.lv != tc.expectEvaluate {
|
||||||
|
t.Errorf("expected to evaluate %v, got %v", tc.expectEvaluate, evaluator.lv)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(result.Warnings, tc.expectWarnings) {
|
||||||
|
t.Errorf("expected warnings:\n%v\ngot\n%v", tc.expectWarnings, result.Warnings)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
18
staging/src/k8s.io/pod-security-admission/admission/doc.go
Normal file
18
staging/src/k8s.io/pod-security-admission/admission/doc.go
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
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 contains PodSecurity admission logic
|
||||||
|
package admission
|
@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NamespaceGetterFromClient(client kubernetes.Interface) NamespaceGetter {
|
||||||
|
return &namespaceGetter{client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NamespaceGetterFromListerAndClient(lister corev1listers.NamespaceLister, client kubernetes.Interface) NamespaceGetter {
|
||||||
|
return &namespaceGetter{lister: lister, client: client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type namespaceGetter struct {
|
||||||
|
lister corev1listers.NamespaceLister
|
||||||
|
client kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *namespaceGetter) GetNamespace(ctx context.Context, name string) (namespace *corev1.Namespace, err error) {
|
||||||
|
if n.lister != nil {
|
||||||
|
namespace, err := n.lister.Get(name)
|
||||||
|
if err == nil || !apierrors.IsNotFound(err) {
|
||||||
|
return namespace, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return n.client.CoreV1().Namespaces().Get(ctx, name, metav1.GetOptions{})
|
||||||
|
}
|
61
staging/src/k8s.io/pod-security-admission/admission/pods.go
Normal file
61
staging/src/k8s.io/pod-security-admission/admission/pods.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
/*
|
||||||
|
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 (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
corev1 "k8s.io/api/core/v1"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
corev1listers "k8s.io/client-go/listers/core/v1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// PodListerFromClient returns a PodLister that does live lists using the provided client.
|
||||||
|
func PodListerFromClient(client kubernetes.Interface) PodLister {
|
||||||
|
return &clientPodLister{client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type clientPodLister struct {
|
||||||
|
client kubernetes.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *clientPodLister) ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error) {
|
||||||
|
list, err := p.client.CoreV1().Pods(namespace).List(ctx, metav1.ListOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
pods := make([]*corev1.Pod, len(list.Items))
|
||||||
|
for i := range list.Items {
|
||||||
|
pods[i] = &list.Items[i]
|
||||||
|
}
|
||||||
|
return pods, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// PodListerFromInformer returns a PodLister that does cached lists using the provided lister.
|
||||||
|
func PodListerFromInformer(lister corev1listers.PodLister) PodLister {
|
||||||
|
return &informerPodLister{lister}
|
||||||
|
}
|
||||||
|
|
||||||
|
type informerPodLister struct {
|
||||||
|
lister corev1listers.PodLister
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *informerPodLister) ListPods(ctx context.Context, namespace string) ([]*corev1.Pod, error) {
|
||||||
|
return p.lister.Pods(namespace).List(labels.Everything())
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user