mirror of https://github.com/k3s-io/k3s
380 lines
14 KiB
Go
380 lines
14 KiB
Go
/*
|
|
Copyright 2016 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 podsecuritypolicy
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"sort"
|
|
"strings"
|
|
|
|
"k8s.io/klog"
|
|
|
|
policyv1beta1 "k8s.io/api/policy/v1beta1"
|
|
apiequality "k8s.io/apimachinery/pkg/api/equality"
|
|
"k8s.io/apimachinery/pkg/labels"
|
|
"k8s.io/apimachinery/pkg/util/validation/field"
|
|
"k8s.io/apiserver/pkg/admission"
|
|
genericadmissioninit "k8s.io/apiserver/pkg/admission/initializer"
|
|
"k8s.io/apiserver/pkg/authentication/user"
|
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
|
"k8s.io/client-go/informers"
|
|
policylisters "k8s.io/client-go/listers/policy/v1beta1"
|
|
api "k8s.io/kubernetes/pkg/apis/core"
|
|
"k8s.io/kubernetes/pkg/apis/extensions"
|
|
"k8s.io/kubernetes/pkg/apis/policy"
|
|
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
|
|
psp "k8s.io/kubernetes/pkg/security/podsecuritypolicy"
|
|
psputil "k8s.io/kubernetes/pkg/security/podsecuritypolicy/util"
|
|
"k8s.io/kubernetes/pkg/serviceaccount"
|
|
)
|
|
|
|
const (
|
|
PluginName = "PodSecurityPolicy"
|
|
)
|
|
|
|
// Register registers a plugin
|
|
func Register(plugins *admission.Plugins) {
|
|
plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
|
|
plugin := newPlugin(psp.NewSimpleStrategyFactory(), true)
|
|
return plugin, nil
|
|
})
|
|
}
|
|
|
|
// PodSecurityPolicyPlugin holds state for and implements the admission plugin.
|
|
type PodSecurityPolicyPlugin struct {
|
|
*admission.Handler
|
|
strategyFactory psp.StrategyFactory
|
|
failOnNoPolicies bool
|
|
authz authorizer.Authorizer
|
|
lister policylisters.PodSecurityPolicyLister
|
|
}
|
|
|
|
// SetAuthorizer sets the authorizer.
|
|
func (plugin *PodSecurityPolicyPlugin) SetAuthorizer(authz authorizer.Authorizer) {
|
|
plugin.authz = authz
|
|
}
|
|
|
|
// ValidateInitialization ensures an authorizer is set.
|
|
func (plugin *PodSecurityPolicyPlugin) ValidateInitialization() error {
|
|
if plugin.authz == nil {
|
|
return fmt.Errorf("%s requires an authorizer", PluginName)
|
|
}
|
|
if plugin.lister == nil {
|
|
return fmt.Errorf("%s requires a lister", PluginName)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
var _ admission.MutationInterface = &PodSecurityPolicyPlugin{}
|
|
var _ admission.ValidationInterface = &PodSecurityPolicyPlugin{}
|
|
var _ genericadmissioninit.WantsAuthorizer = &PodSecurityPolicyPlugin{}
|
|
var _ genericadmissioninit.WantsExternalKubeInformerFactory = &PodSecurityPolicyPlugin{}
|
|
var auditKeyPrefix = strings.ToLower(PluginName) + "." + policy.GroupName + ".k8s.io"
|
|
|
|
// newPlugin creates a new PSP admission plugin.
|
|
func newPlugin(strategyFactory psp.StrategyFactory, failOnNoPolicies bool) *PodSecurityPolicyPlugin {
|
|
return &PodSecurityPolicyPlugin{
|
|
Handler: admission.NewHandler(admission.Create, admission.Update),
|
|
strategyFactory: strategyFactory,
|
|
failOnNoPolicies: failOnNoPolicies,
|
|
}
|
|
}
|
|
|
|
func (a *PodSecurityPolicyPlugin) SetExternalKubeInformerFactory(f informers.SharedInformerFactory) {
|
|
podSecurityPolicyInformer := f.Policy().V1beta1().PodSecurityPolicies()
|
|
a.lister = podSecurityPolicyInformer.Lister()
|
|
a.SetReadyFunc(podSecurityPolicyInformer.Informer().HasSynced)
|
|
}
|
|
|
|
// Admit determines if the pod should be admitted based on the requested security context
|
|
// and the available PSPs.
|
|
//
|
|
// 1. Find available PSPs.
|
|
// 2. Create the providers, includes setting pre-allocated values if necessary.
|
|
// 3. Try to generate and validate a PSP with providers. If we find one then admit the pod
|
|
// with the validated PSP. If we don't find any reject the pod and give all errors from the
|
|
// failed attempts.
|
|
func (c *PodSecurityPolicyPlugin) Admit(a admission.Attributes, o admission.ObjectInterfaces) error {
|
|
if ignore, err := shouldIgnore(a); err != nil {
|
|
return err
|
|
} else if ignore {
|
|
return nil
|
|
}
|
|
|
|
// only mutate if this is a CREATE request. On updates we only validate.
|
|
// TODO(liggitt): allow spec mutation during initializing updates?
|
|
if a.GetOperation() != admission.Create {
|
|
return nil
|
|
}
|
|
|
|
pod := a.GetObject().(*api.Pod)
|
|
|
|
// compute the context. Mutation is allowed. ValidatedPSPAnnotation is not taken into account.
|
|
allowedPod, pspName, validationErrs, err := c.computeSecurityContext(a, pod, true, "")
|
|
if err != nil {
|
|
return admission.NewForbidden(a, err)
|
|
}
|
|
if allowedPod != nil {
|
|
*pod = *allowedPod
|
|
// annotate and accept the pod
|
|
klog.V(4).Infof("pod %s (generate: %s) in namespace %s validated against provider %s", pod.Name, pod.GenerateName, a.GetNamespace(), pspName)
|
|
if pod.ObjectMeta.Annotations == nil {
|
|
pod.ObjectMeta.Annotations = map[string]string{}
|
|
}
|
|
pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation] = pspName
|
|
key := auditKeyPrefix + "/" + "admit-policy"
|
|
if err := a.AddAnnotation(key, pspName); err != nil {
|
|
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// we didn't validate against any provider, reject the pod and give the errors for each attempt
|
|
klog.V(4).Infof("unable to validate pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
|
|
return admission.NewForbidden(a, fmt.Errorf("unable to validate against any pod security policy: %v", validationErrs))
|
|
}
|
|
|
|
func (c *PodSecurityPolicyPlugin) Validate(a admission.Attributes, o admission.ObjectInterfaces) error {
|
|
if ignore, err := shouldIgnore(a); err != nil {
|
|
return err
|
|
} else if ignore {
|
|
return nil
|
|
}
|
|
|
|
pod := a.GetObject().(*api.Pod)
|
|
|
|
// compute the context. Mutation is not allowed. ValidatedPSPAnnotation is used as a hint to gain same speed-up.
|
|
allowedPod, pspName, validationErrs, err := c.computeSecurityContext(a, pod, false, pod.ObjectMeta.Annotations[psputil.ValidatedPSPAnnotation])
|
|
if err != nil {
|
|
return admission.NewForbidden(a, err)
|
|
}
|
|
if apiequality.Semantic.DeepEqual(pod, allowedPod) {
|
|
key := auditKeyPrefix + "/" + "validate-policy"
|
|
if err := a.AddAnnotation(key, pspName); err != nil {
|
|
klog.Warningf("failed to set admission audit annotation %s to %s: %v", key, pspName, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// we didn't validate against any provider, reject the pod and give the errors for each attempt
|
|
klog.V(4).Infof("unable to validate pod %s (generate: %s) in namespace %s against any pod security policy: %v", pod.Name, pod.GenerateName, a.GetNamespace(), validationErrs)
|
|
return admission.NewForbidden(a, fmt.Errorf("unable to validate against any pod security policy: %v", validationErrs))
|
|
}
|
|
|
|
func shouldIgnore(a admission.Attributes) (bool, error) {
|
|
if a.GetResource().GroupResource() != api.Resource("pods") {
|
|
return true, nil
|
|
}
|
|
if len(a.GetSubresource()) != 0 {
|
|
return true, nil
|
|
}
|
|
|
|
// if we can't convert then fail closed since we've already checked that this is supposed to be a pod object.
|
|
// this shouldn't normally happen during admission but could happen if an integrator passes a versioned
|
|
// pod object rather than an internal object.
|
|
if _, ok := a.GetObject().(*api.Pod); !ok {
|
|
return false, admission.NewForbidden(a, fmt.Errorf("unexpected type %T", a.GetObject()))
|
|
}
|
|
|
|
// if this is an update, see if we are only updating the ownerRef/finalizers. Garbage collection does this
|
|
// and we should allow it in general, since you had the power to update and the power to delete.
|
|
// The worst that happens is that you delete something, but you aren't controlling the privileged object itself
|
|
if a.GetOperation() == admission.Update && rbacregistry.IsOnlyMutatingGCFields(a.GetObject(), a.GetOldObject(), apiequality.Semantic) {
|
|
return true, nil
|
|
}
|
|
|
|
return false, nil
|
|
}
|
|
|
|
// computeSecurityContext derives a valid security context while trying to avoid any changes to the given pod. I.e.
|
|
// if there is a matching policy with the same security context as given, it will be reused. If there is no
|
|
// matching policy the returned pod will be nil and the pspName empty. validatedPSPHint is the validated psp name
|
|
// saved in kubernetes.io/psp annotation. This psp is usually the one we are looking for.
|
|
func (c *PodSecurityPolicyPlugin) computeSecurityContext(a admission.Attributes, pod *api.Pod, specMutationAllowed bool, validatedPSPHint string) (*api.Pod, string, field.ErrorList, error) {
|
|
// get all constraints that are usable by the user
|
|
klog.V(4).Infof("getting pod security policies for pod %s (generate: %s)", pod.Name, pod.GenerateName)
|
|
var saInfo user.Info
|
|
if len(pod.Spec.ServiceAccountName) > 0 {
|
|
saInfo = serviceaccount.UserInfo(a.GetNamespace(), pod.Spec.ServiceAccountName, "")
|
|
}
|
|
|
|
policies, err := c.lister.List(labels.Everything())
|
|
if err != nil {
|
|
return nil, "", nil, err
|
|
}
|
|
|
|
// if we have no policies and want to succeed then return. Otherwise we'll end up with no
|
|
// providers and fail with "unable to validate against any pod security policy" below.
|
|
if len(policies) == 0 && !c.failOnNoPolicies {
|
|
return pod, "", nil, nil
|
|
}
|
|
|
|
// sort policies by name to make order deterministic
|
|
// If mutation is not allowed and validatedPSPHint is provided, check the validated policy first.
|
|
// TODO(liggitt): add priority field to allow admins to bucket differently
|
|
sort.SliceStable(policies, func(i, j int) bool {
|
|
if !specMutationAllowed {
|
|
if policies[i].Name == validatedPSPHint {
|
|
return true
|
|
}
|
|
if policies[j].Name == validatedPSPHint {
|
|
return false
|
|
}
|
|
}
|
|
return strings.Compare(policies[i].Name, policies[j].Name) < 0
|
|
})
|
|
|
|
providers, errs := c.createProvidersFromPolicies(policies, pod.Namespace)
|
|
for _, err := range errs {
|
|
klog.V(4).Infof("provider creation error: %v", err)
|
|
}
|
|
|
|
if len(providers) == 0 {
|
|
return nil, "", nil, fmt.Errorf("no providers available to validate pod request")
|
|
}
|
|
|
|
var (
|
|
allowedMutatedPod *api.Pod
|
|
allowingMutatingPSP string
|
|
// Map of PSP name to associated validation errors.
|
|
validationErrs = map[string]field.ErrorList{}
|
|
)
|
|
|
|
for _, provider := range providers {
|
|
podCopy := pod.DeepCopy()
|
|
|
|
if errs := assignSecurityContext(provider, podCopy); len(errs) > 0 {
|
|
validationErrs[provider.GetPSPName()] = errs
|
|
continue
|
|
}
|
|
|
|
// the entire pod validated
|
|
mutated := !apiequality.Semantic.DeepEqual(pod, podCopy)
|
|
if mutated && !specMutationAllowed {
|
|
continue
|
|
}
|
|
|
|
if !isAuthorizedForPolicy(a.GetUserInfo(), saInfo, a.GetNamespace(), provider.GetPSPName(), c.authz) {
|
|
continue
|
|
}
|
|
|
|
switch {
|
|
case !mutated:
|
|
// if it validated without mutating anything, use this result
|
|
return podCopy, provider.GetPSPName(), nil, nil
|
|
|
|
case specMutationAllowed && allowedMutatedPod == nil:
|
|
// if mutation is allowed and this is the first PSP to allow the pod, remember it,
|
|
// but continue to see if another PSP allows without mutating
|
|
allowedMutatedPod = podCopy
|
|
allowingMutatingPSP = provider.GetPSPName()
|
|
}
|
|
}
|
|
|
|
if allowedMutatedPod != nil {
|
|
return allowedMutatedPod, allowingMutatingPSP, nil, nil
|
|
}
|
|
|
|
// Pod is rejected. Filter the validation errors to only include errors from authorized PSPs.
|
|
aggregate := field.ErrorList{}
|
|
for psp, errs := range validationErrs {
|
|
if isAuthorizedForPolicy(a.GetUserInfo(), saInfo, a.GetNamespace(), psp, c.authz) {
|
|
aggregate = append(aggregate, errs...)
|
|
}
|
|
}
|
|
return nil, "", aggregate, nil
|
|
}
|
|
|
|
// assignSecurityContext creates a security context for each container in the pod
|
|
// and validates that the sc falls within the psp constraints. All containers must validate against
|
|
// the same psp or is not considered valid.
|
|
func assignSecurityContext(provider psp.Provider, pod *api.Pod) field.ErrorList {
|
|
errs := field.ErrorList{}
|
|
|
|
if err := provider.MutatePod(pod); err != nil {
|
|
// TODO(tallclair): MutatePod should return a field.ErrorList
|
|
errs = append(errs, field.Invalid(field.NewPath(""), pod, err.Error()))
|
|
}
|
|
|
|
errs = append(errs, provider.ValidatePod(pod)...)
|
|
|
|
return errs
|
|
}
|
|
|
|
// createProvidersFromPolicies creates providers from the constraints supplied.
|
|
func (c *PodSecurityPolicyPlugin) createProvidersFromPolicies(psps []*policyv1beta1.PodSecurityPolicy, namespace string) ([]psp.Provider, []error) {
|
|
var (
|
|
// collected providers
|
|
providers []psp.Provider
|
|
// collected errors to return
|
|
errs []error
|
|
)
|
|
|
|
for _, constraint := range psps {
|
|
provider, err := psp.NewSimpleProvider(constraint, namespace, c.strategyFactory)
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("error creating provider for PSP %s: %v", constraint.Name, err))
|
|
continue
|
|
}
|
|
providers = append(providers, provider)
|
|
}
|
|
return providers, errs
|
|
}
|
|
|
|
func isAuthorizedForPolicy(user, sa user.Info, namespace, policyName string, authz authorizer.Authorizer) bool {
|
|
// Check the service account first, as that is the more common use case.
|
|
return authorizedForPolicy(sa, namespace, policyName, authz) ||
|
|
authorizedForPolicy(user, namespace, policyName, authz)
|
|
}
|
|
|
|
// authorizedForPolicy returns true if info is authorized to perform the "use" verb on the policy resource.
|
|
// TODO: check against only the policy group when PSP will be completely moved out of the extensions
|
|
func authorizedForPolicy(info user.Info, namespace string, policyName string, authz authorizer.Authorizer) bool {
|
|
// Check against extensions API group for backward compatibility
|
|
return authorizedForPolicyInAPIGroup(info, namespace, policyName, policy.GroupName, authz) ||
|
|
authorizedForPolicyInAPIGroup(info, namespace, policyName, extensions.GroupName, authz)
|
|
}
|
|
|
|
// authorizedForPolicyInAPIGroup returns true if info is authorized to perform the "use" verb on the policy resource in the specified API group.
|
|
func authorizedForPolicyInAPIGroup(info user.Info, namespace, policyName, apiGroupName string, authz authorizer.Authorizer) bool {
|
|
if info == nil {
|
|
return false
|
|
}
|
|
attr := buildAttributes(info, namespace, policyName, apiGroupName)
|
|
decision, reason, err := authz.Authorize(attr)
|
|
if err != nil {
|
|
klog.V(5).Infof("cannot authorize for policy: %v,%v", reason, err)
|
|
}
|
|
return (decision == authorizer.DecisionAllow)
|
|
}
|
|
|
|
// buildAttributes builds an attributes record for a SAR based on the user info and policy.
|
|
func buildAttributes(info user.Info, namespace, policyName, apiGroupName string) authorizer.Attributes {
|
|
// check against the namespace that the pod is being created in to allow per-namespace PSP grants.
|
|
attr := authorizer.AttributesRecord{
|
|
User: info,
|
|
Verb: "use",
|
|
Namespace: namespace,
|
|
Name: policyName,
|
|
APIGroup: apiGroupName,
|
|
Resource: "podsecuritypolicies",
|
|
ResourceRequest: true,
|
|
}
|
|
return attr
|
|
}
|