1
16
17
18
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
44 _ "k8s.io/kubernetes/pkg/apis/imagepolicy/install"
45 )
46
47
48 const PluginName = "ImagePolicyWebhook"
49 const ephemeralcontainers = "ephemeralcontainers"
50
51
52
53 var AuditKeyPrefix = strings.ToLower(PluginName) + ".image-policy.k8s.io/"
54
55 const (
56
57
58
59 ImagePolicyFailedOpenKeySuffix string = "failed-open"
60
61
62
63 ImagePolicyAuditRequiredKeySuffix string = "audit-required"
64 )
65
66 var (
67 groupVersions = []schema.GroupVersion{v1alpha1.SchemeGroupVersion}
68 )
69
70
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
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
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
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
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
135 func (a *Plugin) Validate(ctx context.Context, attributes admission.Attributes, o admission.ObjectInterfaces) (err error) {
136
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
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
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256 func NewImagePolicyWebhook(configFile io.Reader) (*Plugin, error) {
257 if configFile == nil {
258 return nil, fmt.Errorf("no config specified")
259 }
260
261
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