mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 19:01:49 +00:00
[PodSecurity] Include error audit annotation on all non-forbidden errors
This commit is contained in:
parent
98c86b350c
commit
c3398729e0
@ -19,7 +19,6 @@ package admission
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"sort"
|
||||
"time"
|
||||
@ -233,13 +232,13 @@ func (a *Admission) ValidateNamespace(ctx context.Context, attrs api.Attributes)
|
||||
}
|
||||
obj, err := attrs.GetObject()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to get object")
|
||||
return internalErrorResponse("failed to get object")
|
||||
klog.ErrorS(err, "failed to decode object")
|
||||
return errorResponse(err, &apierrors.NewBadRequest("failed to decode object").ErrStatus)
|
||||
}
|
||||
namespace, ok := obj.(*corev1.Namespace)
|
||||
if !ok {
|
||||
klog.InfoS("failed to assert namespace type", "type", reflect.TypeOf(obj))
|
||||
return badRequestResponse("failed to decode namespace")
|
||||
return errorResponse(nil, &apierrors.NewBadRequest("failed to decode namespace").ErrStatus)
|
||||
}
|
||||
|
||||
newPolicy, newErrs := a.PolicyToEvaluate(namespace.Labels)
|
||||
@ -257,12 +256,12 @@ func (a *Admission) ValidateNamespace(ctx context.Context, attrs api.Attributes)
|
||||
oldObj, err := attrs.GetOldObject()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to decode old object")
|
||||
return badRequestResponse("failed to decode old object")
|
||||
return errorResponse(err, &apierrors.NewBadRequest("failed to decode old object").ErrStatus)
|
||||
}
|
||||
oldNamespace, ok := oldObj.(*corev1.Namespace)
|
||||
if !ok {
|
||||
klog.InfoS("failed to assert old namespace type", "type", reflect.TypeOf(oldObj))
|
||||
return badRequestResponse("failed to decode old namespace")
|
||||
return errorResponse(nil, &apierrors.NewBadRequest("failed to decode old namespace").ErrStatus)
|
||||
}
|
||||
oldPolicy, oldErrs := a.PolicyToEvaluate(oldNamespace.Labels)
|
||||
|
||||
@ -335,7 +334,7 @@ func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admi
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to fetch pod namespace", "namespace", attrs.GetNamespace())
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
return internalErrorResponse(fmt.Sprintf("failed to lookup namespace %s", attrs.GetNamespace()))
|
||||
return errorResponse(err, &apierrors.NewInternalError(fmt.Errorf("failed to lookup namespace %s", attrs.GetNamespace())).ErrStatus)
|
||||
}
|
||||
nsPolicy, nsPolicyErrs := a.PolicyToEvaluate(namespace.Labels)
|
||||
if len(nsPolicyErrs) == 0 && nsPolicy.Enforce.Level == api.LevelPrivileged && nsPolicy.Warn.Level == api.LevelPrivileged && nsPolicy.Audit.Level == api.LevelPrivileged {
|
||||
@ -347,26 +346,26 @@ func (a *Admission) ValidatePod(ctx context.Context, attrs api.Attributes) *admi
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to decode object")
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
return badRequestResponse("failed to decode object")
|
||||
return errorResponse(err, &apierrors.NewBadRequest("failed to decode object").ErrStatus)
|
||||
}
|
||||
pod, ok := obj.(*corev1.Pod)
|
||||
if !ok {
|
||||
klog.InfoS("failed to assert pod type", "type", reflect.TypeOf(obj))
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
return badRequestResponse("failed to decode pod")
|
||||
return errorResponse(nil, &apierrors.NewBadRequest("failed to decode pod").ErrStatus)
|
||||
}
|
||||
if attrs.GetOperation() == admissionv1.Update {
|
||||
oldObj, err := attrs.GetOldObject()
|
||||
if err != nil {
|
||||
klog.ErrorS(err, "failed to decode old object")
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
return badRequestResponse("failed to decode old object")
|
||||
return errorResponse(err, &apierrors.NewBadRequest("failed to decode old object").ErrStatus)
|
||||
}
|
||||
oldPod, ok := oldObj.(*corev1.Pod)
|
||||
if !ok {
|
||||
klog.InfoS("failed to assert old pod type", "type", reflect.TypeOf(oldObj))
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
return badRequestResponse("failed to decode old pod")
|
||||
return errorResponse(nil, &apierrors.NewBadRequest("failed to decode old pod").ErrStatus)
|
||||
}
|
||||
if !isSignificantPodUpdate(pod, oldPod) {
|
||||
// Nothing we care about changed, so always allow the update.
|
||||
@ -401,7 +400,7 @@ func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attribu
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
response := allowedResponse()
|
||||
response.AuditAnnotations = map[string]string{
|
||||
"error": fmt.Sprintf("failed to lookup namespace %s", attrs.GetNamespace()),
|
||||
"error": fmt.Sprintf("failed to lookup namespace %s: %v", attrs.GetNamespace(), err),
|
||||
}
|
||||
return response
|
||||
}
|
||||
@ -416,7 +415,7 @@ func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attribu
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
response := allowedResponse()
|
||||
response.AuditAnnotations = map[string]string{
|
||||
"error": "failed to decode object",
|
||||
"error": fmt.Sprintf("failed to decode object: %v", err),
|
||||
}
|
||||
return response
|
||||
}
|
||||
@ -426,7 +425,7 @@ func (a *Admission) ValidatePodController(ctx context.Context, attrs api.Attribu
|
||||
a.Metrics.RecordError(true, attrs)
|
||||
response := allowedResponse()
|
||||
response.AuditAnnotations = map[string]string{
|
||||
"error": "failed to extract pod template",
|
||||
"error": fmt.Sprintf("failed to extract pod template: %v", err),
|
||||
}
|
||||
return response
|
||||
}
|
||||
@ -464,8 +463,8 @@ func (a *Admission) EvaluatePod(ctx context.Context, nsPolicy api.Policy, nsPoli
|
||||
|
||||
result := policy.AggregateCheckResults(a.Evaluator.EvaluatePod(nsPolicy.Enforce, podMetadata, podSpec))
|
||||
if !result.Allowed {
|
||||
response = forbiddenResponse(fmt.Sprintf(
|
||||
"pod violates PodSecurity %q: %s",
|
||||
response = forbiddenResponse(attrs, fmt.Errorf(
|
||||
"violates PodSecurity %q: %s",
|
||||
nsPolicy.Enforce.String(),
|
||||
result.ForbiddenDetail(),
|
||||
))
|
||||
@ -613,76 +612,6 @@ func (a *Admission) PolicyToEvaluate(labels map[string]string) (api.Policy, fiel
|
||||
return api.PolicyToEvaluate(labels, a.defaultPolicy)
|
||||
}
|
||||
|
||||
var (
|
||||
_sharedAllowedResponse = allowedResponse()
|
||||
_sharedAllowedByUserExemptionResponse = allowedByExemptResponse("user")
|
||||
_sharedAllowedByNamespaceExemptionResponse = allowedByExemptResponse("namespace")
|
||||
_sharedAllowedByRuntimeClassExemptionResponse = allowedByExemptResponse("runtimeClass")
|
||||
)
|
||||
|
||||
func sharedAllowedResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedResponse
|
||||
}
|
||||
|
||||
func sharedAllowedByUserExemptionResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedByUserExemptionResponse
|
||||
}
|
||||
|
||||
func sharedAllowedByNamespaceExemptionResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedByNamespaceExemptionResponse
|
||||
}
|
||||
|
||||
func sharedAllowedByRuntimeClassExemptionResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedByRuntimeClassExemptionResponse
|
||||
}
|
||||
|
||||
// allowedResponse is the response used when the admission decision is allow.
|
||||
func allowedResponse() *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{Allowed: true}
|
||||
}
|
||||
|
||||
func allowedByExemptResponse(exemptionReason string) *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
AuditAnnotations: map[string]string{api.ExemptionReasonAnnotationKey: exemptionReason},
|
||||
}
|
||||
}
|
||||
|
||||
func failureResponse(msg string, reason metav1.StatusReason, code int32) *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &metav1.Status{
|
||||
Status: metav1.StatusFailure,
|
||||
Reason: reason,
|
||||
Message: msg,
|
||||
Code: code,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// forbiddenResponse is the response used when the admission decision is deny for policy violations.
|
||||
func forbiddenResponse(msg string) *admissionv1.AdmissionResponse {
|
||||
return failureResponse(msg, metav1.StatusReasonForbidden, http.StatusForbidden)
|
||||
}
|
||||
|
||||
// invalidResponse is the response used for namespace requests when namespace labels are invalid.
|
||||
func invalidResponse(attrs api.Attributes, fieldErrors field.ErrorList) *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &apierrors.NewInvalid(attrs.GetKind().GroupKind(), attrs.GetName(), fieldErrors).ErrStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// badRequestResponse is the response used when a request cannot be processed.
|
||||
func badRequestResponse(msg string) *admissionv1.AdmissionResponse {
|
||||
return failureResponse(msg, metav1.StatusReasonBadRequest, http.StatusBadRequest)
|
||||
}
|
||||
|
||||
// internalErrorResponse is the response used for unexpected errors
|
||||
func internalErrorResponse(msg string) *admissionv1.AdmissionResponse {
|
||||
return failureResponse(msg, metav1.StatusReasonInternalError, 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
|
||||
|
@ -688,14 +688,14 @@ func TestValidatePodAndController(t *testing.T) {
|
||||
type testCase struct {
|
||||
desc string
|
||||
|
||||
namespace string
|
||||
username string
|
||||
runtimeClass string
|
||||
namespace string
|
||||
username string
|
||||
|
||||
operation admissionv1.Operation
|
||||
pod *corev1.Pod
|
||||
oldPod *corev1.Pod
|
||||
// pod and oldPod are used to populate obj and oldObj respectively, according to the test type (pod or deployment).
|
||||
pod *corev1.Pod
|
||||
oldPod *corev1.Pod
|
||||
|
||||
operation admissionv1.Operation
|
||||
resource schema.GroupVersionResource
|
||||
kind schema.GroupVersionKind
|
||||
obj runtime.Object
|
||||
@ -744,11 +744,12 @@ func TestValidatePodAndController(t *testing.T) {
|
||||
expectedAuditAnnotationKeys: []string{"exempt"},
|
||||
},
|
||||
{
|
||||
desc: "namespace not found",
|
||||
namespace: "missing-ns",
|
||||
pod: restrictedPod.DeepCopy(),
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonInternalError,
|
||||
desc: "namespace not found",
|
||||
namespace: "missing-ns",
|
||||
pod: restrictedPod.DeepCopy(),
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonInternalError,
|
||||
expectedAuditAnnotationKeys: []string{"error"},
|
||||
},
|
||||
{
|
||||
desc: "short-circuit privileged:latest (implicit)",
|
||||
@ -763,39 +764,43 @@ func TestValidatePodAndController(t *testing.T) {
|
||||
expectAllowed: true,
|
||||
},
|
||||
{
|
||||
desc: "failed decode",
|
||||
namespace: baselineNs,
|
||||
objErr: fmt.Errorf("expected (failed decode)"),
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
desc: "failed decode",
|
||||
namespace: baselineNs,
|
||||
objErr: fmt.Errorf("expected (failed decode)"),
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
expectedAuditAnnotationKeys: []string{"error"},
|
||||
},
|
||||
{
|
||||
desc: "invalid object",
|
||||
namespace: baselineNs,
|
||||
operation: admissionv1.Update,
|
||||
obj: &corev1.Namespace{},
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
desc: "invalid object",
|
||||
namespace: baselineNs,
|
||||
operation: admissionv1.Update,
|
||||
obj: &corev1.Namespace{},
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
expectedAuditAnnotationKeys: []string{"error"},
|
||||
},
|
||||
{
|
||||
desc: "failed decode old object",
|
||||
namespace: baselineNs,
|
||||
operation: admissionv1.Update,
|
||||
pod: restrictedPod.DeepCopy(),
|
||||
oldObjErr: fmt.Errorf("expected (failed decode)"),
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
skipDeployment: true, // Updates aren't special cased for controller resources.
|
||||
desc: "failed decode old object",
|
||||
namespace: baselineNs,
|
||||
operation: admissionv1.Update,
|
||||
pod: restrictedPod.DeepCopy(),
|
||||
oldObjErr: fmt.Errorf("expected (failed decode)"),
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
expectedAuditAnnotationKeys: []string{"error"},
|
||||
skipDeployment: true, // Updates aren't special cased for controller resources.
|
||||
},
|
||||
{
|
||||
desc: "invalid old object",
|
||||
namespace: baselineNs,
|
||||
operation: admissionv1.Update,
|
||||
pod: restrictedPod.DeepCopy(),
|
||||
oldObj: &corev1.Namespace{},
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
skipDeployment: true, // Updates aren't special cased for controller resources.
|
||||
desc: "invalid old object",
|
||||
namespace: baselineNs,
|
||||
operation: admissionv1.Update,
|
||||
pod: restrictedPod.DeepCopy(),
|
||||
oldObj: &corev1.Namespace{},
|
||||
expectAllowed: false,
|
||||
expectReason: metav1.StatusReasonBadRequest,
|
||||
expectedAuditAnnotationKeys: []string{"error"},
|
||||
skipDeployment: true, // Updates aren't special cased for controller resources.
|
||||
},
|
||||
{
|
||||
desc: "insignificant update",
|
||||
@ -907,12 +912,8 @@ func TestValidatePodAndController(t *testing.T) {
|
||||
deploymentTest.desc = "deployment:" + tc.desc
|
||||
deploymentTest.resource = schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}
|
||||
deploymentTest.kind = schema.GroupVersionKind{Group: "apps", Version: "v1", Kind: "Deployment"}
|
||||
deploymentTest.expectAllowed = true // Deployments policies are non-enforcing.
|
||||
deploymentTest.expectAllowed = true // PodController validation is always non-enforcing.
|
||||
deploymentTest.expectReason = ""
|
||||
if tc.expectReason != "" && tc.expectReason != metav1.StatusReasonForbidden {
|
||||
// Error case, expect an error annotation.
|
||||
deploymentTest.expectedAuditAnnotationKeys = append(deploymentTest.expectedAuditAnnotationKeys, "error")
|
||||
}
|
||||
|
||||
if tc.pod != nil {
|
||||
podTest.obj = tc.pod
|
||||
|
@ -0,0 +1,93 @@
|
||||
/*
|
||||
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"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
"k8s.io/pod-security-admission/api"
|
||||
)
|
||||
|
||||
var (
|
||||
_sharedAllowedResponse = allowedResponse()
|
||||
_sharedAllowedByUserExemptionResponse = allowedByExemptResponse("user")
|
||||
_sharedAllowedByNamespaceExemptionResponse = allowedByExemptResponse("namespace")
|
||||
_sharedAllowedByRuntimeClassExemptionResponse = allowedByExemptResponse("runtimeClass")
|
||||
)
|
||||
|
||||
func sharedAllowedResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedResponse
|
||||
}
|
||||
|
||||
func sharedAllowedByUserExemptionResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedByUserExemptionResponse
|
||||
}
|
||||
|
||||
func sharedAllowedByNamespaceExemptionResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedByNamespaceExemptionResponse
|
||||
}
|
||||
|
||||
func sharedAllowedByRuntimeClassExemptionResponse() *admissionv1.AdmissionResponse {
|
||||
return _sharedAllowedByRuntimeClassExemptionResponse
|
||||
}
|
||||
|
||||
// allowedResponse is the response used when the admission decision is allow.
|
||||
func allowedResponse() *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{Allowed: true}
|
||||
}
|
||||
|
||||
func allowedByExemptResponse(exemptionReason string) *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: true,
|
||||
AuditAnnotations: map[string]string{api.ExemptionReasonAnnotationKey: exemptionReason},
|
||||
}
|
||||
}
|
||||
|
||||
// forbiddenResponse is the response used when the admission decision is deny for policy violations.
|
||||
func forbiddenResponse(attrs api.Attributes, err error) *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &apierrors.NewForbidden(attrs.GetResource().GroupResource(), attrs.GetName(), err).ErrStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// invalidResponse is the response used for namespace requests when namespace labels are invalid.
|
||||
func invalidResponse(attrs api.Attributes, fieldErrors field.ErrorList) *admissionv1.AdmissionResponse {
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: &apierrors.NewInvalid(attrs.GetKind().GroupKind(), attrs.GetName(), fieldErrors).ErrStatus,
|
||||
}
|
||||
}
|
||||
|
||||
// errorResponse is the response used to capture generic errors.
|
||||
func errorResponse(err error, status *metav1.Status) *admissionv1.AdmissionResponse {
|
||||
var errDetail string
|
||||
if err != nil {
|
||||
errDetail = fmt.Sprintf("%s: %v", status.Message, err)
|
||||
} else {
|
||||
errDetail = status.Message
|
||||
}
|
||||
return &admissionv1.AdmissionResponse{
|
||||
Allowed: false,
|
||||
Result: status,
|
||||
AuditAnnotations: map[string]string{"error": errDetail},
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user