admission.go 9.2 KB

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