diff --git a/.reuse/dep5 b/.reuse/dep5 index 3bb44faa..6d13eba7 100644 --- a/.reuse/dep5 +++ b/.reuse/dep5 @@ -49,14 +49,6 @@ Copyright: Copyright 2021 The Sigstore Authors. Modifications Copyright 2022 SAP SE or an SAP affiliate company and Gardener contributors License: Apache-2.0 -Files: - pkg/admission/admission_suite_test.go - pkg/admission/http.go - pkg/admission/http_test.go -Copyright: Copyright 2018 The Kubernetes Authors. - Modifications Copyright 2022 SAP SE or an SAP affiliate company and Gardener contributors -License: Apache-2.0 - Files: hack/cherry-pick-pull.sh Copyright: Copyright 2015 The Kubernetes Authors. diff --git a/charts/gardener-extension-shoot-lakom-service/templates/configmap-extension-config.yaml b/charts/gardener-extension-shoot-lakom-service/templates/configmap-extension-config.yaml index 247bbc1e..d5bff3e3 100644 --- a/charts/gardener-extension-shoot-lakom-service/templates/configmap-extension-config.yaml +++ b/charts/gardener-extension-shoot-lakom-service/templates/configmap-extension-config.yaml @@ -14,10 +14,10 @@ data: kind: Configuration cosignPublicKeys: {{ toYaml .Values.controllers.cosignPublicKeys | indent 6 }} - failurePolicy: {{ .Values.controllers.failurePolicy }} seedBootstrap: ownerNamespace: {{ .Release.Namespace }} useOnlyImagePullSecrets: {{ .Values.controllers.useOnlyImagePullSecrets }} + allowUntrustedImages: {{ .Values.controllers.allowUntrustedImages }} debugConfig: enableProfiling: {{ .Values.controllers.debugConfig.enableProfiling | default false }} enableContentionProfiling: {{ .Values.controllers.debugConfig.enableContentionProfiling | default false }} \ No newline at end of file diff --git a/charts/gardener-extension-shoot-lakom-service/values.yaml b/charts/gardener-extension-shoot-lakom-service/values.yaml index 03832a4d..2e05a47b 100644 --- a/charts/gardener-extension-shoot-lakom-service/values.yaml +++ b/charts/gardener-extension-shoot-lakom-service/values.yaml @@ -46,10 +46,10 @@ controllers: # -----BEGIN PUBLIC KEY----- # abcd # -----END PUBLIC KEY----- - failurePolicy: Fail healthPort: 8080 metricsPort: 8081 useOnlyImagePullSecrets: false + allowUntrustedImages: false debugConfig: enableProfiling: false enableContentionProfiling: false diff --git a/charts/lakom/templates/deployment.yaml b/charts/lakom/templates/deployment.yaml index d6fba566..2b95c6da 100644 --- a/charts/lakom/templates/deployment.yaml +++ b/charts/lakom/templates/deployment.yaml @@ -82,6 +82,7 @@ spec: - --kubeconfig=/etc/lakom/client/kubeconfig {{- end }} - --use-only-image-pull-secrets={{ .Values.useOnlyImagePullSecrets }} + - --insecure-allow-untrusted-images={{ .Values.allowUntrustedImages }} {{- if .Values.resources }} resources: {{ toYaml .Values.resources | trim | indent 10 }} diff --git a/charts/lakom/templates/mutatingwebhookconfiguration.yaml b/charts/lakom/templates/mutatingwebhookconfiguration.yaml index 2ae3ef57..e2f59e8e 100644 --- a/charts/lakom/templates/mutatingwebhookconfiguration.yaml +++ b/charts/lakom/templates/mutatingwebhookconfiguration.yaml @@ -24,7 +24,7 @@ webhooks: namespace: {{ .Release.Namespace }} path: /lakom/resolve-tag-to-digest {{- end }} - failurePolicy: {{ .Values.admissionConfig.failurePolicy }} + failurePolicy: Fail matchPolicy: Equivalent name: resolve-tag.lakom.service.gardener.cloud {{- if .Values.admissionConfig.namespaceSelector }} diff --git a/charts/lakom/templates/validatingwebhookconfiguration.yaml b/charts/lakom/templates/validatingwebhookconfiguration.yaml index ede031ee..5888210a 100644 --- a/charts/lakom/templates/validatingwebhookconfiguration.yaml +++ b/charts/lakom/templates/validatingwebhookconfiguration.yaml @@ -24,7 +24,7 @@ webhooks: namespace: {{ .Release.Namespace }} path: /lakom/verify-cosign-signature {{- end }} - failurePolicy: {{ .Values.admissionConfig.failurePolicy }} + failurePolicy: Fail matchPolicy: Equivalent name: verify-signature.lakom.service.gardener.cloud {{- if .Values.admissionConfig.namespaceSelector }} diff --git a/charts/lakom/values.yaml b/charts/lakom/values.yaml index 87b8ce18..921f80a2 100644 --- a/charts/lakom/values.yaml +++ b/charts/lakom/values.yaml @@ -31,6 +31,7 @@ cosign: # abcd # -----END PUBLIC KEY----- useOnlyImagePullSecrets: false +allowUntrustedImages: false kubeconfig: {} admissionConfig: objectSelector: {} @@ -41,7 +42,6 @@ admissionConfig: values: - "kube-system" - "lakom-system" - failurePolicy: Fail clientConfig: caBundle: foo urlHostname: "" diff --git a/cmd/lakom/app/app.go b/cmd/lakom/app/app.go index 450e9782..d927eb59 100644 --- a/cmd/lakom/app/app.go +++ b/cmd/lakom/app/app.go @@ -13,7 +13,6 @@ import ( goruntime "runtime" "time" - "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/admission" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/constants" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/lakom/resolvetag" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/lakom/verifysignature" @@ -99,6 +98,9 @@ type Options struct { // UseOnlyImagePullSecrets sets only the image pull secrets of the pod to be used to access the OCI registry. // Otherwise, also the node identity and docker config file are used. UseOnlyImagePullSecrets bool + // AllowUntrustedImages configures the webhook to allow images without trusted signature. + // Instead to deny the request, the webhook will allow it with a warning. + AllowUntrustedImages bool } // AddFlags adds lakom admission controller's flags to the specified FlagSet. @@ -114,6 +116,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { fs.DurationVar(&o.CacheTTL, "cache-ttl", time.Minute*10, "TTL for the cached objects. Set to 0, if cache has to be disabled") fs.DurationVar(&o.CacheRefreshInterval, "cache-refresh-interval", time.Second*30, "Refresh interval for the cached objects") fs.BoolVar(&o.UseOnlyImagePullSecrets, "use-only-image-pull-secrets", false, "If set, only the credentials from the image pull secrets of the pod are used to access the OCI registry. Otherwise, the node identity and docker config are also used.") + fs.BoolVar(&o.AllowUntrustedImages, "insecure-allow-untrusted-images", false, "If set, the webhook will just return warning for the images without trusted signatures.") } // validate validates all the required options. @@ -230,6 +233,7 @@ func (o *Options) Run(ctx context.Context) error { WithCacheTTL(o.CacheTTL). WithCacheRefreshInterval(o.CacheRefreshInterval). WithUseOnlyImagePullSecrets(o.UseOnlyImagePullSecrets). + WithAllowUntrustedImages(o.AllowUntrustedImages). Build() if err != nil { return err @@ -237,16 +241,14 @@ func (o *Options) Run(ctx context.Context) error { server.Register( constants.LakomResolveTagPath, - &admission.Server{ - Webhook: webhook.Admission{Handler: imageTagResolverHandler}, - Log: imageTagResolverHandler.GetLogger(), + &webhook.Admission{ + Handler: imageTagResolverHandler, }, ) server.Register( constants.LakomVerifyCosignSignaturePath, - &admission.Server{ - Webhook: webhook.Admission{Handler: cosignSignatureVerifyHandler}, - Log: cosignSignatureVerifyHandler.GetLogger(), + &webhook.Admission{ + Handler: cosignSignatureVerifyHandler, }, ) diff --git a/example/00-config.yaml b/example/00-config.yaml index e8329260..6ca143fc 100644 --- a/example/00-config.yaml +++ b/example/00-config.yaml @@ -10,10 +10,10 @@ cosignPublicKeys: MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEyLVOS/TWANf6sZJPDzogodvDz8NT hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== -----END PUBLIC KEY----- -failurePolicy: Fail seedBootstrap: ownerNamespace: lakom-extension useOnlyImagePullSecrets: true +allowUntrustedImages: false debugConfig: enableProfiling: false enableContentionProfiling: false diff --git a/example/controller-registration.yaml b/example/controller-registration.yaml index 1873bfd2..05652f44 100644 --- a/example/controller-registration.yaml +++ b/example/controller-registration.yaml @@ -5,7 +5,7 @@ metadata: name: shoot-lakom-service type: helm providerConfig: - chart: H4sIAAAAAAAAA+0caW/bOLaf9Su46gymLUbynXQNdLFu4rZB28RIMt0dDBYBLTE2J5KoESWnnrb72/eRlGRdPpO66Y4eilSmeDyS7+ajJjiwiUcCg3wMiccp8ww+ZSw0HHzDXIOTYEYt0nh0F2gCHPZ68n+A4v/yudXpttq99sGBKG8dtru9R6h3p1E3hIiHOEDoUQCTXlVv3fvvFCab7b85JY5LJx4LyPZjiA0+6HaX7j9se37/261us/0INe9/umX4i+//YzTCYUgCj6OQIbXD6HZKPDSOqGNTb4J8bN3gCeGm9hhdTilHPPJ9FoTwAFThoInDxsjFoTWF2j+jgDg4pDMC7cJpphx7NnTgkQm8ZR564gfkmn4kNrqlUO9vT0105jlzxDzZUqCEfBIgh3rE1Mzji6uLEHCDLo6Y60IHH44ukE0DrpkTGjbkX4W+Zo7/DBryb1IwnTTEn+Qnn3mNRUdjmF/ko2vqEK49M/mtD3/H+Ab+hi48/xeqfsABZRFHJ8dDGNAP2O/ECjWT2gQ3VD0o0swZt5hNGtq33tXNYUP+P5riIDTn2HV2GGMd/7d73SL/tw47Nf/vA7BPP5BA7HsfzVoa9v30p94ym7pmE24F1A9l0QC9AT2ALEEN6JoFKJwS9DomIfROkAy6UCSDhglByYoXgqhAhHjYJX20GdVpswSVpgm4fEdc9f3AhvxvM8ucsB3HWMf/h0X7r93sdA5q/t8HNBroYnT8b+MVaL8j5s9BZU7DSyCGPmo32210MRihiyECDsae/IGvQVFSHBJkMdfH3lwo9oUMsJgXBnQcga7mWqOhJf2/AyryODFOoFpIrykJQJqAZTElRhtYG+pNWH8iuhBd8ykyLKSPMTz88On14Px4eDo8v3ozOHp7dXxy/qWR1DTkeMxxgIIDMqE8DKR5YULDCjpGJvrhiYVDZJoN+PdheH5xcnb6NP5JPmLXd0hjWZ9C/S3EWr+if11MBCwqaTDFYpJ4eAyWBcrNT1lQUjLGhcLSEtLUYkEAtgVaIIFySGh+tvc7y8QN+T8ksDKAOd/FE9za/2u32t1O7f/tA7be/yuw+cEu52bob2oLrvf/OoX97/QOerX83wd8+mQgZIMnBm6XTl0QLDoyvnzREBJv6DWaYj6SnhrS+RS3ewd9HZkfsBOBQyjrmyGeoLSFH1AvvEb6j/yfP/JizYD4jFNQDfNVXRCHk6oO+zt3CAoKfmQe5XMya4dgm4DXClIXyJ/aagFW8oOh2hhJo7Rv0fJbb+lWsDX/Ww5wDAnACIC/3HDYZAKqa6VruNb+6/YK/H9w0Kn9v73A469o/j3WHm9m/BmGoWUd0WvgZC8c09BUTyZljVkLOz4IIO2GenYfHSkyfCXJUHNJiG0c4j5wvIPHIEHEE8p0lNB5I5z74IDqnBBbhzrKH13D7OKXxn1iiV5jyhePIJ1wALXUYAjdkPmp7A+4Ii6KK2wygqHqxg0DIl6SYzErFAYRgXIZTOujm2hMAo8AN5rPVvb7TP56tkoi7cr/Ctf74f9e67DM/4c1/+8DvnP+HymWyfL/xhy94LdtREYiBn7ngKlsEVKXvCVz4FL+fel+AdvzP/Ou6cTFfqaFKlsqA9bwf6t30Crw/2GnV5//7AUeJP/PWgmXS8J6j/1tOdxUJBlX5jAWtPj0CZnnYLJjTszTpFi4A0nHGTruo8+SubNoyRHMdFweD5IKCtNyWGQnkqol22fnEakQjiy3wG+ZeKNo7FALhAcIH8AuZL+KEFPiyixCQOI5Xx99RtAzLCc6UI4OSC5MnSggIwZV5mq2FR3laiVNhWh7KQL0YYD9xJphtzCn0w2WT1SOOBHHdyfC+xpFjnNBrICEfCkaS+on3dlkHE3UqiXoqDjaKGCCAL3J0p4zTc1CG1g0cPpw5ISwWKmXuegcWoWCQpm3yzAVrSsG/NYMX4A7yH/pac/AAWaBwWYkuA1oSKqUwDr53zxo5+V/p9ltN2v5vw94MPI/DjflwjgfJHGdJbQl2HVrNVFB4BmOjulbiDPovZqkN1QieTMS+76Z8dPAgN0enSUdUQ9I1qvCJavL5FT4VZ4xhVYr65nKtU71S7cQO/vWFFvDfcLW8t8mvsPmrvDKNk0HWS3/W+12tyD/291Oq5b/e4EHI/+zgh1kHm+k0v04pbg7ivcNBXlRE8mksXPCWRRYJLZQseexUFrzsbwPkvdFb0A17yNdxPD0rCD9SgpDZGqafNqQ58N7UDhyTCAbA8/AtcBjII9wnoQEli6LiqksDOo0rhKQGRVYvqFcnOu8oy6FWfTkGx98Fpz3KeLCIxaBqpLYcHkow+KgrAyZvsus8z2u9C5rlQjSGLsMPcv+imQlQ8gBg3pTEmVwLXX+Gf0RsTCDWrGdyDdY6sxAvYBafCRyEtb2xC1wEhcUnVQCbrZueOSuiBDJ8alnOZFNRAIohT37wbyMl8R8CbMZiaRRfV2USX+KTEBUHUjCmAtkNzQjN0K60hrcZQ7LPaXVE0kFhQAnR8T3Ssa7Mb0AqH7LghtxAlBkcGYEwJPUJQaIdnmWAVIfOw67JfZm7W3ggu1a+DI+YsR1tm09dph1Q2zDonZQaJtIpwyJTUL0RGx+lSR6iloZIQEq0wOZmOFoZg9gaQalF0LI/RHRgNjHEVDW5ALo045EJOFEqpG4ePiRWJHMyMy0NBSFXOTE3wKkIBx+9EEi87x8SZrfiBh2NVUV6iLEfJGzBKOgE6/0ciYXpDiAGGIn2lyDX0Ko94XjKnIXEDKfOWwylyF/PY/LlPFQrJe+jIlBXjBg/vmRgzk/zfMrn3PQDsbfm824crweA8sSNHW6K3dzIBY5pohPgWWXzgb4c8BPmXcOpm16xqhA0v4ooDOwCydkyC3sYJUCLONYaT3oW9iBKuBFssupFPy5EgHHKggWvxaaB1MvPkRVq34H2SWla04sJ4ksZqmaCDVWBEhV0oifvsypCOa6YOMu5magxob5y4sWRqy9XjRIaDXyvlVGr+VaOPSaWHPLIYaLP4r2sI0BWMFGQMQPcV3hxRJ9njY1F80u5p7FsxMTY0wJdsKp1ILbj5JpvG4cZQUbiiXFci0snWW9qyZnSYtB2qDYdyEXx6D2iywtlNN7zIpVCMIxwaGR+gcvVka7yw1h5uTWAH0Am4od2FuYir1q5VQ7U7Y7iZtdqFbVm2SMwRsysG0LCf6iv3JPpCFX6CW28TbrJmsQZg1Bce0nywkpK482NS9z0lRxffw6Uw4GZ8gs5vTR5dEoLXfoDFaLc5A245ysmYah/5qEeWkurh/1UUMtyJ/5V6uQrV5BARxIXeD75vJylHkhdDjFzjFx8DzewT5qNRdiFgiQbo23aDXfO9q9tALxZtmNVjv1bjg4Hp5fDd8Njy5Pzk6vTgfvhxejwdEw06/UrK/AY8hPC7x+xz4n10WVK8tHcs6JN2SmXJjW3dKsT/A9eT94PfwAyJ6dX519GJ7/6/zksoQrLLb0lTPB0kZl9DSHTU6tlxAM8uGChBDiwqrDvkWLz6CKqbsIwLaaxYEKY8+YE7nkvTASeHnP8pkP6bloAq5opta/rJtyhim2xYldwVDYcWPWOFlL8Ctt02b45ZZLLVbJ8li5SlZyvpAl3q1OoRMApN2C1RkbtlVWgAIlEMrvt1r6bRd+xZTv5UQlgfh49D2zoeduu5mZW33esAfYOv4PvqtNeRDJG4HjyJ6QtQcB685/O51i/v9Bu13n/+8FHmT835dO2OIEYMTs45TmXkqa289RwAM5002iT+Ch/eLFYXbwtlHrgYe7Yw3FiQuS/0jQSwAO2T9eoJbZPjCa4IkdYV+dGFDo7y10HdOA+ZqG8aMaPvKUgTsHWhgCZjLNJnbkB84tnvOBCFnUB9VbwtbyPxhja8sPAayR/51et5j/3Wl26/zvvcCDkP+P0eXZ8dmTme+JstnTPjonLliLwPUeITaxxZdAXKoC14iBvImvyqZXZCmXDsC8rEskveIonLKA/qmu0N485yqjPJ9Lfs4csirPtHiG2V8lLh+E3ggiR3g6hshifR2wyI9vrYjzdS3nj4rSRWaHeAkSexy/EOpWni9Qrh5uhZYpd7t0pcpjxbdIdhxo2alyeRwXe+B/2Glpfjz9mV7uXNfL3aS2wtdfmUXVws8G7HQYbYaAupqUPEU+UDTZbKbpoaUanqu0XIXKrEwalStoMRbY1MsyW3kgSamF3mAsLL3D9Su63RhJuTTu1LuNQsSVJ1Zp8DaPfLzKya7cTQ69hALqTf7PxBFMLI4AJru5Yl20NHk+I57vsgo8GouvNkmBqHq+yB2wfU1H4lur+aWwtf2XhLa2MAHX2H/tVrOY/9fpHdT3f/YCK+2/zre+/xMz6F3TusX1lo09/lIOViZnZGk623UAzIIdRw5lqAwpAwgLNAE34kQSQ52eoZ9++6SLR32zw7ef9eQ0TO/rl0cj/ct/ftoYr3TKRhIniBHIBAoAj0IaQ+4gBkZVnepfthnZZ7YhtU468uJwFVaEijy+nK3zIFRUEmlR2Qux5jkZleIs3wzBzBmsUXF8unGWXzx8+aBVEe0Wx7nfWoLVcBfYVf9jZTNtZAasu/9/cFj8/kev1evW+n8f8CDiP+v0f2Kg/5Vi/uASMXn2nV+DS3ZD0iy8e9j/rfl/5uNtvwO79v5/6fsf7cN2q+b/fcCD4f9CNoMgM3Wt2i7c+tQFZ4iEVGF+piEUPZYYUC2k8HLE7EFcr/L7IF9BcCS2Y8VMEis5m12aL1PSJpNOB4U0SVPJ5NntnAXsUm+g/JBFRodLXBaoTNgk7xzpy/E2F32YqqmKu6uW+ooZV7Qsfp0tsTzTFLGqC2GivHQpbLfMFHGmKCN22fVXJSoZJdNSTChb2VzU+94vx24o/1W6/I4fAF8X/2n3ivK/1a2//7sfeBDyP8OZ/arP9mrZuzV91NE0lfMvvOFMQv/J9SkLR+LDaVIqhHgi8vrAaAmluE0+FdlHJAqYTwxbXPcJTP9mYtpktkjqjz/v31C3idLyRvYspgJJrSoDD+SIEDWP4/v4fflcSDuMy/I4XjNmjnFQOXq+lZznrGW2zY5WuKyaGIpa7mBESGxYlFi5WH7UR72mq2U1Qqv9/D3VQO6JOrEeTjMrqzRXWb0kXR10oad1wlYXylrXtEyYQX7EYNkdgMU9lPSqQ6pBs/cQBBlr4k5selNhTTV1spPc6S3n5gP1iZqlz/eg3/4j2zyGrf0cP8E2C3g5fH1yika/vHx3coTeDn+VhWmVVrvTzdcfnh4vqb1l13hs2Zt0Xfhw0Cv4ma6ZCgQ9bz4Xs84EfmSZUMhLP/yTbFDpUz6lD/ksrhSt+AzPvXk8NdRQQw011FBDDTXUUEMNNdTw14P/AZrn0hkAeAAA + chart: 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 values: image: tag: v0.12.0-dev diff --git a/hack/api-reference/config.md b/hack/api-reference/config.md index d68d048d..4360be43 100644 --- a/hack/api-reference/config.md +++ b/hack/api-reference/config.md @@ -67,18 +67,6 @@ github.com/gardener/gardener/extensions/pkg/apis/config/v1alpha1.HealthCheckConf -failurePolicy
- -string - - - -(Optional) -

