diff --git a/Makefile b/Makefile index ec998391..c97f4938 100644 --- a/Makefile +++ b/Makefile @@ -219,6 +219,10 @@ CERT_MANAGER_VERSION ?= 1.12.1 manifests: controller-gen kustomize authorino-manifests ## Generate WebhookConfiguration, ClusterRole and CustomResourceDefinition objects. $(CONTROLLER_GEN) crd rbac:roleName=authorino-operator-manager webhook paths="./..." output:crd:artifacts:config=config/crd/bases && $(KUSTOMIZE) build config/install > $(OPERATOR_MANIFESTS) + # ValidatingAdmissionPolicy/Binding are cluster-scoped. Build them on their own (no namespace + # overlay) and append, so this kustomize version does not stamp a namespace on them. + printf '%s\n' '---' >> $(OPERATOR_MANIFESTS) + $(KUSTOMIZE) build config/vap >> $(OPERATOR_MANIFESTS) $(MAKE) deploy-manifest OPERATOR_IMAGE=$(OPERATOR_IMAGE) .PHONY: authorino-manifests @@ -314,6 +318,9 @@ deploy-manifest: kustomize mkdir -p $(DEPLOYMENT_DIR) cd $(PROJECT_DIR)/config/manager && $(KUSTOMIZE) edit set image controller=$(OPERATOR_IMAGE) ;\ cd $(PROJECT_DIR) && $(KUSTOMIZE) build config/deploy > $(DEPLOYMENT_FILE) + # ValidatingAdmissionPolicy/Binding are cluster-scoped; build separately (no namespace) and append. + printf '%s\n' '---' >> $(DEPLOYMENT_FILE) + $(KUSTOMIZE) build config/vap >> $(DEPLOYMENT_FILE) # clean up cd $(PROJECT_DIR)/config/manager && $(KUSTOMIZE) edit set image controller=${DEFAULT_OPERATOR_IMAGE} diff --git a/charts/authorino-operator/templates/manifests.yaml b/charts/authorino-operator/templates/manifests.yaml index 4a564ead..594925fd 100644 --- a/charts/authorino-operator/templates/manifests.yaml +++ b/charts/authorino-operator/templates/manifests.yaml @@ -3858,3 +3858,116 @@ spec: runAsNonRoot: true serviceAccountName: authorino-operator terminationGracePeriodSeconds: 10 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-cluster-wide +rules: +- apiGroups: + - operator.authorino.kuadrant.io + resources: + - authorinos + verbs: + - set-cluster-wide +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-privileged-fields +rules: +- apiGroups: + - authorino.kuadrant.io + resources: + - authconfigs + verbs: + - set-privileged-fields +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authconfig-restrict-all-namespaces +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - authorino.kuadrant.io + apiVersions: + - v1beta3 + operations: + - CREATE + - UPDATE + resources: + - authconfigs + validations: + - expression: '!variables.wantsAllNamespaces || variables.wasAllNamespaces || variables.isExempt' + message: 'allNamespaces: true (cluster-wide secret lookup) can only be set by + a subject granted the ''set-privileged-fields'' permission on authconfigs' + reason: Forbidden + variables: + - expression: authorizer.requestResource.check('set-privileged-fields').allowed() + name: isExempt + - expression: has(object.spec.authentication) && object.spec.authentication.exists(k, + (has(object.spec.authentication[k].apiKey) && has(object.spec.authentication[k].apiKey.allNamespaces) + && object.spec.authentication[k].apiKey.allNamespaces) || (has(object.spec.authentication[k].x509) + && has(object.spec.authentication[k].x509.allNamespaces) && object.spec.authentication[k].x509.allNamespaces)) + name: wantsAllNamespaces + - expression: request.operation == 'UPDATE' && has(oldObject.spec.authentication) + && oldObject.spec.authentication.exists(k, (has(oldObject.spec.authentication[k].apiKey) + && has(oldObject.spec.authentication[k].apiKey.allNamespaces) && oldObject.spec.authentication[k].apiKey.allNamespaces) + || (has(oldObject.spec.authentication[k].x509) && has(oldObject.spec.authentication[k].x509.allNamespaces) + && oldObject.spec.authentication[k].x509.allNamespaces)) + name: wasAllNamespaces +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authorino-restrict-spec-fields +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - operator.authorino.kuadrant.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - authorinos + validations: + - expression: '!has(object.spec.image) || object.spec.image == ''''' + message: spec.image is not allowed; the Authorino image is controlled by the operator + reason: Invalid + - expression: '!variables.wantsClusterWide || variables.wasClusterWide || variables.isExempt' + message: 'spec.clusterWide: true can only be set by a subject granted the ''set-cluster-wide'' + permission on authorinos' + reason: Invalid + variables: + - expression: authorizer.requestResource.check('set-cluster-wide').allowed() + name: isExempt + - expression: has(object.spec.clusterWide) && object.spec.clusterWide + name: wantsClusterWide + - expression: request.operation == 'UPDATE' && has(oldObject.spec.clusterWide) && + oldObject.spec.clusterWide + name: wasClusterWide +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authconfig-restrict-all-namespaces-binding +spec: + policyName: authconfig-restrict-all-namespaces + validationActions: + - Deny +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authorino-restrict-spec-fields-binding +spec: + policyName: authorino-restrict-spec-fields + validationActions: + - Deny diff --git a/config/deploy/manifests.yaml b/config/deploy/manifests.yaml index 612bf1a4..158b45f2 100644 --- a/config/deploy/manifests.yaml +++ b/config/deploy/manifests.yaml @@ -3865,3 +3865,116 @@ spec: runAsNonRoot: true serviceAccountName: authorino-operator terminationGracePeriodSeconds: 10 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-cluster-wide +rules: +- apiGroups: + - operator.authorino.kuadrant.io + resources: + - authorinos + verbs: + - set-cluster-wide +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-privileged-fields +rules: +- apiGroups: + - authorino.kuadrant.io + resources: + - authconfigs + verbs: + - set-privileged-fields +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authconfig-restrict-all-namespaces +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - authorino.kuadrant.io + apiVersions: + - v1beta3 + operations: + - CREATE + - UPDATE + resources: + - authconfigs + validations: + - expression: '!variables.wantsAllNamespaces || variables.wasAllNamespaces || variables.isExempt' + message: 'allNamespaces: true (cluster-wide secret lookup) can only be set by + a subject granted the ''set-privileged-fields'' permission on authconfigs' + reason: Forbidden + variables: + - expression: authorizer.requestResource.check('set-privileged-fields').allowed() + name: isExempt + - expression: has(object.spec.authentication) && object.spec.authentication.exists(k, + (has(object.spec.authentication[k].apiKey) && has(object.spec.authentication[k].apiKey.allNamespaces) + && object.spec.authentication[k].apiKey.allNamespaces) || (has(object.spec.authentication[k].x509) + && has(object.spec.authentication[k].x509.allNamespaces) && object.spec.authentication[k].x509.allNamespaces)) + name: wantsAllNamespaces + - expression: request.operation == 'UPDATE' && has(oldObject.spec.authentication) + && oldObject.spec.authentication.exists(k, (has(oldObject.spec.authentication[k].apiKey) + && has(oldObject.spec.authentication[k].apiKey.allNamespaces) && oldObject.spec.authentication[k].apiKey.allNamespaces) + || (has(oldObject.spec.authentication[k].x509) && has(oldObject.spec.authentication[k].x509.allNamespaces) + && oldObject.spec.authentication[k].x509.allNamespaces)) + name: wasAllNamespaces +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authorino-restrict-spec-fields +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - operator.authorino.kuadrant.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - authorinos + validations: + - expression: '!has(object.spec.image) || object.spec.image == ''''' + message: spec.image is not allowed; the Authorino image is controlled by the operator + reason: Invalid + - expression: '!variables.wantsClusterWide || variables.wasClusterWide || variables.isExempt' + message: 'spec.clusterWide: true can only be set by a subject granted the ''set-cluster-wide'' + permission on authorinos' + reason: Invalid + variables: + - expression: authorizer.requestResource.check('set-cluster-wide').allowed() + name: isExempt + - expression: has(object.spec.clusterWide) && object.spec.clusterWide + name: wantsClusterWide + - expression: request.operation == 'UPDATE' && has(oldObject.spec.clusterWide) && + oldObject.spec.clusterWide + name: wasClusterWide +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authconfig-restrict-all-namespaces-binding +spec: + policyName: authconfig-restrict-all-namespaces + validationActions: + - Deny +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authorino-restrict-spec-fields-binding +spec: + policyName: authorino-restrict-spec-fields + validationActions: + - Deny diff --git a/config/install/manifests.yaml b/config/install/manifests.yaml index 03039a9d..6c82c757 100644 --- a/config/install/manifests.yaml +++ b/config/install/manifests.yaml @@ -544,3 +544,116 @@ subjects: - kind: ServiceAccount name: authorino-operator namespace: authorino-operator +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-cluster-wide +rules: +- apiGroups: + - operator.authorino.kuadrant.io + resources: + - authorinos + verbs: + - set-cluster-wide +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-privileged-fields +rules: +- apiGroups: + - authorino.kuadrant.io + resources: + - authconfigs + verbs: + - set-privileged-fields +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authconfig-restrict-all-namespaces +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - authorino.kuadrant.io + apiVersions: + - v1beta3 + operations: + - CREATE + - UPDATE + resources: + - authconfigs + validations: + - expression: '!variables.wantsAllNamespaces || variables.wasAllNamespaces || variables.isExempt' + message: 'allNamespaces: true (cluster-wide secret lookup) can only be set by + a subject granted the ''set-privileged-fields'' permission on authconfigs' + reason: Forbidden + variables: + - expression: authorizer.requestResource.check('set-privileged-fields').allowed() + name: isExempt + - expression: has(object.spec.authentication) && object.spec.authentication.exists(k, + (has(object.spec.authentication[k].apiKey) && has(object.spec.authentication[k].apiKey.allNamespaces) + && object.spec.authentication[k].apiKey.allNamespaces) || (has(object.spec.authentication[k].x509) + && has(object.spec.authentication[k].x509.allNamespaces) && object.spec.authentication[k].x509.allNamespaces)) + name: wantsAllNamespaces + - expression: request.operation == 'UPDATE' && has(oldObject.spec.authentication) + && oldObject.spec.authentication.exists(k, (has(oldObject.spec.authentication[k].apiKey) + && has(oldObject.spec.authentication[k].apiKey.allNamespaces) && oldObject.spec.authentication[k].apiKey.allNamespaces) + || (has(oldObject.spec.authentication[k].x509) && has(oldObject.spec.authentication[k].x509.allNamespaces) + && oldObject.spec.authentication[k].x509.allNamespaces)) + name: wasAllNamespaces +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authorino-restrict-spec-fields +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: + - operator.authorino.kuadrant.io + apiVersions: + - v1beta1 + operations: + - CREATE + - UPDATE + resources: + - authorinos + validations: + - expression: '!has(object.spec.image) || object.spec.image == ''''' + message: spec.image is not allowed; the Authorino image is controlled by the operator + reason: Invalid + - expression: '!variables.wantsClusterWide || variables.wasClusterWide || variables.isExempt' + message: 'spec.clusterWide: true can only be set by a subject granted the ''set-cluster-wide'' + permission on authorinos' + reason: Invalid + variables: + - expression: authorizer.requestResource.check('set-cluster-wide').allowed() + name: isExempt + - expression: has(object.spec.clusterWide) && object.spec.clusterWide + name: wantsClusterWide + - expression: request.operation == 'UPDATE' && has(oldObject.spec.clusterWide) && + oldObject.spec.clusterWide + name: wasClusterWide +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authconfig-restrict-all-namespaces-binding +spec: + policyName: authconfig-restrict-all-namespaces + validationActions: + - Deny +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authorino-restrict-spec-fields-binding +spec: + policyName: authorino-restrict-spec-fields + validationActions: + - Deny diff --git a/config/vap/kustomization.yaml b/config/vap/kustomization.yaml new file mode 100644 index 00000000..5a110bbc --- /dev/null +++ b/config/vap/kustomization.yaml @@ -0,0 +1,7 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - restrict-spec-fields.yaml + - restrict-all-namespaces.yaml + - set-privileged-fields-role.yaml + - set-cluster-wide-role.yaml diff --git a/config/vap/restrict-all-namespaces.yaml b/config/vap/restrict-all-namespaces.yaml new file mode 100644 index 00000000..1e4b153e --- /dev/null +++ b/config/vap/restrict-all-namespaces.yaml @@ -0,0 +1,31 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authconfig-restrict-all-namespaces +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["authorino.kuadrant.io"] + apiVersions: ["v1beta3"] + operations: ["CREATE", "UPDATE"] + resources: ["authconfigs"] + variables: + - name: isExempt + expression: "authorizer.requestResource.check('set-privileged-fields').allowed()" + - name: wantsAllNamespaces + expression: "has(object.spec.authentication) && object.spec.authentication.exists(k, (has(object.spec.authentication[k].apiKey) && has(object.spec.authentication[k].apiKey.allNamespaces) && object.spec.authentication[k].apiKey.allNamespaces) || (has(object.spec.authentication[k].x509) && has(object.spec.authentication[k].x509.allNamespaces) && object.spec.authentication[k].x509.allNamespaces))" + - name: wasAllNamespaces + expression: "request.operation == 'UPDATE' && has(oldObject.spec.authentication) && oldObject.spec.authentication.exists(k, (has(oldObject.spec.authentication[k].apiKey) && has(oldObject.spec.authentication[k].apiKey.allNamespaces) && oldObject.spec.authentication[k].apiKey.allNamespaces) || (has(oldObject.spec.authentication[k].x509) && has(oldObject.spec.authentication[k].x509.allNamespaces) && oldObject.spec.authentication[k].x509.allNamespaces))" + validations: + - expression: "!variables.wantsAllNamespaces || variables.wasAllNamespaces || variables.isExempt" + message: "allNamespaces: true (cluster-wide secret lookup) can only be set by a subject granted the 'set-privileged-fields' permission on authconfigs" + reason: Forbidden +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authconfig-restrict-all-namespaces-binding +spec: + policyName: authconfig-restrict-all-namespaces + validationActions: ["Deny"] diff --git a/config/vap/restrict-spec-fields.yaml b/config/vap/restrict-spec-fields.yaml new file mode 100644 index 00000000..8f908022 --- /dev/null +++ b/config/vap/restrict-spec-fields.yaml @@ -0,0 +1,34 @@ +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicy +metadata: + name: authorino-restrict-spec-fields +spec: + failurePolicy: Fail + matchConstraints: + resourceRules: + - apiGroups: ["operator.authorino.kuadrant.io"] + apiVersions: ["v1beta1"] + operations: ["CREATE", "UPDATE"] + resources: ["authorinos"] + variables: + - name: isExempt + expression: "authorizer.requestResource.check('set-cluster-wide').allowed()" + - name: wantsClusterWide + expression: "has(object.spec.clusterWide) && object.spec.clusterWide" + - name: wasClusterWide + expression: "request.operation == 'UPDATE' && has(oldObject.spec.clusterWide) && oldObject.spec.clusterWide" + validations: + - expression: "!has(object.spec.image) || object.spec.image == ''" + message: "spec.image is not allowed; the Authorino image is controlled by the operator" + reason: Invalid + - expression: "!variables.wantsClusterWide || variables.wasClusterWide || variables.isExempt" + message: "spec.clusterWide: true can only be set by a subject granted the 'set-cluster-wide' permission on authorinos" + reason: Invalid +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingAdmissionPolicyBinding +metadata: + name: authorino-restrict-spec-fields-binding +spec: + policyName: authorino-restrict-spec-fields + validationActions: ["Deny"] diff --git a/config/vap/set-cluster-wide-role.yaml b/config/vap/set-cluster-wide-role.yaml new file mode 100644 index 00000000..f7b7a915 --- /dev/null +++ b/config/vap/set-cluster-wide-role.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-cluster-wide +rules: + - apiGroups: ["operator.authorino.kuadrant.io"] + resources: ["authorinos"] + verbs: ["set-cluster-wide"] diff --git a/config/vap/set-privileged-fields-role.yaml b/config/vap/set-privileged-fields-role.yaml new file mode 100644 index 00000000..4e9afff6 --- /dev/null +++ b/config/vap/set-privileged-fields-role.yaml @@ -0,0 +1,8 @@ +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: authorino-set-privileged-fields +rules: + - apiGroups: ["authorino.kuadrant.io"] + resources: ["authconfigs"] + verbs: ["set-privileged-fields"] diff --git a/make/helm.mk b/make/helm.mk index ea145c9e..e205d0b5 100644 --- a/make/helm.mk +++ b/make/helm.mk @@ -15,6 +15,9 @@ helm-build: yq kustomize manifests ## Build the helm chart from kustomize manife cd config/manager && $(KUSTOMIZE) edit set image controller=$(OPERATOR_IMAGE) # Build the helm chart templates from kustomize manifests $(KUSTOMIZE) build config/helm > $(CHART_DIRECTORY)/templates/manifests.yaml + # ValidatingAdmissionPolicy/Binding are cluster-scoped; build separately (no namespace) and append. + printf '%s\n' '---' >> $(CHART_DIRECTORY)/templates/manifests.yaml + $(KUSTOMIZE) build config/vap >> $(CHART_DIRECTORY)/templates/manifests.yaml V="$(BUNDLE_VERSION)" $(YQ) -i e '.version = strenv(V)' $(CHART_DIRECTORY)/Chart.yaml V="$(BUNDLE_VERSION)" $(YQ) -i e '.appVersion = strenv(V)' $(CHART_DIRECTORY)/Chart.yaml # Roll back edit