...

Source file src/k8s.io/kubernetes/plugin/pkg/admission/imagepolicy/admission.go

Documentation: k8s.io/kubernetes/plugin/pkg/admission/imagepolicy

     1  /*
     2  Copyright 2016 The Kubernetes Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  // Package imagepolicy contains an admission controller that configures a webhook to which policy
    18  // decisions are delegated.
    19  package imagepolicy
    20  
    21  import (
    22  	"context"
    23  	"encoding/json"
    24  	"errors"
    25  	"fmt"
    26  	"io"
    27  	"strings"
    28  	"time"
    29  
    30  	"k8s.io/klog/v2"
    31  
    32  	"k8s.io/api/imagepolicy/v1alpha1"
    33  	apierrors "k8s.io/apimachinery/pkg/api/errors"
    34  	"k8s.io/apimachinery/pkg/runtime/schema"
    35  	"k8s.io/apimachinery/pkg/util/cache"
    36  	"k8s.io/apimachinery/pkg/util/yaml"
    37  	"k8s.io/apiserver/pkg/admission"
    38  	"k8s.io/apiserver/pkg/util/webhook"
    39  	"k8s.io/client-go/rest"
    40  	"k8s.io/kubernetes/pkg/api/legacyscheme"
    41  	api "k8s.io/kubernetes/pkg/apis/core"
    42  
    43  	// install the clientgo image policy API for use with api registry
    44  	_ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
    45  )
    46  
    47  // PluginName indicates name of admission plugin.
    48  const PluginName = "ImagePolicyWebhook"
    49  const ephemeralcontainers = "ephemeralcontainers"
    50  
    51  // AuditKeyPrefix is used as the prefix for all audit keys handled by this
    52  // pluggin. Some well known suffixes are listed below.
    53  var AuditKeyPrefix = strings.ToLower(PluginName) + ".image-policy.k8s.io/"
    54  
    55  const (
    56  	// ImagePolicyFailedOpenKeySuffix in an annotation indicates the image
    57  	// review failed open when the image policy webhook backend connection
    58  	// failed.
    59  	ImagePolicyFailedOpenKeySuffix string = "failed-open"
    60  
    61  	// ImagePolicyAuditRequiredKeySuffix in an annotation indicates the pod
    62  	// should be audited.
    63  	ImagePolicyAuditRequiredKeySuffix string = "audit-required"
    64  )
    65  
    66  var (
    67  	groupVersions = []schema.GroupVersion{v1alpha1.SchemeGroupVersion}
    68  )
    69  
    70  // Register registers a plugin
    71  func Register(plugins *admission.Plugins) {
    72  	plugins.Register(PluginName, func(config io.Reader) (admission.Interface, error) {
    73  		newImagePolicyWebhook, err := NewImagePolicyWebhook(config)
    74  		if err != nil {
    75  			return nil, err
    76  		}
    77  		return newImagePolicyWebhook, nil
    78  	})
    79  }
    80  
    81  // Plugin is an implementation of admission.Interface.
    82  type Plugin struct {
    83  	*admission.Handler
    84  	webhook       *webhook.GenericWebhook
    85  	responseCache *cache.LRUExpireCache
    86  	allowTTL      time.Duration
    87  	denyTTL       time.Duration
    88  	defaultAllow  bool
    89  }
    90  
    91  var _ admission.ValidationInterface = &Plugin{}
    92  
    93  func (a *Plugin) statusTTL(status v1alpha1.ImageReviewStatus) time.Duration {
    94  	if status.Allowed {
    95  		return a.allowTTL
    96  	}
    97  	return a.denyTTL
    98  }
    99  
   100  // Filter out annotations that don't match *.image-policy.k8s.io/*
   101  func (a *Plugin) filterAnnotations(allAnnotations map[string]string) map[string]string {
   102  	annotations := make(map[string]string)
   103  	for k, v := range allAnnotations {
   104  		if strings.Contains(k, ".image-policy.k8s.io/") {
   105  			annotations[k] = v
   106  		}
   107  	}
   108  	return annotations
   109  }
   110  
   111  // Function to call on webhook failure; behavior determined by defaultAllow flag
   112  func (a *Plugin) webhookError(pod *api.Pod, attributes admission.Attributes, err error) error {
   113  	if err != nil {
   114  		klog.V(2).Infof("error contacting webhook backend: %s", err)
   115  		if a.defaultAllow {
   116  			attributes.AddAnnotation(AuditKeyPrefix+ImagePolicyFailedOpenKeySuffix, "true")
   117  			// TODO(wteiken): Remove the annotation code for the 1.13 release
   118  			annotations := pod.GetAnnotations()
   119  			if annotations == nil {
   120  				annotations = make(map[string]string)
   121  			}
   122  			annotations[api.ImagePolicyFailedOpenKey] = "true"
   123  			pod.ObjectMeta.SetAnnotations(annotations)
   124  
   125  			klog.V(2).Infof("resource allowed in spite of webhook backend failure")
   126  			return nil
   127  		}
   128  		klog.V(2).Infof("resource not allowed due to webhook backend failure ")
   129  		return admission.NewForbidden(attributes, err)
   130  	}
   131  	return nil
   132  }
   133  
   134  // Validate makes an admission decision based on the request attributes
   135  func (a *Plugin) Validate(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) (err error) {
   136  	// Ignore all calls to subresources other than ephemeralcontainers or calls to resources other than pods.
   137  	subresource := attributes.GetSubresource()
   138  	if (subresource != "" && subresource != ephemeralcontainers) || attributes.GetResource().GroupResource() != api.Resource("pods") {
   139  		return nil
   140  	}
   141  
   142  	pod, ok := attributes.GetObject().(*api.Pod)
   143  	if !ok {
   144  		return apierrors.NewBadRequest("Resource was marked with kind Pod but was unable to be converted")
   145  	}
   146  
   147  	// Build list of ImageReviewContainerSpec
   148  	var imageReviewContainerSpecs []v1alpha1.ImageReviewContainerSpec
   149  	if subresource == "" {
   150  		containers := make([]api.Container, 0, len(pod.Spec.Containers)+len(pod.Spec.InitContainers))
   151  		containers = append(containers, pod.Spec.Containers...)
   152  		containers = append(containers, pod.Spec.InitContainers...)
   153  		for _, c := range containers {
   154  			imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{
   155  				Image: c.Image,
   156  			})
   157  		}
   158  	} else if subresource == ephemeralcontainers {
   159  		for _, c := range pod.Spec.EphemeralContainers {
   160  			imageReviewContainerSpecs = append(imageReviewContainerSpecs, v1alpha1.ImageReviewContainerSpec{
   161  				Image: c.Image,
   162  			})
   163  		}
   164  	}
   165  	imageReview := v1alpha1.ImageReview{
   166  		Spec: v1alpha1.ImageReviewSpec{
   167  			Containers:  imageReviewContainerSpecs,
   168  			Annotations: a.filterAnnotations(pod.Annotations),
   169  			Namespace:   attributes.GetNamespace(),
   170  		},
   171  	}
   172  	if err := a.admitPod(ctx, pod, attributes, &imageReview); err != nil {
   173  		return admission.NewForbidden(attributes, err)
   174  	}
   175  	return nil
   176  }
   177  
   178  func (a *Plugin) admitPod(ctx context.Context, pod *api.Pod, attributes admission.Attributes, review *v1alpha1.ImageReview) error {
   179  	cacheKey, err := json.Marshal(review.Spec)
   180  	if err != nil {
   181  		return err
   182  	}
   183  	if entry, ok := a.responseCache.Get(string(cacheKey)); ok {
   184  		review.Status = entry.(v1alpha1.ImageReviewStatus)
   185  	} else {
   186  		result := a.webhook.WithExponentialBackoff(ctx, func() rest.Result {
   187  			return a.webhook.RestClient.Post().Body(review).Do(ctx)
   188  		})
   189  
   190  		if err := result.Error(); err != nil {
   191  			return a.webhookError(pod, attributes, err)
   192  		}
   193  		var statusCode int
   194  		if result.StatusCode(&statusCode); statusCode < 200 || statusCode >= 300 {
   195  			return a.webhookError(pod, attributes, fmt.Errorf("Error contacting webhook: %d", statusCode))
   196  		}
   197  
   198  		if err := result.Into(review); err != nil {
   199  			return a.webhookError(pod, attributes, err)
   200  		}
   201  
   202  		a.responseCache.Add(string(cacheKey), review.Status, a.statusTTL(review.Status))
   203  	}
   204  
   205  	for k, v := range review.Status.AuditAnnotations {
   206  		if err := attributes.AddAnnotation(AuditKeyPrefix+k, v); err != nil {
   207  			klog.Warningf("failed to set admission audit annotation %s to %s: %v", AuditKeyPrefix+k, v, err)
   208  		}
   209  	}
   210  	if !review.Status.Allowed {
   211  		if len(review.Status.Reason) > 0 {
   212  			return fmt.Errorf("image policy webhook backend denied one or more images: %s", review.Status.Reason)
   213  		}
   214  		return errors.New("one or more images rejected by webhook backend")
   215  	}
   216  	return nil
   217  }
   218  
   219  // NewImagePolicyWebhook a new ImagePolicyWebhook plugin from the provided config file.
   220  // The config file is specified by --admission-control-config-file and has the
   221  // following format for a webhook:
   222  //
   223  //	{
   224  //	  "imagePolicy": {
   225  //	     "kubeConfigFile": "path/to/kubeconfig/for/backend",
   226  //	     "allowTTL": 30,           # time in s to cache approval
   227  //	     "denyTTL": 30,            # time in s to cache denial
   228  //	     "retryBackoff": 500,      # time in ms to wait between retries
   229  //	     "defaultAllow": true      # determines behavior if the webhook backend fails
   230  //	  }
   231  //	}
   232  //
   233  // The config file may be json or yaml.
   234  //
   235  // The kubeconfig property refers to another file in the kubeconfig format which
   236  // specifies how to connect to the webhook backend.
   237  //
   238  // The kubeconfig's cluster field is used to refer to the remote service, user refers to the returned authorizer.
   239  //
   240  //	# clusters refers to the remote service.
   241  //	clusters:
   242  //	- name: name-of-remote-imagepolicy-service
   243  //	  cluster:
   244  //	    certificate-authority: /path/to/ca.pem      # CA for verifying the remote service.
   245  //	    server: https://images.example.com/policy # URL of remote service to query. Must use 'https'.
   246  //
   247  //	# users refers to the API server's webhook configuration.
   248  //	users:
   249  //	- name: name-of-api-server
   250  //	  user:
   251  //	    client-certificate: /path/to/cert.pem # cert for the webhook plugin to use
   252  //	    client-key: /path/to/key.pem          # key matching the cert
   253  //
   254  // For additional HTTP configuration, refer to the kubeconfig documentation
   255  // http://kubernetes.io/v1.1/docs/user-guide/kubeconfig-file.html.
   256  func NewImagePolicyWebhook(configFile io.Reader) (*Plugin, error) {
   257  	if configFile == nil {
   258  		return nil, fmt.Errorf("no config specified")
   259  	}
   260  
   261  	// TODO: move this to a versioned configuration file format
   262  	var config AdmissionConfig
   263  	d := yaml.NewYAMLOrJSONDecoder(configFile, 4096)
   264  	err := d.Decode(&config)
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	whConfig := config.ImagePolicyWebhook
   270  	if err := normalizeWebhookConfig(&whConfig); err != nil {
   271  		return nil, err
   272  	}
   273  
   274  	clientConfig, err := webhook.LoadKubeconfig(whConfig.KubeConfigFile, nil)
   275  	if err != nil {
   276  		return nil, err
   277  	}
   278  	retryBackoff := webhook.DefaultRetryBackoffWithInitialDelay(whConfig.RetryBackoff)
   279  	gw, err := webhook.NewGenericWebhook(legacyscheme.Scheme, legacyscheme.Codecs, clientConfig, groupVersions, retryBackoff)
   280  	if err != nil {
   281  		return nil, err
   282  	}
   283  	return &Plugin{
   284  		Handler:       admission.NewHandler(admission.Create, admission.Update),
   285  		webhook:       gw,
   286  		responseCache: cache.NewLRUExpireCache(1024),
   287  		allowTTL:      whConfig.AllowTTL,
   288  		denyTTL:       whConfig.DenyTTL,
   289  		defaultAllow:  whConfig.DefaultAllow,
   290  	}, nil
   291  }
   292  

View as plain text