diff --git a/README.md b/README.md index cc5f2fb1bf2..16169464e85 100644 --- a/README.md +++ b/README.md @@ -358,6 +358,34 @@ status: ``` > NOTE: The supported enforcementActions are [`deny`, `dryrun`] for constraints. Update the `--disable-enforcementaction-validation=true` flag if the desire is to disable enforcementAction validation against the list of supported enforcementActions. +### Exempting Namespaces from the Gatekeeper Admission Webhook + +Note that the following only exempts resources from the admission webhook. They will still be audited. Editing individual constraints is +necessary to exclude them from audit. + +If it becomes necessary to exempt a namespace from Gatekeeper entirely (e.g. you want `kube-system` to bypass admission checks), here's how to do it: + + 1. Make sure the validating admission webhook configuration for Gatekeeper has the following namespace selector: + + ```yaml + namespaceSelector: + matchExpressions: + - key: admission.gatekeeper.sh/ignore + operator: DoesNotExist + ``` + the default Gatekeeper manifest should already have added this. The default name for the + webhook configuration is `gatekeeper-validating-webhook-configuration` and the default + name for the webhook that needs the namespace selector is `validation.gatekeeper.sh` + + 2. Tell Gatekeeper it's okay for the namespace to be ignored by adding a flag to the pod: + `--exempt-namespace=`. This step is necessary because otherwise the + permission to modify a namespace would be equivalent to the permission to exempt everything + in that namespace from policy checks. This way a user must explicitly have permissions + to configure the Gatekeeper pod before they can add exemptions. + + 3. Add the `admission.gatekeeper.sh/ignore` label to the namespace. The value attached + to the label is ignored, so it can be used to annotate the reason for the exemption. + ### Debugging > NOTE: Verbose logging with DEBUG level can be turned on with `--log-level=DEBUG`. By default, the `--log-level` flag is set to minimum log level `INFO`. Acceptable values for minimum log level are [`DEBUG`, `INFO`, `WARNING`, `ERROR`]. In production, this flag should not be set to `DEBUG`. @@ -411,6 +439,55 @@ When applying the constraint using `kubectl apply -f constraint.yaml` with a Con To find the error, run `kubectl get -f [CONSTRAINT_FILENAME].yaml -oyaml`. Build errors are shown in the `status` field. +### Customizing Admission Behavior + +Gatekeeper is a [Kubernetes admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#webhook-configuration) +whose default configuration can be found in the `gatekeeper.yaml` manifest file. By default, it is +a `ValidatingWebhookConfiguration` resource named `gatekeeper-validating-webhook-configuration`. + +Currently the configuration specifies two webhooks: one for checking a request against +the installed constraints and a second webhook for checking labels on namespace requests +that would result in bypassing constraints for the namespace. The namespace-label webhook +is necessary to prevent a privilege escalation where the permission to add a label to a +namespace is equivalent to the ability to bypass all constraints for that namespace. +You can read more about the ability to exempt namespaces by label [above](#exempting-namespaces-from-the-gatekeeper-admission-webhook). + +Because Kubernetes adds features with each version, if you want to know how the webhook can be configured it +is best to look at the official documentation linked at the top of this section. However, two particularly important +configuration options deserve special mention: [timeouts](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#timeouts) and +[failure policy](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#failure-policy). + +Timeouts allow you to configure how long the API server will wait for a response from the admission webhook before it +considers the request to have failed. Note that setting the timeout longer than the overall request timeout +means that the main request will time out before the webhook's failure policy is invoked. + +Failure policy controls what happens when a webhook fails for whatever reason. Common +failure scenarios include timeouts, a 5xx error from the server or the webhook being unavailable. +You have the option to ignore errors, allowing the request through, or failing, rejecting the request. +This results in a direct tradeoff between availability and enforcement. + +Currently Gatekeeper is defaulting to using `Ignore` for the constraint requests. This is because +the webhook server currently only has one instance, which risks downtime during actions like upgrades. +As the theoretical availability improves we will likely change the default to `Fail`. + +The namespace label webhook defaults to `Fail`, this is to help ensure that policies preventing +labels that bypass the webhook from being applied are enforced. Because this webhook only gets +called for namespace modification requests, the impact of downtime is mitigated, making the +theoretical maximum availability less of an issue. + +Because the manifest is available for customization, the webhook configuration can +be tuned to meet your specific needs if they differ from the defaults. + +### Emergency Recovery + +If a situation arises where Gatekeeper is preventing the cluster from operating correctly, +the webhook can be disabled. This will remove all Gatekeeper admission checks. Assuming +the default webhook name has been used this can be achieved by running: + +`kubectl delete validatingwebhookconfigurations.admissionregistration.k8s.io gatekeeper-validating-webhook-configuration` + +Redeploying the webhook configuration will re-enable Gatekeeper. + ## Kick The Tires The [demo/basic](https://github.com/open-policy-agent/gatekeeper/tree/master/demo/basic) directory contains the above examples of simple constraints, templates and configs to play with. The [demo/agilebank](https://github.com/open-policy-agent/gatekeeper/tree/master/demo/agilebank) directory contains more complex examples based on a slightly more realistic scenario. Both folders have a handy demo script to step you through the demos. @@ -419,17 +496,6 @@ The [demo/basic](https://github.com/open-policy-agent/gatekeeper/tree/master/dem ## Finalizers -### Why does Gatekeeper add sync finalizers? - -When Gatekeeper syncs resources it's adding them to OPA's internal cache. This -cache may be used by constraints to render decisions. Because of this stale data -is bad. It can lead to invalid rejections (e.g. when a uniqueness constraint is -violated because an update conflicts with a since-deleted resource), or invalid -acceptance (e.g. if a constraint uses the cache to make sure a Deployment exists -before a Service can be created). Finalizers help avoid stale state by making -sure Gatekeeper has processed the deletion and removed the object from its cache -before the API Server can garbage collect the object. - ### How can I remove finalizers? Why are they hanging around? If Gatekeeper is running, it should automatically clean up the finalizer. If it @@ -448,7 +514,4 @@ If Gatekeeper is not running: * The container was sent a hard kill signal * The container had a panic -It is safest to remove the Config resource before uninstalling Gatekeeper, as -that causes finalizers to be removed outside of the normal GC process. - Finalizers can be removed manually via `kubectl edit` or `kubectl patch` diff --git a/config/default/kustomization.yaml b/config/default/kustomization.yaml index 2d1f4c2a4f6..c0ad03c52ad 100644 --- a/config/default/kustomization.yaml +++ b/config/default/kustomization.yaml @@ -9,8 +9,8 @@ namespace: gatekeeper-system namePrefix: gatekeeper- # Labels to add to all resources and selectors. -#commonLabels: -# someName: someValue +commonLabels: + gatekeeper.sh/system: "yes" bases: - ../crd diff --git a/config/manager/manager.yaml b/config/manager/manager.yaml index ed6640ffac4..0a1dca89a43 100644 --- a/config/manager/manager.yaml +++ b/config/manager/manager.yaml @@ -3,6 +3,7 @@ kind: Namespace metadata: labels: control-plane: controller-manager + admission.gatekeeper.sh/ignore: no-self-managing name: system --- apiVersion: apps/v1 @@ -30,6 +31,7 @@ spec: - "--audit-interval=30" - "--port=8443" - "--logtostderr" + - "--exempt-namespace=gatekeeper-system" image: quay.io/open-policy-agent/gatekeeper:v3.1.0-beta.4 imagePullPolicy: Always name: manager diff --git a/config/webhook/manifests.yaml b/config/webhook/manifests.yaml index 9658c4d0535..54f5cf81ec1 100644 --- a/config/webhook/manifests.yaml +++ b/config/webhook/manifests.yaml @@ -6,6 +6,24 @@ metadata: creationTimestamp: null name: validating-webhook-configuration webhooks: +- clientConfig: + caBundle: Cg== + service: + name: webhook-service + namespace: system + path: /v1/admitlabel + failurePolicy: Fail + name: check-ignore-label.gatekeeper.sh + rules: + - apiGroups: + - "" + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + resources: + - namespaces - clientConfig: caBundle: Cg== service: diff --git a/config/webhook/webhook_patch.yaml b/config/webhook/webhook_patch.yaml index 245f63d3c63..fba78304bd9 100644 --- a/config/webhook/webhook_patch.yaml +++ b/config/webhook/webhook_patch.yaml @@ -16,5 +16,12 @@ webhooks: timeoutSeconds: 5 namespaceSelector: matchExpressions: + # using the control-plane label to bypass Gatekeeper is deprecated + # and will be removed from the default config in a future version - key: control-plane operator: DoesNotExist + - key: admission.gatekeeper.sh/ignore + operator: DoesNotExist + - name: check-ignore-label.gatekeeper.sh + sideEffects: None + timeoutSeconds: 5 diff --git a/manifest_staging/chart/gatekeeper-operator/helm-modifications/helm-modifications.yaml b/manifest_staging/chart/gatekeeper-operator/helm-modifications/helm-modifications.yaml index d5ff491de40..2f4a9a0e1ec 100644 --- a/manifest_staging/chart/gatekeeper-operator/helm-modifications/helm-modifications.yaml +++ b/manifest_staging/chart/gatekeeper-operator/helm-modifications/helm-modifications.yaml @@ -54,6 +54,7 @@ spec: - --logtostderr - --constraint-violations-limit={{ .Values.constraintViolationsLimit }} - --audit-from-cache={{ .Values.auditFromCache }} + - --exempt-namespace=gatekeeper-system imagePullPolicy: "{{ .Values.image.pullPolicy }}" image: "{{ .Values.image.repository }}:{{ .Values.image.release }}" resources: HELMSUBST_DEPLOYMENT_CONTAINER_RESOURCES diff --git a/manifest_staging/chart/gatekeeper-operator/templates/gatekeeper.yaml b/manifest_staging/chart/gatekeeper-operator/templates/gatekeeper.yaml index e104cdd6df0..cacec5bc0fb 100644 --- a/manifest_staging/chart/gatekeeper-operator/templates/gatekeeper.yaml +++ b/manifest_staging/chart/gatekeeper-operator/templates/gatekeeper.yaml @@ -2,9 +2,11 @@ apiVersion: v1 kind: Namespace metadata: labels: + admission.gatekeeper.sh/ignore: no-self-managing app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' control-plane: controller-manager + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-system @@ -20,6 +22,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: configs.config.gatekeeper.sh @@ -220,6 +223,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-admin @@ -232,6 +236,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-manager-role @@ -257,6 +262,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-manager-role @@ -352,6 +358,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-manager-rolebinding @@ -371,6 +378,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-manager-rolebinding @@ -389,6 +397,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-webhook-server-cert @@ -400,6 +409,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-webhook-service @@ -412,6 +422,7 @@ spec: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' control-plane: controller-manager + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' --- @@ -422,6 +433,7 @@ metadata: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' control-plane: controller-manager + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-controller-manager @@ -433,6 +445,7 @@ spec: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' control-plane: controller-manager + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' template: @@ -441,6 +454,7 @@ spec: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' control-plane: controller-manager + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' spec: @@ -451,6 +465,7 @@ spec: - --logtostderr - --constraint-violations-limit={{ .Values.constraintViolationsLimit }} - --audit-from-cache={{ .Values.auditFromCache }} + - --exempt-namespace=gatekeeper-system command: - /manager env: @@ -516,6 +531,7 @@ metadata: labels: app: '{{ template "gatekeeper-operator.name" . }}' chart: '{{ template "gatekeeper-operator.name" . }}' + gatekeeper.sh/system: "yes" heritage: '{{ .Release.Service }}' release: '{{ .Release.Name }}' name: gatekeeper-validating-webhook-configuration @@ -532,6 +548,8 @@ webhooks: matchExpressions: - key: control-plane operator: DoesNotExist + - key: admission.gatekeeper.sh/ignore + operator: DoesNotExist rules: - apiGroups: - '*' @@ -544,3 +562,23 @@ webhooks: - '*' sideEffects: None timeoutSeconds: 5 +- clientConfig: + caBundle: Cg== + service: + name: gatekeeper-webhook-service + namespace: gatekeeper-system + path: /v1/admitlabel + failurePolicy: Fail + name: check-ignore-label.gatekeeper.sh + rules: + - apiGroups: + - "" + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + resources: + - namespaces + sideEffects: None + timeoutSeconds: 5 diff --git a/manifest_staging/deploy/gatekeeper.yaml b/manifest_staging/deploy/gatekeeper.yaml index 2273160bca0..8f9d5f8892b 100644 --- a/manifest_staging/deploy/gatekeeper.yaml +++ b/manifest_staging/deploy/gatekeeper.yaml @@ -2,7 +2,9 @@ apiVersion: v1 kind: Namespace metadata: labels: + admission.gatekeeper.sh/ignore: no-self-managing control-plane: controller-manager + gatekeeper.sh/system: "yes" name: gatekeeper-system --- apiVersion: apiextensions.k8s.io/v1beta1 @@ -11,6 +13,8 @@ metadata: annotations: controller-gen.kubebuilder.io/version: v0.2.4 creationTimestamp: null + labels: + gatekeeper.sh/system: "yes" name: configs.config.gatekeeper.sh spec: group: config.gatekeeper.sh @@ -104,6 +108,8 @@ status: apiVersion: v1 kind: ServiceAccount metadata: + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-admin namespace: gatekeeper-system --- @@ -111,6 +117,8 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: creationTimestamp: null + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-manager-role namespace: gatekeeper-system rules: @@ -131,6 +139,8 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: creationTimestamp: null + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-manager-role rules: - apiGroups: @@ -221,6 +231,8 @@ rules: apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-manager-rolebinding namespace: gatekeeper-system roleRef: @@ -235,6 +247,8 @@ subjects: apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-manager-rolebinding roleRef: apiGroup: rbac.authorization.k8s.io @@ -248,12 +262,16 @@ subjects: apiVersion: v1 kind: Secret metadata: + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-webhook-server-cert namespace: gatekeeper-system --- apiVersion: v1 kind: Service metadata: + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-webhook-service namespace: gatekeeper-system spec: @@ -262,12 +280,14 @@ spec: targetPort: 8443 selector: control-plane: controller-manager + gatekeeper.sh/system: "yes" --- apiVersion: apps/v1 kind: Deployment metadata: labels: control-plane: controller-manager + gatekeeper.sh/system: "yes" name: gatekeeper-controller-manager namespace: gatekeeper-system spec: @@ -275,16 +295,19 @@ spec: selector: matchLabels: control-plane: controller-manager + gatekeeper.sh/system: "yes" template: metadata: labels: control-plane: controller-manager + gatekeeper.sh/system: "yes" spec: containers: - args: - --audit-interval=30 - --port=8443 - --logtostderr + - --exempt-namespace=gatekeeper-system command: - /manager env: @@ -346,6 +369,8 @@ apiVersion: admissionregistration.k8s.io/v1beta1 kind: ValidatingWebhookConfiguration metadata: creationTimestamp: null + labels: + gatekeeper.sh/system: "yes" name: gatekeeper-validating-webhook-configuration webhooks: - clientConfig: @@ -360,6 +385,8 @@ webhooks: matchExpressions: - key: control-plane operator: DoesNotExist + - key: admission.gatekeeper.sh/ignore + operator: DoesNotExist rules: - apiGroups: - '*' @@ -372,6 +399,26 @@ webhooks: - '*' sideEffects: None timeoutSeconds: 5 +- clientConfig: + caBundle: Cg== + service: + name: gatekeeper-webhook-service + namespace: gatekeeper-system + path: /v1/admitlabel + failurePolicy: Fail + name: check-ignore-label.gatekeeper.sh + rules: + - apiGroups: + - "" + apiVersions: + - '*' + operations: + - CREATE + - UPDATE + resources: + - namespaces + sideEffects: None + timeoutSeconds: 5 --- apiVersion: apiextensions.k8s.io/v1beta1 kind: CustomResourceDefinition diff --git a/pkg/webhook/namespacelabel.go b/pkg/webhook/namespacelabel.go new file mode 100644 index 00000000000..e4f3e06923f --- /dev/null +++ b/pkg/webhook/namespacelabel.go @@ -0,0 +1,85 @@ +package webhook + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "net/http" + + opa "github.com/open-policy-agent/frameworks/constraint/pkg/client" + "github.com/pkg/errors" + types "k8s.io/api/admission/v1beta1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "sigs.k8s.io/controller-runtime/pkg/manager" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +var ( + exemptNamespace = newNSSet() +) + +func init() { + AddToManagerFuncs = append(AddToManagerFuncs, AddLabelWebhook) + flag.Var(exemptNamespace, "exempt-namespace", "The specified namespace is allowed to set the admission.gatekeeper.sh/ignore label. To exempt multiple namespaces, this flag can be declared more than once.") +} + +const ignoreLabel = "admission.gatekeeper.sh/ignore" + +type nsSet map[string]bool + +var _ flag.Value = nsSet{} + +func newNSSet() nsSet { + return make(map[string]bool) +} + +func (l nsSet) String() string { + contents := make([]string, 0) + for k := range l { + contents = append(contents, k) + } + return fmt.Sprintf("%s", contents) +} + +func (l nsSet) Set(s string) error { + l[s] = true + return nil +} + +// +kubebuilder:webhook:verbs=CREATE;UPDATE,path=/v1/admitlabel,mutating=false,failurePolicy=fail,groups="",resources=namespaces,versions=*,name=check-ignore-label.gatekeeper.sh + +// AddLabelWebhook registers the label webhook server with the manager +func AddLabelWebhook(mgr manager.Manager, _ *opa.Client) error { + wh := &admission.Webhook{Handler: &namespaceLabelHandler{}} + mgr.GetWebhookServer().Register("/v1/admitlabel", wh) + return nil +} + +var _ admission.Handler = &namespaceLabelHandler{} + +type namespaceLabelHandler struct{} + +func (h *namespaceLabelHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + if req.Operation == types.Delete { + return admission.Allowed("Delete is always allowed") + } + if req.AdmissionRequest.Kind.Group != "" || req.AdmissionRequest.Kind.Kind != "Namespace" { + return admission.Allowed("Not a namespace") + } + obj := &unstructured.Unstructured{} + if err := json.Unmarshal(req.Object.Raw, obj); err != nil { + r := admission.Denied(errors.Wrap(err, "while deserializing resource").Error()) + r.Result.Code = http.StatusInternalServerError + return r + } + if exemptNamespace[obj.GetName()] { + return admission.Allowed(fmt.Sprintf("Namespace %s is allowed to set %s", obj.GetName(), ignoreLabel)) + } + for label := range obj.GetLabels() { + if label == ignoreLabel { + return admission.Denied(fmt.Sprintf("Only exempt namespace can have the %s label", ignoreLabel)) + } + } + return admission.Allowed(fmt.Sprintf("Namespace is not setting the %s label", ignoreLabel)) +} diff --git a/pkg/webhook/namespacelabel_test.go b/pkg/webhook/namespacelabel_test.go new file mode 100644 index 00000000000..f2c3eb7f4da --- /dev/null +++ b/pkg/webhook/namespacelabel_test.go @@ -0,0 +1,178 @@ +package webhook + +import ( + "context" + "encoding/json" + "testing" + + admissionv1beta1 "k8s.io/api/admission/v1beta1" + types "k8s.io/api/admission/v1beta1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "sigs.k8s.io/controller-runtime/pkg/webhook/admission" +) + +func gvk(group, version, kind string) metav1.GroupVersionKind { + return metav1.GroupVersionKind{Group: group, Version: version, Kind: kind} +} + +func TestAdmission(t *testing.T) { + tests := []struct { + name string + kind metav1.GroupVersionKind + obj runtime.Object + op types.Operation + expectAllowed bool + }{ + { + name: "Wrong group", + kind: gvk("random", "v1", "Namespace"), + obj: &unstructured.Unstructured{}, + op: types.Create, + expectAllowed: true, + }, + { + name: "Wrong kind", + kind: gvk("", "v1", "Arbitrary"), + obj: &unstructured.Unstructured{}, + op: types.Create, + expectAllowed: true, + }, + { + name: "Bad Namespace create rejected", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-namespace", + Labels: map[string]string{ignoreLabel: "true"}, + }, + }, + op: types.Create, + expectAllowed: false, + }, + { + name: "Bad Namespace update rejected", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-namespace", + Labels: map[string]string{ignoreLabel: "true"}, + }, + }, + op: types.Update, + expectAllowed: false, + }, + { + name: "Bad Namespace delete allowed", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-namespace", + Labels: map[string]string{ignoreLabel: "true"}, + }, + }, + op: types.Delete, + expectAllowed: true, + }, + { + name: "Bad Namespace no label allowed", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-namespace", + }, + }, + op: types.Create, + expectAllowed: true, + }, + { + name: "Bad Namespace irrelevant label allowed", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-namespace", + Labels: map[string]string{"some-label": "true"}, + }, + }, + op: types.Update, + expectAllowed: true, + }, + { + name: "Exempt Namespace create allowed", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-allowed-ns", + Labels: map[string]string{ignoreLabel: "true"}, + }, + }, + op: types.Create, + expectAllowed: true, + }, + { + name: "Exempt Namespace update allowed", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-allowed-ns", + Labels: map[string]string{ignoreLabel: "true"}, + }, + }, + op: types.Update, + expectAllowed: true, + }, + { + name: "Exempt Namespace delete allowed", + kind: gvk("", "v1", "Namespace"), + obj: &corev1.Namespace{ + ObjectMeta: metav1.ObjectMeta{ + Name: "random-allowed-ns", + Labels: map[string]string{ignoreLabel: "true"}, + }, + }, + op: types.Delete, + expectAllowed: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + exemptNamespace = map[string]bool{"random-allowed-ns": true} + gvk := tt.obj.GetObjectKind() + gvk.SetGroupVersionKind(schema.GroupVersionKind{Group: tt.kind.Group, Version: tt.kind.Version, Kind: tt.kind.Kind}) + bytes, err := json.Marshal(tt.obj) + if err != nil { + t.Fatal(err) + } + req := admission.Request{ + AdmissionRequest: admissionv1beta1.AdmissionRequest{ + Kind: tt.kind, + Object: runtime.RawExtension{Raw: bytes}, + Operation: tt.op, + }, + } + handler := &namespaceLabelHandler{} + resp := handler.Handle(context.Background(), req) + if resp.Allowed != tt.expectAllowed { + t.Errorf("resp.Allowed = %v, expected %v. Reason: %s", resp.Allowed, tt.expectAllowed, resp.Result.Reason) + } + }) + } +} + +func TestBadSerialization(t *testing.T) { + req := admission.Request{ + AdmissionRequest: admissionv1beta1.AdmissionRequest{ + Kind: gvk("", "v1", "Namespace"), + Object: runtime.RawExtension{Raw: []byte("asdfadsfa awdf+-=-=pasdf")}, + Operation: types.Create, + }, + } + handler := &namespaceLabelHandler{} + resp := handler.Handle(context.Background(), req) + if resp.Allowed { + t.Errorf("resp.Allowed = %v, expected false. Reason: %s", resp.Allowed, resp.Result.Reason) + } +} diff --git a/test/bats/helpers.bash b/test/bats/helpers.bash index 21a6785c083..40b9a17bc4e 100644 --- a/test/bats/helpers.bash +++ b/test/bats/helpers.bash @@ -64,3 +64,11 @@ wait_for_process(){ done return 1 } + +get_ca_cert() { + destination="$1" + if [ $(kubectl get secret -n gatekeeper-system gatekeeper-webhook-server-cert -o jsonpath='{.data.ca\.crt}' | wc -w) -eq 0 ]; then + return 1 + fi + kubectl get secret -n gatekeeper-system gatekeeper-webhook-server-cert -o jsonpath='{.data.ca\.crt}' | base64 -d > $destination +} diff --git a/test/bats/test.bats b/test/bats/test.bats index 0646cf837c8..2d202439494 100644 --- a/test/bats/test.bats +++ b/test/bats/test.bats @@ -5,12 +5,30 @@ load helpers BATS_TESTS_DIR=test/bats/tests WAIT_TIME=120 SLEEP_TIME=1 +CLEAN_CMD="echo cleaning..." + +teardown() { + bash -c "${CLEAN_CMD}" +} @test "gatekeeper-controller-manager is running" { run wait_for_process $WAIT_TIME $SLEEP_TIME "kubectl -n gatekeeper-system wait --for=condition=Ready --timeout=60s pod -l control-plane=controller-manager" assert_success } +@test "namespace label webhook is serving" { + cert=$(mktemp) + CLEAN_CMD="${CLEAN_CMD}; rm ${CERT}" + wait_for_process $WAIT_TIME $SLEEP_TIME "get_ca_cert ${cert}" + + kubectl port-forward -n gatekeeper-system deployment/gatekeeper-controller-manager 8443:8443 & + FORWARDING_PID=$! + CLEAN_CMD="${CLEAN_CMD}; kill ${FORWARDING_PID}" + + run wait_for_process $WAIT_TIME $SLEEP_TIME "curl -f -v --resolve gatekeeper-webhook-service.gatekeeper-system.svc:8443:127.0.0.1 --cacert ${cert} https://gatekeeper-webhook-service.gatekeeper-system.svc:8443/v1/admitlabel" + assert_success +} + @test "constrainttemplates crd is established" { wait_for_process $WAIT_TIME $SLEEP_TIME "kubectl -n gatekeeper-system wait --for condition=established --timeout=60s crd/constrainttemplates.templates.gatekeeper.sh" @@ -36,6 +54,16 @@ SLEEP_TIME=1 assert_success } +@test "no ignore label unless namespace is exempt test" { + run kubectl apply -f ${BATS_TESTS_DIR}/good/ignore_label_ns.yaml + assert_failure +} + +@test "gatekeeper-system ignore label can be patched" { + run kubectl patch ns gatekeeper-system --type=json -p='[{"op": "replace", "path": "/metadata/labels/admission.gatekeeper.sh~1ignore", "value": "ignore-label-test-passed"}]' + assert_success +} + @test "required labels test" { run kubectl apply -f ${BATS_TESTS_DIR}/templates/k8srequiredlabels_template.yaml assert_success diff --git a/test/bats/tests/bad/ignore_label_ns.yaml b/test/bats/tests/bad/ignore_label_ns.yaml new file mode 100644 index 00000000000..5a3f81b933d --- /dev/null +++ b/test/bats/tests/bad/ignore_label_ns.yaml @@ -0,0 +1,6 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: bad-ns + labels: + admission.gatekeeper.sh/ignore: yes