FailurePolicy is the failure policy used to configure the failurePolicy of the lakom admission webhooks.

- - - - debugConfig
@@ -116,6 +104,18 @@ bool Otherwise, also the node identity and docker config file are used.

+ + +allowUntrustedImages
+ +bool + + + +

AllowUntrustedImages sets lakom webhook to allow images without trusted signature. +Instead to deny the request, the webhook will allow it with a warning.

+ +

DebugConfig diff --git a/pkg/admission/admission_suite_test.go b/pkg/admission/admission_suite_test.go deleted file mode 100644 index 7048c026..00000000 --- a/pkg/admission/admission_suite_test.go +++ /dev/null @@ -1,40 +0,0 @@ -/* -Copyright 2018 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. - -This file was copied and modified from the kubernetes-sigs/controller-runtime project -https://github.com/kubernetes-sigs/controller-runtime/blob/4c9c9564e4652bbdec14a602d6196d8622500b51/pkg/webhook/admission/admission_suite_test.go - -Modifications Copyright 2022 SAP SE or an SAP affiliate company and Gardener contributors -*/ - -package admission - -import ( - "testing" - - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" -) - -func TestAdmissionWebhook(t *testing.T) { - RegisterFailHandler(Fail) - RunSpecs(t, "Admission Webhook Suite") -} - -var _ = BeforeSuite(func() { - logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) -}) diff --git a/pkg/admission/http.go b/pkg/admission/http.go deleted file mode 100644 index 382f8095..00000000 --- a/pkg/admission/http.go +++ /dev/null @@ -1,171 +0,0 @@ -/* -Copyright 2018 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. - -This file was copied and modified from the kubernetes-sigs/controller-runtime project -https://github.com/kubernetes-sigs/controller-runtime/blob/4c9c9564e4652bbdec14a602d6196d8622500b51/pkg/webhook/admission/http.go - -Modifications Copyright 2022 SAP SE or an SAP affiliate company and Gardener contributors -*/ - -package admission - -import ( - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - - "github.com/go-logr/logr" - v1 "k8s.io/api/admission/v1" - "k8s.io/api/admission/v1beta1" - "k8s.io/apimachinery/pkg/runtime" - "k8s.io/apimachinery/pkg/runtime/schema" - "k8s.io/apimachinery/pkg/runtime/serializer" - utilruntime "k8s.io/apimachinery/pkg/util/runtime" - "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var admissionScheme = runtime.NewScheme() -var admissionCodecs = serializer.NewCodecFactory(admissionScheme) - -func init() { - utilruntime.Must(v1.AddToScheme(admissionScheme)) - utilruntime.Must(v1beta1.AddToScheme(admissionScheme)) -} - -var _ http.Handler = &Server{} - -// Server extends sigs.k8s.io/controller-runtime/pkg/webhook/admission.Webhook with custom ServeHTTP method -// so that the admission response code is also written in the HTTP status header. -// This allows the admission controller to run with failurePolicy=Ignore. -type Server struct { - admission.Webhook - - Log logr.Logger -} - -func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { - var body []byte - var err error - ctx := r.Context() - if s.Webhook.WithContextFunc != nil { - ctx = s.Webhook.WithContextFunc(ctx, r) - } - - var reviewResponse admission.Response - if r.Body == nil { - err = errors.New("request body is empty") - s.Log.Error(err, "bad request") - reviewResponse = admission.Errored(http.StatusBadRequest, err) - s.writeResponse(w, reviewResponse) - return - } - - defer func() { - _ = r.Body.Close() - }() - - if body, err = io.ReadAll(r.Body); err != nil { - s.Log.Error(err, "unable to read the body from the incoming request") - reviewResponse = admission.Errored(http.StatusBadRequest, err) - s.writeResponse(w, reviewResponse) - return - } - - // verify the content type is accurate - if contentType := r.Header.Get("Content-Type"); contentType != "application/json" { - err = fmt.Errorf("contentType=%s, expected application/json", contentType) - s.Log.Error(err, "unable to process a request with an unknown content type", "content type", contentType) - reviewResponse = admission.Errored(http.StatusBadRequest, err) - s.writeResponse(w, reviewResponse) - return - } - - // Both v1 and v1beta1 AdmissionReview types are exactly the same, so the v1beta1 type can - // be decoded into the v1 type. However the runtime codec's decoder guesses which type to - // decode into by type name if an Object's TypeMeta isn't set. By setting TypeMeta of an - // unregistered type to the v1 GVK, the decoder will coerce a v1beta1 AdmissionReview to v1. - // The actual AdmissionReview GVK will be used to write a typed response in case the - // webhook config permits multiple versions, otherwise this response will fail. - req := admission.Request{} - ar := unversionedAdmissionReview{} - // avoid an extra copy - ar.Request = &req.AdmissionRequest - ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview")) - _, actualAdmRevGVK, err := admissionCodecs.UniversalDeserializer().Decode(body, nil, &ar) - if err != nil { - s.Log.Error(err, "unable to decode the request") - reviewResponse = admission.Errored(http.StatusBadRequest, err) - s.writeResponse(w, reviewResponse) - return - } - s.Log.V(1).Info("received request", "UID", req.UID, "kind", req.Kind, "resource", req.Resource) - - reviewResponse = s.Webhook.Handle(ctx, req) - s.writeResponseTyped(w, reviewResponse, actualAdmRevGVK) -} - -// writeResponse writes response to w generically, i.e. without encoding GVK information. -func (s *Server) writeResponse(w http.ResponseWriter, response admission.Response) { - s.writeAdmissionResponse(w, v1.AdmissionReview{Response: &response.AdmissionResponse}) -} - -// writeResponseTyped writes response to w with GVK set to admRevGVK, which is necessary -// if multiple AdmissionReview versions are permitted by the webhook. -func (s *Server) writeResponseTyped(w http.ResponseWriter, response admission.Response, admRevGVK *schema.GroupVersionKind) { - ar := v1.AdmissionReview{ - Response: &response.AdmissionResponse, - } - // Default to a v1 AdmissionReview, otherwise the API server may not recognize the request - // if multiple AdmissionReview versions are permitted by the webhook config. - // TODO(estroz): this should be configurable since older API servers won't know about v1. - if admRevGVK == nil || *admRevGVK == (schema.GroupVersionKind{}) { - ar.SetGroupVersionKind(v1.SchemeGroupVersion.WithKind("AdmissionReview")) - } else { - ar.SetGroupVersionKind(*admRevGVK) - } - s.writeAdmissionResponse(w, ar) -} - -// writeAdmissionResponse writes ar to w. -func (s *Server) writeAdmissionResponse(w http.ResponseWriter, ar v1.AdmissionReview) { - w.WriteHeader(int(ar.Response.Result.Code)) - if err := json.NewEncoder(w).Encode(ar); err != nil { - s.Log.Error(err, "unable to encode and write the response") - // Since the `ar v1.AdmissionReview` is a clear and legal object, - // it should not have problem to be marshalled into bytes. - // The error here is probably caused by the abnormal HTTP connection, - // e.g., broken pipe, so we can only write the error response once, - // to avoid endless circular calling. - serverError := admission.Errored(http.StatusInternalServerError, err) - w.WriteHeader(int(serverError.Result.Code)) - if err = json.NewEncoder(w).Encode(v1.AdmissionReview{Response: &serverError.AdmissionResponse}); err != nil { - s.Log.Error(err, "still unable to encode and write the InternalServerError response") - } - } else { - res := ar.Response - if log := s.Log; log.V(1).Enabled() { - if res.Result != nil { - log = log.WithValues("code", res.Result.Code, "reason", res.Result.Reason) - } - log.V(1).Info("wrote response", "UID", res.UID, "allowed", res.Allowed) - } - } -} - -// unversionedAdmissionReview is used to decode both v1 and v1beta1 AdmissionReview types. -type unversionedAdmissionReview struct { - v1.AdmissionReview -} - -var _ runtime.Object = &unversionedAdmissionReview{} diff --git a/pkg/admission/http_test.go b/pkg/admission/http_test.go deleted file mode 100644 index 1e9619b6..00000000 --- a/pkg/admission/http_test.go +++ /dev/null @@ -1,282 +0,0 @@ -/* -Copyright 2018 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. - -This file was copied and modified from the kubernetes-sigs/controller-runtime project -https://github.com/kubernetes-sigs/controller-runtime/blob/4c9c9564e4652bbdec14a602d6196d8622500b51/pkg/webhook/admission/http_test.go - -Modifications Copyright 2022 SAP SE or an SAP affiliate company and Gardener contributors -*/ - -package admission_test - -import ( - "bytes" - "context" - "fmt" - "io" - "net/http" - "net/http/httptest" - "time" - - "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/admission" - - "github.com/go-logr/logr" - . "github.com/onsi/ginkgo/v2" - . "github.com/onsi/gomega" - admissionv1 "k8s.io/api/admission/v1" - logf "sigs.k8s.io/controller-runtime/pkg/log" - cradmission "sigs.k8s.io/controller-runtime/pkg/webhook/admission" -) - -var _ = Describe("Admission Webhooks", func() { - - const ( - gvkJSONv1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1"` - gvkJSONv1beta1 = `"kind":"AdmissionReview","apiVersion":"admission.k8s.io/v1beta1"` - ) - - Describe("HTTP Handler", func() { - var ( - logger logr.Logger - respRecorder *httptest.ResponseRecorder - server *admission.Server - ) - - BeforeEach(func() { - logger = logf.Log - respRecorder = &httptest.ResponseRecorder{ - Body: bytes.NewBuffer(nil), - } - - server = &admission.Server{ - Log: logger, - } - }) - - It("should return bad-request when given an empty body", func() { - req := &http.Request{Body: nil} - - expected := `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"request body is empty","code":400}}} -` - server.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusBadRequest)) - }) - - It("should return bad-request when given the wrong content-type", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/foo"}}, - Body: nopCloser{Reader: bytes.NewBuffer(nil)}, - } - - expected := - `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"contentType=application/foo, expected application/json","code":400}}} -` - server.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusBadRequest)) - }) - - It("should return bad-request when given an undecodable body", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString("{")}, - } - - expected := - `{"response":{"uid":"","allowed":false,"status":{"metadata":{},"message":"couldn't get version/kind; json parse error: unexpected end of JSON input","code":400}}} -` - server.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusBadRequest)) - }) - - It("should return the response given by the handler with version defaulted to v1", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)}, - } - server := admission.Server{ - Webhook: cradmission.Webhook{ - Handler: &fakeHandler{}, - }, - Log: logger.WithName("server"), - } - - expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}} -`, gvkJSONv1) - server.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusOK)) - }) - - It("should return the v1 response given by the handler", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))}, - } - server := admission.Server{ - Webhook: cradmission.Webhook{ - Handler: &fakeHandler{}, - }, - Log: logger.WithName("server"), - } - - expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}} -`, gvkJSONv1) - server.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusOK)) - }) - - It("should return the v1beta1 response given by the handler", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1beta1))}, - } - server := admission.Server{ - Webhook: cradmission.Webhook{ - Handler: &fakeHandler{}, - }, - Log: logger.WithName("server"), - } - - expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"code":200}}} -`, gvkJSONv1beta1) - server.ServeHTTP(respRecorder, req) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusOK)) - }) - - It("should present the Context from the HTTP request, if any", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)}, - } - type ctxkey int - const key ctxkey = 1 - const value = "from-ctx" - server := admission.Server{ - Webhook: cradmission.Webhook{ - Handler: &fakeHandler{ - fn: func(ctx context.Context, req cradmission.Request) cradmission.Response { - <-ctx.Done() - return cradmission.Allowed(ctx.Value(key).(string)) - }, - }, - }, - Log: logger.WithName("server"), - } - - expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}} -`, gvkJSONv1, value) - - ctx, cancel := context.WithCancel(context.WithValue(context.Background(), key, value)) - cancel() - server.ServeHTTP(respRecorder, req.WithContext(ctx)) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusOK)) - }) - - It("should mutate the Context from the HTTP request, if func supplied", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString(`{"request":{}}`)}, - } - type ctxkey int - const key ctxkey = 1 - server := admission.Server{ - Webhook: cradmission.Webhook{ - Handler: &fakeHandler{ - fn: func(ctx context.Context, req cradmission.Request) cradmission.Response { - return cradmission.Allowed(ctx.Value(key).(string)) - }, - }, - WithContextFunc: func(ctx context.Context, r *http.Request) context.Context { - return context.WithValue(ctx, key, r.Header["Content-Type"][0]) - }, - }, - Log: logger.WithName("server"), - } - - expected := fmt.Sprintf(`{%s,"response":{"uid":"","allowed":true,"status":{"metadata":{},"message":%q,"code":200}}} -`, gvkJSONv1, "application/json") - - ctx, cancel := context.WithCancel(context.Background()) - cancel() - server.ServeHTTP(respRecorder, req.WithContext(ctx)) - Expect(respRecorder.Body.String()).To(Equal(expected)) - Expect(respRecorder.Code).To(Equal(http.StatusOK)) - }) - - It("should never run into circular calling if the writer has broken", func() { - req := &http.Request{ - Header: http.Header{"Content-Type": []string{"application/json"}}, - Body: nopCloser{Reader: bytes.NewBufferString(fmt.Sprintf(`{%s,"request":{}}`, gvkJSONv1))}, - } - server := admission.Server{ - Webhook: cradmission.Webhook{ - Handler: &fakeHandler{}, - }, - Log: logger.WithName("server"), - } - - bw := &brokenWriter{ResponseWriter: respRecorder} - Eventually(func() int { - // This should not be blocked by the circular calling of writeResponse and writeAdmissionResponse - server.ServeHTTP(bw, req) - return respRecorder.Body.Len() - }, time.Second*3).Should(Equal(0)) - }) - }) -}) - -type nopCloser struct { - io.Reader -} - -func (nopCloser) Close() error { return nil } - -type fakeHandler struct { - invoked bool - fn func(context.Context, cradmission.Request) cradmission.Response - decoder *cradmission.Decoder - injectedString string -} - -func (h *fakeHandler) InjectDecoder(d *cradmission.Decoder) error { - h.decoder = d - return nil -} - -func (h *fakeHandler) InjectString(s string) error { - h.injectedString = s - return nil -} - -func (h *fakeHandler) Handle(ctx context.Context, req cradmission.Request) cradmission.Response { - h.invoked = true - if h.fn != nil { - return h.fn(ctx, req) - } - return cradmission.Response{AdmissionResponse: admissionv1.AdmissionResponse{ - Allowed: true, - }} -} - -type brokenWriter struct { - http.ResponseWriter -} - -func (bw *brokenWriter) Write(_ []byte) (int, error) { - return 0, fmt.Errorf("mock: write: broken pipe") -} diff --git a/pkg/apis/config/types.go b/pkg/apis/config/types.go index 797fe706..1c16331c 100644 --- a/pkg/apis/config/types.go +++ b/pkg/apis/config/types.go @@ -19,8 +19,6 @@ type Configuration struct { HealthCheckConfig *healthcheckconfig.HealthCheckConfig // CosignPublicKeys is the cosign public keys used to verify image signatures. CosignPublicKeys []string - // FailurePolicy is the failure policy used to configure the failurePolicy of the lakom admission webhooks. - FailurePolicy *string // DebugConfig contains debug configurations for the controller. DebugConfig *DebugConfig // SeedBootstrap configures the seed bootstrap controller. @@ -28,6 +26,9 @@ type Configuration struct { // UseOnlyImagePullSecrets sets lakom to use only the image pull secrets of the pod to access the OCI registry. // Otherwise, also the node identity and docker config file are used. UseOnlyImagePullSecrets bool + // AllowUntrustedImages sets lakom webhook to allow images without trusted signature. + // Instead to deny the request, the webhook will allow it with a warning. + AllowUntrustedImages bool } // DebugConfig contains debug configurations for the controller. diff --git a/pkg/apis/config/v1alpha1/types.go b/pkg/apis/config/v1alpha1/types.go index 176b7bce..8a03e760 100644 --- a/pkg/apis/config/v1alpha1/types.go +++ b/pkg/apis/config/v1alpha1/types.go @@ -21,9 +21,6 @@ type Configuration struct { HealthCheckConfig *healthcheckconfigv1alpha1.HealthCheckConfig `json:"healthCheckConfig,omitempty"` // CosignPublicKeys is the cosign public keys used to verify image signatures. CosignPublicKeys []string `json:"cosignPublicKeys,omitempty"` - // FailurePolicy is the failure policy used to configure the failurePolicy of the lakom admission webhooks. - // +optional - FailurePolicy *string `json:"failurePolicy,omitempty"` // DebugConfig contains debug configurations for the controller. // +optional DebugConfig *DebugConfig `json:"debugConfig,omitempty"` @@ -32,6 +29,9 @@ type Configuration struct { // UseOnlyImagePullSecrets sets lakom to use only the image pull secrets of the pod to access the OCI registry. // Otherwise, also the node identity and docker config file are used. UseOnlyImagePullSecrets bool `json:"useOnlyImagePullSecrets"` + // AllowUntrustedImages sets lakom webhook to allow images without trusted signature. + // Instead to deny the request, the webhook will allow it with a warning. + AllowUntrustedImages bool `json:"allowUntrustedImages"` } // DebugConfig contains debug configurations for the controller. diff --git a/pkg/apis/config/v1alpha1/zz_generated.conversion.go b/pkg/apis/config/v1alpha1/zz_generated.conversion.go index 4d685360..c29b0695 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.conversion.go +++ b/pkg/apis/config/v1alpha1/zz_generated.conversion.go @@ -62,12 +62,12 @@ func RegisterConversions(s *runtime.Scheme) error { func autoConvert_v1alpha1_Configuration_To_config_Configuration(in *Configuration, out *config.Configuration, s conversion.Scope) error { out.HealthCheckConfig = (*apisconfig.HealthCheckConfig)(unsafe.Pointer(in.HealthCheckConfig)) out.CosignPublicKeys = *(*[]string)(unsafe.Pointer(&in.CosignPublicKeys)) - out.FailurePolicy = (*string)(unsafe.Pointer(in.FailurePolicy)) out.DebugConfig = (*config.DebugConfig)(unsafe.Pointer(in.DebugConfig)) if err := Convert_v1alpha1_SeedBootstrap_To_config_SeedBootstrap(&in.SeedBootstrap, &out.SeedBootstrap, s); err != nil { return err } out.UseOnlyImagePullSecrets = in.UseOnlyImagePullSecrets + out.AllowUntrustedImages = in.AllowUntrustedImages return nil } @@ -79,12 +79,12 @@ func Convert_v1alpha1_Configuration_To_config_Configuration(in *Configuration, o func autoConvert_config_Configuration_To_v1alpha1_Configuration(in *config.Configuration, out *Configuration, s conversion.Scope) error { out.HealthCheckConfig = (*configv1alpha1.HealthCheckConfig)(unsafe.Pointer(in.HealthCheckConfig)) out.CosignPublicKeys = *(*[]string)(unsafe.Pointer(&in.CosignPublicKeys)) - out.FailurePolicy = (*string)(unsafe.Pointer(in.FailurePolicy)) out.DebugConfig = (*DebugConfig)(unsafe.Pointer(in.DebugConfig)) if err := Convert_config_SeedBootstrap_To_v1alpha1_SeedBootstrap(&in.SeedBootstrap, &out.SeedBootstrap, s); err != nil { return err } out.UseOnlyImagePullSecrets = in.UseOnlyImagePullSecrets + out.AllowUntrustedImages = in.AllowUntrustedImages return nil } diff --git a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go index b03a4a7b..fdc4bbd7 100644 --- a/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/config/v1alpha1/zz_generated.deepcopy.go @@ -28,11 +28,6 @@ func (in *Configuration) DeepCopyInto(out *Configuration) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.FailurePolicy != nil { - in, out := &in.FailurePolicy, &out.FailurePolicy - *out = new(string) - **out = **in - } if in.DebugConfig != nil { in, out := &in.DebugConfig, &out.DebugConfig *out = new(DebugConfig) diff --git a/pkg/apis/config/zz_generated.deepcopy.go b/pkg/apis/config/zz_generated.deepcopy.go index 0246891f..44b74555 100644 --- a/pkg/apis/config/zz_generated.deepcopy.go +++ b/pkg/apis/config/zz_generated.deepcopy.go @@ -28,11 +28,6 @@ func (in *Configuration) DeepCopyInto(out *Configuration) { *out = make([]string, len(*in)) copy(*out, *in) } - if in.FailurePolicy != nil { - in, out := &in.FailurePolicy, &out.FailurePolicy - *out = new(string) - **out = **in - } if in.DebugConfig != nil { in, out := &in.DebugConfig, &out.DebugConfig *out = new(DebugConfig) diff --git a/pkg/controller/lifecycle/actuator.go b/pkg/controller/lifecycle/actuator.go index 5952dab5..7561dfcc 100644 --- a/pkg/controller/lifecycle/actuator.go +++ b/pkg/controller/lifecycle/actuator.go @@ -11,12 +11,12 @@ import ( "strings" "time" - "github.com/Masterminds/semver/v3" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/apis/config" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/constants" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/imagevector" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/secrets" + "github.com/Masterminds/semver/v3" "github.com/gardener/gardener/extensions/pkg/controller" "github.com/gardener/gardener/extensions/pkg/controller/extension" extensionssecretsmanager "github.com/gardener/gardener/extensions/pkg/util/secret/manager" @@ -44,7 +44,6 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/sets" vpaautoscalingv1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" "k8s.io/client-go/rest" "k8s.io/component-base/version" @@ -148,26 +147,17 @@ func (a *actuator) Reconcile(ctx context.Context, logger logr.Logger, ex *extens a.serviceConfig.CosignPublicKeys, image.String(), a.serviceConfig.UseOnlyImagePullSecrets, + a.serviceConfig.AllowUntrustedImages, seedK8sSemverVersion, ) if err != nil { return err } - var ( - failurePolicy = admissionregistration.Fail - allowedFailurePolicies = sets.NewString(string(admissionregistration.Fail), string(admissionregistration.Ignore)) - ) - - if a.serviceConfig.FailurePolicy != nil && allowedFailurePolicies.Has(*a.serviceConfig.FailurePolicy) { - failurePolicy = admissionregistration.FailurePolicyType(*a.serviceConfig.FailurePolicy) - } - shootResources, err := getShootResources( caBundleSecret.Data[secretutils.DataKeyCertificateBundle], namespace, lakomShootAccessSecret.ServiceAccountName, - failurePolicy, ) if err != nil { @@ -275,7 +265,7 @@ func getLabels() map[string]string { } } -func getSeedResources(lakomReplicas *int32, namespace, genericKubeconfigName, shootAccessSecretName, serverTLSSecretName string, cosignPublicKeys []string, image string, useOnlyImagePullSecrets bool, k8sVersion *semver.Version) (map[string][]byte, error) { +func getSeedResources(lakomReplicas *int32, namespace, genericKubeconfigName, shootAccessSecretName, serverTLSSecretName string, cosignPublicKeys []string, image string, useOnlyImagePullSecrets, allowUntrustedImages bool, k8sVersion *semver.Version) (map[string][]byte, error) { var ( tcpProto = corev1.ProtocolTCP serverPort = intstr.FromInt(10250) @@ -365,6 +355,7 @@ func getSeedResources(lakomReplicas *int32, namespace, genericKubeconfigName, sh "--port=" + serverPort.String(), "--kubeconfig=" + gutil.PathGenericKubeconfig, "--use-only-image-pull-secrets=" + strconv.FormatBool(useOnlyImagePullSecrets), + "--insecure-allow-untrusted-images=" + strconv.FormatBool(allowUntrustedImages), }, Ports: []corev1.ContainerPort{ { @@ -576,10 +567,11 @@ func getSeedResources(lakomReplicas *int32, namespace, genericKubeconfigName, sh return resources, nil } -func getShootResources(webhookCaBundle []byte, namespace, shootAccessServiceAccountName string, failurePolicy admissionregistration.FailurePolicyType) (map[string][]byte, error) { +func getShootResources(webhookCaBundle []byte, namespace, shootAccessServiceAccountName string) (map[string][]byte, error) { var ( matchPolicy = admissionregistration.Equivalent sideEffectClass = admissionregistration.SideEffectClassNone + failurePolicy = admissionregistration.Fail timeOutSeconds = ptr.To[int32](25) webhookHost = fmt.Sprintf("https://%s.%s", constants.ExtensionServiceName, namespace) validatingWebhookURL = webhookHost + constants.LakomVerifyCosignSignaturePath diff --git a/pkg/controller/lifecycle/actuator_test.go b/pkg/controller/lifecycle/actuator_test.go index cc0510ae..98048dd6 100644 --- a/pkg/controller/lifecycle/actuator_test.go +++ b/pkg/controller/lifecycle/actuator_test.go @@ -7,13 +7,13 @@ package lifecycle import ( b64 "encoding/base64" "fmt" + "strconv" "strings" "github.com/Masterminds/semver/v3" "github.com/gardener/gardener/pkg/resourcemanager/controller/garbagecollector/references" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" ) var _ = Describe("Actuator", func() { @@ -49,7 +49,6 @@ var _ = Describe("Actuator", func() { const ( namespace = "shoot--for--bar" shootAccessServiceAccountName = "extension-shoot-lakom-service-access" - failurePolicy = admissionregistrationv1.Ignore validatingWebhookKey = "validatingwebhookconfiguration____gardener-extension-shoot-lakom-service-shoot.yaml" mutatingWebhookKey = "mutatingwebhookconfiguration____gardener-extension-shoot-lakom-service-shoot.yaml" roleKey = "role__kube-system__gardener-extension-shoot-lakom-service-resource-reader.yaml" @@ -61,47 +60,47 @@ var _ = Describe("Actuator", func() { It("Should ensure the correct shoot resources are created", func() { - resources, err := getShootResources(caBundle, namespace, shootAccessServiceAccountName, failurePolicy) + resources, err := getShootResources(caBundle, namespace, shootAccessServiceAccountName) Expect(err).ToNot(HaveOccurred()) Expect(resources).To(HaveLen(4)) Expect(resources).To(Equal(map[string][]byte{ - validatingWebhookKey: []byte(expectedSeedValidatingWebhook(caBundle, namespace, failurePolicy)), - mutatingWebhookKey: []byte(expectedShootMutatingWebhook(caBundle, namespace, failurePolicy)), + validatingWebhookKey: []byte(expectedSeedValidatingWebhook(caBundle, namespace)), + mutatingWebhookKey: []byte(expectedShootMutatingWebhook(caBundle, namespace)), roleKey: []byte(expectedShootRole()), roleBindingKey: []byte(expectedShootRoleBinding(shootAccessServiceAccountName)), })) }) DescribeTable("Should ensure the mutating webhook config is correctly set", - func(ca []byte, ns string, fp admissionregistrationv1.FailurePolicyType) { - resources, err := getShootResources(ca, ns, shootAccessServiceAccountName, fp) + func(ca []byte, ns string) { + resources, err := getShootResources(ca, ns, shootAccessServiceAccountName) Expect(err).ToNot(HaveOccurred()) mutatingWebhook, ok := resources[mutatingWebhookKey] Expect(ok).To(BeTrue()) - Expect(string(mutatingWebhook)).To(Equal(expectedShootMutatingWebhook(ca, ns, fp))) + Expect(string(mutatingWebhook)).To(Equal(expectedShootMutatingWebhook(ca, ns))) }, - Entry("Failure policy Fail", caBundle, namespace, admissionregistrationv1.Fail), - Entry("Failure policy Ignore", []byte("anotherCABundle"), "different-namespace", admissionregistrationv1.Ignore), + Entry("Global CA bundle and namespace name", caBundle, namespace), + Entry("Custom CA bundle and namespace name", []byte("anotherCABundle"), "different-namespace"), ) DescribeTable("Should ensure the validating webhook config is correctly set", - func(ca []byte, ns string, fp admissionregistrationv1.FailurePolicyType) { - resources, err := getShootResources(ca, ns, shootAccessServiceAccountName, fp) + func(ca []byte, ns string) { + resources, err := getShootResources(ca, ns, shootAccessServiceAccountName) Expect(err).ToNot(HaveOccurred()) validatingWebhook, ok := resources[validatingWebhookKey] Expect(ok).To(BeTrue()) - Expect(string(validatingWebhook)).To(Equal(expectedSeedValidatingWebhook(ca, ns, fp))) + Expect(string(validatingWebhook)).To(Equal(expectedSeedValidatingWebhook(ca, ns))) }, - Entry("Failure policy Fail", caBundle, namespace, admissionregistrationv1.Fail), - Entry("Failure policy Ignore", []byte("anotherCABundle"), "different-namespace", admissionregistrationv1.Ignore), + Entry("Global CA bundle and namespace name", caBundle, namespace), + Entry("Custom CA bundle and namespace name", []byte("anotherCABundle"), "different-namespace"), ) DescribeTable("Should ensure the rolebinding is correctly set", func(saName string) { - resources, err := getShootResources(caBundle, namespace, saName, failurePolicy) + resources, err := getShootResources(caBundle, namespace, saName) Expect(err).ToNot(HaveOccurred()) roleBinding, ok := resources[roleBindingKey] @@ -121,7 +120,6 @@ var _ = Describe("Actuator", func() { shootAccessServiceAccountName = "extension-shoot-lakom-service" serverTLSSecretName = "shoot-lakom-service-tls" //#nosec G101 -- this is false positive image = "europe-docker.pkg.dev/gardener-project/releases/gardener/extensions/lakom:v0.0.0" - useOnlyImagePullSecrets = true cosignSecretName = "extension-shoot-lakom-service-cosign-public-keys-e3b0c442" cosignSecretNameKey = "secret__" + namespace + "__" + cosignSecretName + ".yaml" @@ -155,7 +153,7 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== }) DescribeTable("Should ensure resources are correctly created for different Kubernetes versions", - func(k8sVersion *semver.Version, withUnhealthyPodEvictionPolicy bool) { + func(k8sVersion *semver.Version, withUnhealthyPodEvictionPolicy, useOnlyImagePullSecrets, allowUntrustedImages bool) { resources, err := getSeedResources( &replicas, namespace, @@ -165,6 +163,7 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== cosignPublicKeys, image, useOnlyImagePullSecrets, + allowUntrustedImages, k8sVersion, ) Expect(err).ToNot(HaveOccurred()) @@ -172,7 +171,7 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== expectedResources := map[string]string{ configMapKey: expectedSeedConfigMap(namespace), - deploymentKey: expectedSeedDeployment(replicas, namespace, genericKubeconfigName, shootAccessServiceAccountName, image, cosignSecretName, serverTLSSecretName), + deploymentKey: expectedSeedDeployment(replicas, namespace, genericKubeconfigName, shootAccessServiceAccountName, image, cosignSecretName, serverTLSSecretName, strconv.FormatBool(useOnlyImagePullSecrets), strconv.FormatBool(allowUntrustedImages)), pdbKey: expectedSeedPDB(namespace, withUnhealthyPodEvictionPolicy), cosignSecretNameKey: expectedSeedSecretCosign(namespace, cosignSecretName, cosignPublicKeys), serviceKey: expectedSeedService(namespace), @@ -188,17 +187,16 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== Expect(strResource).To(Equal(expectedResource), key) } }, - Entry("Kubernetes version < 1.26", semver.MustParse("1.25.0"), false), - Entry("Kubernetes version >= 1.26", semver.MustParse("1.26.0"), true), + Entry("Kubernetes version < 1.26", semver.MustParse("1.25.0"), false, false, false), + Entry("Kubernetes version >= 1.26", semver.MustParse("1.26.0"), true, false, false), + Entry("Use only image pull secrets", semver.MustParse("1.27.0"), true, true, false), + Entry("Allow untrusted images", semver.MustParse("1.28.0"), true, false, true), ) }) }) -func expectedShootMutatingWebhook(caBundle []byte, namespace string, failurePolicy admissionregistrationv1.FailurePolicyType) string { - var ( - caBundleEncoded = b64.StdEncoding.EncodeToString(caBundle) - strFailurePolicy = string(failurePolicy) - ) +func expectedShootMutatingWebhook(caBundle []byte, namespace string) string { + caBundleEncoded := b64.StdEncoding.EncodeToString(caBundle) return `apiVersion: admissionregistration.k8s.io/v1 kind: MutatingWebhookConfiguration @@ -215,7 +213,7 @@ webhooks: clientConfig: caBundle: ` + caBundleEncoded + ` url: https://extension-shoot-lakom-service.` + namespace + `/lakom/resolve-tag-to-digest - failurePolicy: ` + strFailurePolicy + ` + failurePolicy: Fail matchPolicy: Equivalent name: resolve-tag.lakom.service.extensions.gardener.cloud namespaceSelector: @@ -246,11 +244,9 @@ webhooks: ` } -func expectedSeedValidatingWebhook(caBundle []byte, namespace string, failurePolicy admissionregistrationv1.FailurePolicyType) string { - var ( - caBundleEncoded = b64.StdEncoding.EncodeToString(caBundle) - strFailurePolicy = string(failurePolicy) - ) +func expectedSeedValidatingWebhook(caBundle []byte, namespace string) string { + caBundleEncoded := b64.StdEncoding.EncodeToString(caBundle) + return `apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration metadata: @@ -266,7 +262,7 @@ webhooks: clientConfig: caBundle: ` + caBundleEncoded + ` url: https://extension-shoot-lakom-service.` + namespace + `/lakom/verify-cosign-signature - failurePolicy: ` + strFailurePolicy + ` + failurePolicy: Fail matchPolicy: Equivalent name: verify-signature.lakom.service.extensions.gardener.cloud namespaceSelector: @@ -377,7 +373,7 @@ metadata: ` } -func expectedSeedDeployment(replicas int32, namespace, genericKubeconfigSecretName, shootAccessSecretName, image, cosignPublicKeysSecretName, serverTLSSecretName string) string { +func expectedSeedDeployment(replicas int32, namespace, genericKubeconfigSecretName, shootAccessSecretName, image, cosignPublicKeysSecretName, serverTLSSecretName, useOnlyImagePullSecrets, allowUntrustedImages string) string { var ( genericKubeconfigSecretNameAnnotationKey = references.AnnotationKey("secret", genericKubeconfigSecretName) shootAccessSecretNameAnnotationKey = references.AnnotationKey("secret", shootAccessSecretName) @@ -451,7 +447,8 @@ spec: - --metrics-bind-address=:8080 - --port=10250 - --kubeconfig=/var/run/secrets/gardener.cloud/shoot/generic-kubeconfig/kubeconfig - - --use-only-image-pull-secrets=true + - --use-only-image-pull-secrets=` + useOnlyImagePullSecrets + ` + - --insecure-allow-untrusted-images=` + allowUntrustedImages + ` image: ` + image + ` imagePullPolicy: IfNotPresent livenessProbe: @@ -518,7 +515,12 @@ status: {} } func expectedSeedPDB(namespace string, withUnhealthyPodEvictionPolicy bool) string { - out := `apiVersion: policy/v1 + unhealthyPodEvictionPolicyStr := "" + if withUnhealthyPodEvictionPolicy { + unhealthyPodEvictionPolicyStr = ` unhealthyPodEvictionPolicy: AlwaysAllow +` + } + return `apiVersion: policy/v1 kind: PodDisruptionBudget metadata: creationTimestamp: null @@ -533,18 +535,12 @@ spec: matchLabels: app.kubernetes.io/name: lakom app.kubernetes.io/part-of: shoot-lakom-service -` - if withUnhealthyPodEvictionPolicy { - out += ` unhealthyPodEvictionPolicy: AlwaysAllow -` - } - out += `status: +` + unhealthyPodEvictionPolicyStr + `status: currentHealthy: 0 desiredHealthy: 0 disruptionsAllowed: 0 expectedPods: 0 ` - return out } func expectedSeedSecretCosign(namespace, cosignSecretName string, cosignPublicKeys []string) string { diff --git a/pkg/controller/seed/add.go b/pkg/controller/seed/add.go index f3c85017..c7c7209f 100644 --- a/pkg/controller/seed/add.go +++ b/pkg/controller/seed/add.go @@ -8,9 +8,9 @@ import ( "context" "fmt" - "github.com/Masterminds/semver/v3" controllerconfig "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/controller/config" + "github.com/Masterminds/semver/v3" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" diff --git a/pkg/controller/seed/reconciler.go b/pkg/controller/seed/reconciler.go index 997ef56f..74207f7b 100644 --- a/pkg/controller/seed/reconciler.go +++ b/pkg/controller/seed/reconciler.go @@ -11,11 +11,11 @@ import ( "strings" "time" - "github.com/Masterminds/semver/v3" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/apis/config" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/constants" "github.com/gardener/gardener-extension-shoot-lakom-service/pkg/imagevector" + "github.com/Masterminds/semver/v3" extensionssecretsmanager "github.com/gardener/gardener/extensions/pkg/util/secret/manager" v1beta1constants "github.com/gardener/gardener/pkg/apis/core/v1beta1/constants" resourcesv1alpha1 "github.com/gardener/gardener/pkg/apis/resources/v1alpha1" @@ -36,7 +36,6 @@ import ( "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/sets" vpaautoscalingv1 "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/apis/autoscaling.k8s.io/v1" "k8s.io/component-base/version" "k8s.io/utils/clock" @@ -104,21 +103,13 @@ func (kcr *kubeSystemReconciler) reconcile(ctx context.Context, logger logr.Logg image.Tag = ptr.To[string](version.Get().GitVersion) } - var ( - failurePolicy = admissionregistration.Fail - allowedFailurePolicies = sets.NewString(string(admissionregistration.Fail), string(admissionregistration.Ignore)) - ) - if kcr.serviceConfig.FailurePolicy != nil && allowedFailurePolicies.Has(*kcr.serviceConfig.FailurePolicy) { - failurePolicy = admissionregistration.FailurePolicyType(*kcr.serviceConfig.FailurePolicy) - } - resources, err := getResources( generatedSecrets[constants.SeedWebhookTLSSecretName].Name, image.String(), kcr.serviceConfig.CosignPublicKeys, caBundleSecret.Data[secretutils.DataKeyCertificateBundle], - failurePolicy, kcr.serviceConfig.UseOnlyImagePullSecrets, + kcr.serviceConfig.AllowUntrustedImages, kcr.seedK8sVersion, ) if err != nil { @@ -183,7 +174,7 @@ func (kcr *kubeSystemReconciler) setOwnerReferenceToSecrets(ctx context.Context, return nil } -func getResources(serverTLSSecretName, image string, cosignPublicKeys []string, webhookCaBundle []byte, failurePolicy admissionregistration.FailurePolicyType, useOnlyImagePullSecrets bool, k8sVersion *semver.Version) (map[string][]byte, error) { +func getResources(serverTLSSecretName, image string, cosignPublicKeys []string, webhookCaBundle []byte, useOnlyImagePullSecrets, allowUntrustedImages bool, k8sVersion *semver.Version) (map[string][]byte, error) { var ( tcpProto = corev1.ProtocolTCP serverPort = intstr.FromInt(10250) @@ -201,6 +192,7 @@ func getResources(serverTLSSecretName, image string, cosignPublicKeys []string, kubeSystemNamespace = metav1.NamespaceSystem matchPolicy = admissionregistration.Equivalent sideEffectClass = admissionregistration.SideEffectClassNone + failurePolicy = admissionregistration.Fail timeOutSeconds = ptr.To[int32](25) namespaceSelector = metav1.LabelSelector{ MatchExpressions: []metav1.LabelSelectorRequirement{ @@ -293,6 +285,7 @@ func getResources(serverTLSSecretName, image string, cosignPublicKeys []string, "--metrics-bind-address=:" + metricsPort.String(), "--port=" + serverPort.String(), "--use-only-image-pull-secrets=" + strconv.FormatBool(useOnlyImagePullSecrets), + "--insecure-allow-untrusted-images=" + strconv.FormatBool(allowUntrustedImages), }, Ports: []corev1.ContainerPort{ { diff --git a/pkg/controller/seed/reconciler_test.go b/pkg/controller/seed/reconciler_test.go index 4c8b65dc..2dcc84a0 100644 --- a/pkg/controller/seed/reconciler_test.go +++ b/pkg/controller/seed/reconciler_test.go @@ -6,13 +6,13 @@ package seed import ( b64 "encoding/base64" + "strconv" "strings" "github.com/Masterminds/semver/v3" "github.com/gardener/gardener/pkg/resourcemanager/controller/garbagecollector/references" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" - admissionregistrationv1 "k8s.io/api/admissionregistration/v1" ) var _ = Describe("Reconciler", func() { @@ -34,11 +34,11 @@ var _ = Describe("Reconciler", func() { const ( namespace = "kube-system" ownerNamespace = "garden" - failurePolicy = admissionregistrationv1.Ignore cosignSecretName = "extension-shoot-lakom-service-seed-cosign-public-keys-e3b0c442" serverTLSSecretName = "shoot-lakom-service-seed-tls" //#nosec G101 -- this is false positive image = "europe-docker.pkg.dev/gardener-project/releases/gardener/extensions/lakom:v0.0.0" useOnlyImagePullSecrets = true + allowUntrustedImages = false validatingWebhookKey = "validatingwebhookconfiguration____gardener-extension-shoot-lakom-service-seed.yaml" mutatingWebhookKey = "mutatingwebhookconfiguration____gardener-extension-shoot-lakom-service-seed.yaml" @@ -75,14 +75,14 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== }) DescribeTable("Should ensure resources are correctly created for different Kubernetes versions", - func(k8sVersion string, withUnhealthyPodEvictionPolicy bool) { + func(k8sVersion string, withUnhealthyPodEvictionPolicy, onlyImagePullSecrets, untrustedImages bool) { resources, err := getResources( serverTLSSecretName, image, cosignPublicKeys, caBundle, - failurePolicy, - useOnlyImagePullSecrets, + onlyImagePullSecrets, + untrustedImages, semver.MustParse(k8sVersion), ) @@ -90,11 +90,11 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== Expect(resources).To(HaveLen(10)) expectedResources := map[string]string{ - validatingWebhookKey: expectedValidatingWebhook(caBundle, failurePolicy), - mutatingWebhookKey: expectedMutatingWebhook(caBundle, failurePolicy), + validatingWebhookKey: expectedValidatingWebhook(caBundle), + mutatingWebhookKey: expectedMutatingWebhook(caBundle), clusterRoleKey: expectedClusterRole(), clusterRoleBindingKey: expectedClusterRoleBinding(), - deploymentKey: expectedDeployment(namespace, image, cosignSecretName, serverTLSSecretName), + deploymentKey: expectedDeployment(namespace, image, cosignSecretName, serverTLSSecretName, strconv.FormatBool(onlyImagePullSecrets), strconv.FormatBool(untrustedImages)), pdbKey: expectedPDB(namespace, withUnhealthyPodEvictionPolicy), cosignSecretNameKey: expectedSecretCosign(namespace, cosignSecretName, cosignPublicKeys), serviceKey: expectedService(namespace), @@ -110,50 +110,52 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== Expect(strResource).To(Equal(expectedResource), key, string(resource)) } }, - Entry("Kubernetes version < 1.26", "1.25.0", false), - Entry("Kubernetes version >= 1.26", "1.26.0", true), + Entry("Kubernetes version < 1.26", "1.25.0", false, false, false), + Entry("Kubernetes version >= 1.26", "1.26.0", true, false, false), + Entry("Use only image pull secrets", "1.27.0", true, true, false), + Entry("Allow untrusted images", "1.28.0", true, false, true), ) DescribeTable("Should ensure the mutating webhook config is correctly set", - func(ca []byte, fp admissionregistrationv1.FailurePolicyType) { + func(ca []byte) { resources, err := getResources( serverTLSSecretName, image, cosignPublicKeys, ca, - fp, useOnlyImagePullSecrets, + allowUntrustedImages, k8sVersion, ) Expect(err).ToNot(HaveOccurred()) mutatingWebhook, ok := resources[mutatingWebhookKey] Expect(ok).To(BeTrue()) - Expect(string(mutatingWebhook)).To(Equal(expectedMutatingWebhook(ca, fp))) + Expect(string(mutatingWebhook)).To(Equal(expectedMutatingWebhook(ca))) }, - Entry("Failure policy Fail", caBundle, admissionregistrationv1.Fail), - Entry("Failure policy Ignore", []byte("anotherCABundle"), admissionregistrationv1.Ignore), + Entry("Global CA bundle", caBundle), + Entry("Custom CA bundle", []byte("anotherCABundle")), ) DescribeTable("Should ensure the validating webhook config is correctly set", - func(ca []byte, fp admissionregistrationv1.FailurePolicyType) { + func(ca []byte) { resources, err := getResources( serverTLSSecretName, image, cosignPublicKeys, ca, - fp, useOnlyImagePullSecrets, + allowUntrustedImages, k8sVersion, ) Expect(err).ToNot(HaveOccurred()) validatingWebhook, ok := resources[validatingWebhookKey] Expect(ok).To(BeTrue()) - Expect(string(validatingWebhook)).To(Equal(expectedValidatingWebhook(ca, fp))) + Expect(string(validatingWebhook)).To(Equal(expectedValidatingWebhook(ca))) }, - Entry("Failure policy Fail", caBundle, admissionregistrationv1.Fail), - Entry("Failure policy Ignore", []byte("anotherCABundle"), admissionregistrationv1.Ignore), + Entry("Global CA bundle", caBundle), + Entry("Custom ca bundle", []byte("anotherCABundle")), ) It("Should ensure the clusterrolebinding is correctly set", func() { @@ -162,8 +164,8 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== image, cosignPublicKeys, caBundle, - failurePolicy, useOnlyImagePullSecrets, + allowUntrustedImages, k8sVersion, ) @@ -176,10 +178,9 @@ hjZVcW2ygAvImCAULGph2fqGkNUszl7ycJH/Dntw4wMLSbstUZomqPuIVQ== }) }) -func expectedMutatingWebhook(caBundle []byte, failurePolicy admissionregistrationv1.FailurePolicyType) string { +func expectedMutatingWebhook(caBundle []byte) string { var ( - caBundleEncoded = b64.StdEncoding.EncodeToString(caBundle) - strFailurePolicy = string(failurePolicy) + caBundleEncoded = b64.StdEncoding.EncodeToString(caBundle) ) return `apiVersion: admissionregistration.k8s.io/v1 @@ -200,7 +201,7 @@ webhooks: name: extension-shoot-lakom-service-seed namespace: kube-system path: /lakom/resolve-tag-to-digest - failurePolicy: ` + strFailurePolicy + ` + failurePolicy: Fail matchPolicy: Equivalent name: resolve-tag.seed.lakom.service.extensions.gardener.cloud namespaceSelector: @@ -225,10 +226,9 @@ webhooks: ` } -func expectedValidatingWebhook(caBundle []byte, failurePolicy admissionregistrationv1.FailurePolicyType) string { +func expectedValidatingWebhook(caBundle []byte) string { var ( - caBundleEncoded = b64.StdEncoding.EncodeToString(caBundle) - strFailurePolicy = string(failurePolicy) + caBundleEncoded = b64.StdEncoding.EncodeToString(caBundle) ) return `apiVersion: admissionregistration.k8s.io/v1 kind: ValidatingWebhookConfiguration @@ -248,7 +248,7 @@ webhooks: name: extension-shoot-lakom-service-seed namespace: kube-system path: /lakom/verify-cosign-signature - failurePolicy: ` + strFailurePolicy + ` + failurePolicy: Fail matchPolicy: Equivalent name: verify-signature.seed.lakom.service.extensions.gardener.cloud namespaceSelector: @@ -312,7 +312,7 @@ subjects: ` } -func expectedDeployment(namespace, image, cosignPublicKeysSecretName, serverTLSSecretName string) string { +func expectedDeployment(namespace, image, cosignPublicKeysSecretName, serverTLSSecretName, useOnlyImagePullSecrets, allowUntrustedImages string) string { var ( serverTLSSecretNameAnnotationKey = references.AnnotationKey("secret", serverTLSSecretName) cosignPublicKeysSecretNameAnnotationKey = references.AnnotationKey("secret", cosignPublicKeysSecretName) @@ -380,7 +380,8 @@ spec: - --health-bind-address=:8081 - --metrics-bind-address=:8080 - --port=10250 - - --use-only-image-pull-secrets=true + - --use-only-image-pull-secrets=` + useOnlyImagePullSecrets + ` + - --insecure-allow-untrusted-images=` + allowUntrustedImages + ` image: ` + image + ` imagePullPolicy: IfNotPresent livenessProbe: diff --git a/pkg/lakom/verifysignature/admission.go b/pkg/lakom/verifysignature/admission.go index 888eb4e3..ded01bdd 100644 --- a/pkg/lakom/verifysignature/admission.go +++ b/pkg/lakom/verifysignature/admission.go @@ -35,6 +35,7 @@ type HandleBuilder struct { cacheTTL time.Duration cacheRefreshInterval time.Duration useOnlyImagePullSecrets bool + allowUntrustedImages bool } // NewHandleBuilder returns new handle builder. @@ -55,6 +56,12 @@ func (hb HandleBuilder) WithUseOnlyImagePullSecrets(useOnlyImagePullSecrets bool return hb } +// WithAllowUntrustedImages configures the webhook to allow images without trusted signature. +func (hb HandleBuilder) WithAllowUntrustedImages(allowUntrustedImages bool) HandleBuilder { + hb.allowUntrustedImages = allowUntrustedImages + return hb +} + // WithCosignPublicKeysReader sets the reader with the cosign public keys. func (hb HandleBuilder) WithCosignPublicKeysReader(cosignPublicKeysReader io.Reader) HandleBuilder { hb.cosignPublicKeysReader = cosignPublicKeysReader @@ -87,6 +94,7 @@ func (hb HandleBuilder) Build() (*handler, error) { reader: hb.mgr.GetAPIReader(), decoder: admission.NewDecoder(hb.mgr.GetScheme()), useOnlyImagePullSecrets: hb.useOnlyImagePullSecrets, + allowUntrustedImages: hb.allowUntrustedImages, } verifier Verifier ) @@ -121,6 +129,7 @@ type handler struct { verifier Verifier useOnlyImagePullSecrets bool + allowUntrustedImages bool } var ( @@ -160,6 +169,14 @@ func (h *handler) Handle(ctx context.Context, request admission.Request) admissi logger := h.logger.WithValues("pod", client.ObjectKeyFromObject(pod)) if err := h.validatePod(ctx, logger, pod); err != nil { + if h.allowUntrustedImages { + logger.Info("pod validation failed but untrusted images are allowed", "error", err.Error()) + warningsResponse := admission.Allowed("untrusted images are allowed") + warningsResponse.Warnings = []string{ + fmt.Sprintf("Failed to admit pod with error: %q", err.Error()), + } + return warningsResponse + } logger.Error(err, "pod validation failed") return admission.Denied(err.Error()) } diff --git a/pkg/lakom/verifysignature/admission_test.go b/pkg/lakom/verifysignature/admission_test.go index 544b4217..77d6fd7a 100644 --- a/pkg/lakom/verifysignature/admission_test.go +++ b/pkg/lakom/verifysignature/admission_test.go @@ -103,10 +103,9 @@ var _ = Describe("Admission Handler", func() { WithCosignPublicKeysReader(reader). WithCacheTTL(time.Minute * 10). WithCacheRefreshInterval(time.Second * 30). + WithAllowUntrustedImages(false). Build() - Expect(err).ToNot(HaveOccurred()) - handler = h }) @@ -137,6 +136,39 @@ var _ = Describe("Admission Handler", func() { Expect(response.Result.Code).To(BeEquivalentTo(http.StatusOK)) }) + It("Should allow untrusted images", func() { + mgr.EXPECT().GetAPIReader().Return(apiReader) + mgr.EXPECT().GetScheme().Return(scheme) + reader := strings.NewReader(cosignPublicKey) + + allowUntrustedHandler, err := verifysignature. + NewHandleBuilder(). + WithManager(mgr). + WithLogger(logger.WithName("test-cosign-untrusted-handler")). + WithCosignPublicKeysReader(reader). + WithCacheTTL(time.Minute * 10). + WithCacheRefreshInterval(time.Second * 30). + WithAllowUntrustedImages(true). + Build() + Expect(err).ToNot(HaveOccurred()) + + req := admissionRequestBuilder{ + gvk: podGVK, + operation: admissionv1.Update, + object: podWithImage(pod, "alpine@sha256:11e21d7b981a59554b3f822c49f6e9f57b6068bb74f49c4cd5cc4c663c7e5160"), + }.Build() + ar := allowUntrustedHandler.Handle(ctx, req) + Expect(ar.Allowed).To(BeTrue()) + Expect(ar.Result.Code).To(BeEquivalentTo(http.StatusOK)) + Expect(ar.Warnings).To(ContainElement(ContainSubstring("Failed to admit pod with error"))) + Expect(ar.Warnings).To(ContainElement(ContainSubstring("Forbidden: no valid signature found for image"))) + Expect(ar.Result.Message).To(ContainSubstring("untrusted images are allowed")) + + ar = handler.Handle(ctx, req) + Expect(ar.Allowed).To(BeFalse()) + Expect(ar.Result.Code).To(Satisfy(isHTTPError)) + Expect(ar.Result.Message).To(ContainSubstring("Forbidden: no valid signature found for image")) + }) }) func isHTTPError(code int32) bool {