k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/plugin/pkg/admission/imagepolicy/admission.go (about) 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 }