From 871ffc136c0659cc0bcfbb60f30f7bc048f20347 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Thu, 18 Jan 2024 19:25:20 +0000 Subject: [PATCH 01/13] Add BackendTLS Policy support --- .github/workflows/conformance.yml | 2 + .gitleaksignore | 1 + .yamllint.yaml | 1 + cmd/gateway/commands.go | 18 +- conformance/Makefile | 7 +- conformance/README.md | 41 +- conformance/scripts/install-gateway.sh | 17 +- conformance/scripts/uninstall-gateway.sh | 22 +- deploy/helm-chart/templates/deployment.yaml | 3 + deploy/helm-chart/templates/rbac.yaml | 2 + deploy/helm-chart/values.yaml | 5 + deploy/manifests/nginx-gateway.yaml | 2 + examples/backend-tls/README.md | 97 +++++ examples/backend-tls/app-secret.yaml | 8 + .../backend-tls/backend-certs-configmap.yaml | 27 ++ examples/backend-tls/ca.cert | 21 + examples/backend-tls/gateway.yaml | 18 + examples/backend-tls/policy.yaml | 17 + examples/backend-tls/secure-app-routes.yaml | 35 ++ examples/backend-tls/secure-app.yaml | 78 ++++ internal/framework/gatewayclass/validate.go | 9 +- internal/mode/static/config/config.go | 10 +- internal/mode/static/manager.go | 25 +- internal/mode/static/manager_test.go | 27 +- .../mode/static/nginx/config/generator.go | 22 ++ .../static/nginx/config/generator_test.go | 7 +- .../mode/static/nginx/config/http/config.go | 10 +- internal/mode/static/nginx/config/servers.go | 48 ++- .../static/nginx/config/servers_template.go | 8 + .../mode/static/nginx/config/servers_test.go | 140 ++++++- .../mode/static/state/change_processor.go | 29 +- .../static/state/change_processor_test.go | 5 +- .../static/state/dataplane/configuration.go | 63 +++ .../state/dataplane/configuration_test.go | 332 ++++++++++++++++ internal/mode/static/state/dataplane/types.go | 17 + .../mode/static/state/graph/backend_refs.go | 138 ++++++- .../static/state/graph/backend_refs_test.go | 367 +++++++++++++++++- .../mode/static/state/graph/config_maps.go | 125 ++++++ .../static/state/graph/config_maps_test.go | 221 +++++++++++ internal/mode/static/state/graph/graph.go | 28 +- .../mode/static/state/graph/graph_test.go | 109 +++++- internal/mode/static/state/graph/httproute.go | 30 +- .../install-gateway-api-resources.md | 8 + .../uninstall-gateway-api-resources.md | 6 + .../installation/installing-ngf/helm.md | 16 + .../installation/installing-ngf/manifests.md | 28 ++ .../overview/gateway-api-compatibility.md | 91 +++-- site/content/reference/cli-help.md | 1 + 48 files changed, 2200 insertions(+), 142 deletions(-) create mode 100644 examples/backend-tls/README.md create mode 100644 examples/backend-tls/app-secret.yaml create mode 100644 examples/backend-tls/backend-certs-configmap.yaml create mode 100644 examples/backend-tls/ca.cert create mode 100644 examples/backend-tls/gateway.yaml create mode 100644 examples/backend-tls/policy.yaml create mode 100644 examples/backend-tls/secure-app-routes.yaml create mode 100644 examples/backend-tls/secure-app.yaml create mode 100644 internal/mode/static/state/graph/config_maps.go create mode 100644 internal/mode/static/state/graph/config_maps_test.go diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml index 7b1f5e4447..486c208841 100644 --- a/.github/workflows/conformance.yml +++ b/.github/workflows/conformance.yml @@ -30,6 +30,7 @@ jobs: matrix: k8s-version: ["1.23.17", "latest"] nginx-image: [nginx, nginx-plus] + enable-experimental: [true, false] permissions: contents: write # needed for uploading release artifacts steps: @@ -148,6 +149,7 @@ jobs: ngf_tag=${{ steps.ngf-meta.outputs.version }} if [ ${{ github.event_name }} == "schedule" ]; then export GW_API_VERSION=main; fi if [ ${{ startsWith(matrix.k8s-version, '1.23') || startsWith(matrix.k8s-version, '1.24') }} == "true" ]; then export INSTALL_WEBHOOK=true; fi + if [ ${{ matrix.enable-experimental }} == "true" ]; then export ENABLE_EXPERIMENTAL=true; fi make install-ngf-local-no-build${{ matrix.nginx-image == 'nginx-plus' && '-with-plus' || ''}} PREFIX=${ngf_prefix} TAG=${ngf_tag} working-directory: ./conformance diff --git a/.gitleaksignore b/.gitleaksignore index 9cc154248f..5b2f473b18 100644 --- a/.gitleaksignore +++ b/.gitleaksignore @@ -3,3 +3,4 @@ 68d1f6eb80d23c8650c11629459dd6a06c986ca1:internal/state/graph/graph_test.go:private-key:44 890fddb787ff3560b9b743647a36b649d498ae51:internal/state/graph/secret_test.go:private-key:35 890fddb787ff3560b9b743647a36b649d498ae51:internal/state/change_processor_test.go:private-key:211 +internal/mode/static/state/graph/config_maps_test.go:private-key:35 diff --git a/.yamllint.yaml b/.yamllint.yaml index 3a77819f42..9782deb0f9 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -32,6 +32,7 @@ rules: ignore: | deploy/manifests/nginx-gateway.yaml deploy/manifests/crds + examples/backend-tls/secure-app.yaml key-duplicates: enable key-ordering: disable line-length: diff --git a/cmd/gateway/commands.go b/cmd/gateway/commands.go index ac97185fd1..3cf5c20c81 100644 --- a/cmd/gateway/commands.go +++ b/cmd/gateway/commands.go @@ -56,6 +56,7 @@ func createStaticModeCommand() *cobra.Command { leaderElectionDisableFlag = "leader-election-disable" leaderElectionLockNameFlag = "leader-election-lock-name" plusFlag = "nginx-plus" + experimentalEnableFlag = "experimental-features-enable" ) // flag values @@ -95,6 +96,8 @@ func createStaticModeCommand() *cobra.Command { } plus bool + + enableExperimental bool ) cmd := &cobra.Command{ @@ -169,9 +172,10 @@ func createStaticModeCommand() *cobra.Command { LockName: leaderElectionLockName.String(), Identity: podName, }, - Plus: plus, - TelemetryReportPeriod: period, - Version: version, + Plus: plus, + TelemetryReportPeriod: period, + Version: version, + EnableExperimentalFeatures: enableExperimental, } if err := static.StartManager(conf); err != nil { @@ -285,6 +289,14 @@ func createStaticModeCommand() *cobra.Command { "Use NGINX Plus", ) + cmd.Flags().BoolVar( + &enableExperimental, + experimentalEnableFlag, + false, + "Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. "+ + "Requires the Gateway APIs installed from the experimental channel.", + ) + return cmd } diff --git a/conformance/Makefile b/conformance/Makefile index 795348b914..30f772ceb4 100644 --- a/conformance/Makefile +++ b/conformance/Makefile @@ -15,6 +15,7 @@ CRDS=../deploy/manifests/crds/ STATIC_MANIFEST=provisioner/static-deployment.yaml PROVISIONER_MANIFEST=provisioner/provisioner.yaml INSTALL_WEBHOOK ?= false +ENABLE_EXPERIMENTAL ?= false .DEFAULT_GOAL := help .PHONY: help @@ -37,7 +38,7 @@ create-kind-cluster: ## Create a kind cluster .PHONY: update-ngf-manifest update-ngf-manifest: ## Update the NGF deployment manifest image names and imagePullPolicies - cd .. && make generate-manifests HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE="--set nginxGateway.kind=skip" HELM_TEMPLATE_COMMON_ARGS="--set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=$(NGINX_PREFIX) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never" && cd - + cd .. && make generate-manifests HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE="--set nginxGateway.kind=skip" HELM_TEMPLATE_COMMON_ARGS="--set nginxGateway.image.repository=$(PREFIX) --set nginxGateway.image.tag=$(TAG) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=$(NGINX_PREFIX) --set nginx.image.tag=$(TAG) --set nginx.image.pullPolicy=Never --set nginxGateway.experimentalFeatures.enable=$(ENABLE_EXPERIMENTAL)" && cd - .PHONY: update-ngf-manifest-with-plus update-ngf-manifest-with-plus: ## Update the NGF deployment manifest image names and imagePullPolicies including nginx-plus @@ -61,7 +62,7 @@ load-images-with-plus: ## Load NGF and NGINX Plus images on configured kind clus .PHONY: prepare-ngf-dependencies prepare-ngf-dependencies: update-ngf-manifest ## Install NGF dependencies on configured kind cluster - ./scripts/install-gateway.sh $(GW_API_VERSION) $(INSTALL_WEBHOOK) + ./scripts/install-gateway.sh $(GW_API_VERSION) $(INSTALL_WEBHOOK) $(ENABLE_EXPERIMENTAL) kubectl apply -f $(CRDS) kubectl apply -f $(NGF_MANIFEST) @@ -118,7 +119,7 @@ uninstall-ngf: uninstall-k8s-components undo-manifests-update ## Uninstall NGF o .PHONY: uninstall-k8s-components uninstall-k8s-components: ## Uninstall installed components on configured kind cluster -kubectl delete -f $(NGF_MANIFEST) - ./scripts/uninstall-gateway.sh $(GW_API_VERSION) $(INSTALL_WEBHOOK) + ./scripts/uninstall-gateway.sh $(GW_API_VERSION) $(INSTALL_WEBHOOK) $(ENABLE_EXPERIMENTAL) kubectl delete clusterrole nginx-gateway-provisioner kubectl delete clusterrolebinding nginx-gateway-provisioner diff --git a/conformance/README.md b/conformance/README.md index aba66e6933..87c06d4383 100644 --- a/conformance/README.md +++ b/conformance/README.md @@ -44,23 +44,24 @@ update-ngf-manifest Update the NGF deployment manifest image na **Note:** The following variables are configurable when running the below `make` commands: -| Variable | Default | Description | -|----------------------|---------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------| -| CONFORMANCE_TAG | latest | The tag for the conformance test image | -| CONFORMANCE_PREFIX | conformance-test-runner | The prefix for the conformance test image | -| TAG | edge | The tag for the locally built NGF image | -| PREFIX | nginx-gateway-fabric | The prefix for the locally built NGF image | -| GW_API_VERSION | 1.0.0 | Tag for the Gateway API version to check out. Set to `main` to get the latest version | -| KIND_IMAGE | Latest kind image, as defined in the tests/Dockerfile | The kind image to use | -| KIND_KUBE_CONFIG | ~/.kube/kind/config | The location of the kubeconfig | -| GATEWAY_CLASS | nginx | The gateway class that should be used for the tests | -| SUPPORTED_FEATURES | HTTPRoute,HTTPRouteQueryParamMatching, HTTPRouteMethodMatching,HTTPRoutePortRedirect, HTTPRouteSchemeRedirect | The supported features that should be tested by the conformance tests. Ensure the list is comma separated with no spaces. | -| EXEMPT_FEATURES | ReferenceGrant | The features that should not be tested by the conformance tests | -| NGF_MANIFEST | ../deploy/manifests/nginx-gateway.yaml | The location of the NGF manifest | -| SERVICE_MANIFEST | ../deploy/manifests/service/nodeport.yaml | The location of the NGF Service manifest | -| STATIC_MANIFEST | provisioner/static-deployment.yaml | The location of the NGF static deployment manifest | -| PROVISIONER_MANIFEST | provisioner/provisioner.yaml | The location of the NGF provisioner manifest | -| INSTALL_WEBHOOK | false | Install the Gateway API Validating Webhook. Necessary for Kubernetes versions < 1.25. | +| Variable | Default | Description | +| -------------------- | ------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | +| CONFORMANCE_TAG | latest | The tag for the conformance test image | +| CONFORMANCE_PREFIX | conformance-test-runner | The prefix for the conformance test image | +| TAG | edge | The tag for the locally built NGF image | +| PREFIX | nginx-gateway-fabric | The prefix for the locally built NGF image | +| GW_API_VERSION | 1.0.0 | Tag for the Gateway API version to check out. Set to `main` to get the latest version | +| KIND_IMAGE | Latest kind image, as defined in the tests/Dockerfile | The kind image to use | +| KIND_KUBE_CONFIG | ~/.kube/kind/config | The location of the kubeconfig | +| GATEWAY_CLASS | nginx | The gateway class that should be used for the tests | +| SUPPORTED_FEATURES | HTTPRoute,HTTPRouteQueryParamMatching, HTTPRouteMethodMatching,HTTPRoutePortRedirect, HTTPRouteSchemeRedirect | The supported features that should be tested by the conformance tests. Ensure the list is comma separated with no spaces. | +| EXEMPT_FEATURES | ReferenceGrant | The features that should not be tested by the conformance tests | +| NGF_MANIFEST | ../deploy/manifests/nginx-gateway.yaml | The location of the NGF manifest | +| SERVICE_MANIFEST | ../deploy/manifests/service/nodeport.yaml | The location of the NGF Service manifest | +| STATIC_MANIFEST | provisioner/static-deployment.yaml | The location of the NGF static deployment manifest | +| PROVISIONER_MANIFEST | provisioner/provisioner.yaml | The location of the NGF provisioner manifest | +| INSTALL_WEBHOOK | false | Install the Gateway API Validating Webhook. Necessary for Kubernetes versions < 1.25. | +| ENABLE_EXPERIMENTAL | false | Enable experimental features. Installs the Gateway APIs from the experimental channel and enables any supported experimental features in NGF. | ### Step 1 - Create a kind Cluster @@ -85,6 +86,12 @@ make create-kind-cluster KIND_IMAGE=kindest/node:v1.27.3 ``` > Otherwise, the latest stable version will be used by default. +> Additionally, if you want to run conformance tests with experimental features enabled, set the following +> environment variable before deploying NGF: + +```bash + export ENABLE_EXPERIMENTAL=true +``` #### *Option 1* Build and install NGINX Gateway Fabric from local to configured kind cluster diff --git a/conformance/scripts/install-gateway.sh b/conformance/scripts/install-gateway.sh index 485e77353e..902d91b91b 100755 --- a/conformance/scripts/install-gateway.sh +++ b/conformance/scripts/install-gateway.sh @@ -10,18 +10,31 @@ if [ -z $2 ]; then exit 1 fi +if [ -z $3 ]; then + echo "enable experimental argument not set; exiting" + exit 1 +fi + if [ $1 == "main" ]; then temp_dir=$(mktemp -d) cd ${temp_dir} curl -s https://codeload.github.com/kubernetes-sigs/gateway-api/tar.gz/main | tar -xz --strip=2 gateway-api-main/config - kubectl apply -f crd/standard + if [ $3 == "true" ]; then + kubectl apply -f crd/experimental + else + kubectl apply -f crd/standard + fi if [ $2 == "true" ]; then kubectl apply -f webhook kubectl wait --for=condition=available --timeout=60s deployment gateway-api-admission-server -n gateway-system fi rm -rf ${temp_dir} else - kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/standard-install.yaml + if [ $3 == "true" ]; then + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/experimental-install.yaml + else + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/standard-install.yaml + fi if [ $2 == "true" ]; then kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/webhook-install.yaml kubectl wait --for=condition=available --timeout=60s deployment gateway-api-admission-server -n gateway-system diff --git a/conformance/scripts/uninstall-gateway.sh b/conformance/scripts/uninstall-gateway.sh index 333085661d..35d4fc2635 100755 --- a/conformance/scripts/uninstall-gateway.sh +++ b/conformance/scripts/uninstall-gateway.sh @@ -5,17 +5,35 @@ if [ -z $1 ]; then exit 1 fi +if [ -z $2 ]; then + echo "install webhook argument not set; exiting" + exit 1 +fi + +if [ -z $3 ]; then + echo "enable experimental argument not set; exiting" + exit 1 +fi + if [ $1 == "main" ]; then temp_dir=$(mktemp -d) cd ${temp_dir} curl -s https://codeload.github.com/kubernetes-sigs/gateway-api/tar.gz/main | tar -xz --strip=2 gateway-api-main/config - kubectl delete -f crd/standard + if [ $3 == "true" ]; then + kubectl delete -f crd/experimental + else + kubectl delete -f crd/standard + fi if [ $2 == "true" ]; then kubectl delete -f webhook fi rm -rf ${temp_dir} else - kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/standard-install.yaml + if [ $3 == "true" ]; then + kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/experimental-install.yaml + else + kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/standard-install.yaml + fi if [ $2 == "true" ]; then kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v$1/webhook-install.yaml fi diff --git a/deploy/helm-chart/templates/deployment.yaml b/deploy/helm-chart/templates/deployment.yaml index bb5a64c4f0..8061c6012e 100644 --- a/deploy/helm-chart/templates/deployment.yaml +++ b/deploy/helm-chart/templates/deployment.yaml @@ -52,6 +52,9 @@ spec: {{- else }} - --leader-election-disable {{- end }} + {{- if .Values.nginxGateway.experimentalFeatures.enable }} + - --experimental-features-enable + {{- end }} env: - name: POD_IP valueFrom: diff --git a/deploy/helm-chart/templates/rbac.yaml b/deploy/helm-chart/templates/rbac.yaml index cbcb66565e..85a4d02802 100644 --- a/deploy/helm-chart/templates/rbac.yaml +++ b/deploy/helm-chart/templates/rbac.yaml @@ -32,6 +32,7 @@ rules: - namespaces - services - secrets + - configmaps verbs: - list - watch @@ -76,6 +77,7 @@ rules: - gateways - httproutes - referencegrants + - backendtlspolicies verbs: - list - watch diff --git a/deploy/helm-chart/values.yaml b/deploy/helm-chart/values.yaml index 961f327603..57923d5cdb 100644 --- a/deploy/helm-chart/values.yaml +++ b/deploy/helm-chart/values.yaml @@ -51,6 +51,11 @@ nginxGateway: ## extraVolumeMounts are the additional volume mounts for the nginx-gateway container. extraVolumeMounts: [] + experimentalFeatures: + ## Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. Requires the Gateway + ## APIs installed from the experimental channel. + enable: false + nginx: ## The NGINX image to use image: diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index 578e4950b3..fdc5ed740d 100644 --- a/deploy/manifests/nginx-gateway.yaml +++ b/deploy/manifests/nginx-gateway.yaml @@ -32,6 +32,7 @@ rules: - namespaces - services - secrets + - configmaps verbs: - list - watch @@ -76,6 +77,7 @@ rules: - gateways - httproutes - referencegrants + - backendtlspolicies verbs: - list - watch diff --git a/examples/backend-tls/README.md b/examples/backend-tls/README.md new file mode 100644 index 0000000000..11e11027b2 --- /dev/null +++ b/examples/backend-tls/README.md @@ -0,0 +1,97 @@ +# Backend TLS Policy Example + +In this example, we will create a Backend TLS Policy, attach it to our + +## Running the Example + +## 1. Deploy NGINX Gateway Fabric + +1. Follow the [installation instructions](https://docs.nginx.com/nginx-gateway-fabric/installation/) to deploy NGINX Gateway Fabric. + Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the + `- --experimental-features-enable` flag. + +2. Save the public IP address of NGINX Gateway Fabric into a shell variable: + + ```text + GW_IP=XXX.YYY.ZZZ.III + ``` + +3. Save the HTTPS port of NGINX Gateway Fabric: + + ```text + GW_HTTPS_PORT= + ``` + +## 2. Deploy the secure-app Application + +1. Create the secure-app Deployment and Service: + + ```shell + kubectl apply -f secure-app.yaml + ``` + +1. Check that the Pods are running in the `default` namespace: + + ```shell + kubectl -n default get pods + ``` + + ```text + NAME READY STATUS RESTARTS AGE + secure-app-575785644-b6nwh 1/1 Running 0 5s + ``` + +## 3. Deploy the Backend TLS Policy + +1. Create the ConfigMap that holds the `ca.crt` entry for verifying our self-signed certificates: + + ```shell + kubectl apply -f backend-certs-configmap.yaml + ``` + +2. Create the Backend TLS Policy which targets our `secure-app` Service and refers to our ConfigMap created in the + previous step: + + ```shell + kubectl apply -f policy.yaml + ``` + +## 3. Configure HTTPS Termination and Routing + +1. Create the Secret with a TLS certificate and key: + + ```shell + kubectl apply -f app-secret.yaml + ``` + + The TLS certificate and key in this Secret are used to terminate the TLS connections for the secure-app application. + > **Important**: This certificate and key are for demo purposes only. + +2. Create the Gateway resource: + + ```shell + kubectl apply -f gateway.yaml + ``` + + This [Gateway](./gateway.yaml) configures: + - `http` listener for HTTP traffic + - `https` listener for HTTPS traffic. It terminates TLS connections using the `app-secret` we created in step 1. + +3. Create the HTTPRoute resources: + + ```shell + kubectl apply -f secure-app-routes.yaml + ``` + +## 4. Test the Application + +To access the application, we will use `curl` to send requests to the `secure-app` Service over HTTPS. Since our +certificate is self-signed, we will use curl's `--cacert` option to supply the certificate for verification. + +```shell +curl --resolve secure-app.example.com:$GW_HTTPS_PORT:$GW_IP https://secure-app.example.com:$GW_HTTPS_PORT/ --cacert ca.cert +``` + +```text +hello from pod secure-app-575785644-749tq +``` diff --git a/examples/backend-tls/app-secret.yaml b/examples/backend-tls/app-secret.yaml new file mode 100644 index 0000000000..ffe9a7aaf0 --- /dev/null +++ b/examples/backend-tls/app-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: app-secret +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwRENDQXJpZ0F3SUJBZ0lVVDQwYTFYd3doUHVBdDJNMkdZZUovYXluZlFBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRWZNQjBHQTFVRUF3d1djMlZqZFhKbExXRndjQzVsZUdGdGNHeGxMbU52YlRFTE1Ba0dBMVVFQmhNQwpWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1YzJselkyOHdIaGNOTWpRd01URTRNVGd3TVRBeFdoY05NalV3Ck1URTNNVGd3TVRBeFdqQi9NUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVcKTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzV6YVhOamJ6RU9NQXdHQTFVRUNnd0ZUa2RKVGxneEVqQVFCZ05WQkFzTQpDVTVIU1U1WUlFUmxkakVmTUIwR0ExVUVBd3dXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdmJUQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMeUx0eURNbTZ4M0ZEUFJsOGZ0azNweCtrRWQKYTVpTGZOQ3lDbUVjYktBQVBDNEhZckl5b1B5QXpSTlJCMWErekE0UTlrbzJZRG5vR0dkeFJaMEdydldKZUV2Mgo3MWlHNGxhbHRVTS9WOWNvSktQY0UyTEI0R3R6cFA3ckdIWXNvRDlOUXFpV3YwZ0lOdE42MjdrWGg4UW41V1hYCk92Y2FkS2h0bjJER3RvU0VzT3dpNzR5NEt3SmFkWnlwLzJaM0hPakRTNjVIVmxydmUxUXpBMVRzTEp6S3cva3gKbHBSR0lWK0lhUjZXbXZsaVFVdDJxWFg0L3hGeVVEM2Vic05TeXpHUk5mQ0NOTWxlWlV3MTR3ZUdhOEVnc2tDcQprOGdYSmpFZXQxMlR4OGxkY3BpVWlxYVpkOStYZjJmUS8yL2Y5c1IzM3Q4K0VVUWpoZ2ZIbHlsLzV1RUNBd0VBCkFhTjlNSHN3SHdZRFZSMGpCQmd3Rm9BVTRUT096c1d0Q3ZWdGJlWXFSU0FqN2tXajFkb3dDUVlEVlIwVEJBSXcKQURBTEJnTlZIUThFQkFNQ0JQQXdJUVlEVlIwUkJCb3dHSUlXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdgpiVEFkQmdOVkhRNEVGZ1FVZmtWREFFWmIwcjRTZ2swck10a0FvQ2c2RjRnd0RRWUpLb1pJaHZjTkFRRUxCUUFECmdnRUJBQWFiQit6RzVSODl6WitBT2RsRy9wWE9nYjF6VkJsQ0dMSkhyYTl1cTMvcXRPR1VacDlnd2dZSWJ4VnkKUkVLbWVRa05pV0haSDNCSlNTZ3czbE9abGNxcW5xbUJ2OFAxTUxDZ3JqbDJSN1d2NVhkb2RlQkJxc0lvZkNxVgp3ZG51THJUU3RTbmd2MGhDcldBNlBmTnlQeXMzSGJva1k3RExNREhuNmhBQWcwMUNDT0pWWGpNZjFqLzNIMFNCClBQSWxtek5aRUpEd0JMR2hyb1V3aUY3NkNUV1Fudi8yc1pvWHMwUlFiRTY3TmNraXc2Z0svaWRwVTVzMmlkOEQKVExjVjNxenVFaE1ZeUlua0ZWNEJLZlFkTWxDQnE1QWdyU1Jqb2FoaCszbFRwYVpUalJGUGFVd3VZYXVsQXRzNgpra1ROaGltWWQ3Ym1aVk5MK2I0MzhmN1RMaGc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQzhpN2Nnekp1c2R4UXoKMFpmSDdaTjZjZnBCSFd1WWkzelFzZ3BoSEd5Z0FEd3VCMkt5TXFEOGdNMFRVUWRXdnN3T0VQWktObUE1NkJobgpjVVdkQnE3MWlYaEw5dTlZaHVKV3BiVkRQMWZYS0NTajNCTml3ZUJyYzZUKzZ4aDJMS0EvVFVLb2xyOUlDRGJUCmV0dTVGNGZFSitWbDF6cjNHblNvYlo5Z3hyYUVoTERzSXUrTXVDc0NXbldjcWY5bWR4em93MHV1UjFaYTczdFUKTXdOVTdDeWN5c1A1TVphVVJpRmZpR2tlbHByNVlrRkxkcWwxK1A4UmNsQTkzbTdEVXNzeGtUWHdnalRKWG1WTQpOZU1IaG12QklMSkFxcFBJRnlZeEhyZGRrOGZKWFhLWWxJcW1tWGZmbDM5bjBQOXYzL2JFZDk3ZlBoRkVJNFlICng1Y3BmK2JoQWdNQkFBRUNnZ0VBUXJucGJleXJmVTVKTW91Ty9UenBrQkJ0UWdVZzhvUVBBS2E1d0tONEYrbnQKWWxiUHlZUGNjSEErNDRLdUo3ZHZiTno0NU11NG8xV3Q2Vkh2a29KdWdjd01iRW53YTdLVXdKaDFmVjZaL2pXaApQZkpoVS9hTUwwcm1qaWJ5YWNRaVZEVEtEZk1Ici96a05sVEpGUWlzVGpIV1lBUGJSTjh5Z1BjR3pBK1hRVzg5CmxsOFdoeWwrZndjRVAzNTM4TUdKYUpHVmV4Q2d5cVRyKzZwQ29yRUpFL1pSNytiMTNyejRsbmxpZXVYa2pCVkcKSnYvUVI0RVhTSDhuRit3K2FvWTFQaGd2QnFJTnFQZjJMT1V0MzNiN3FDTkFmSVBRdFZFejJsN3NqbmJlcElTTwpvTkhNUFY0N21XTzY2dzhFMzJVMjNjVEtUbytJcWovM0d1eGhweXlYaHdLQmdRRDVNRDhmY3ZyM0xVZHkvZ3I0Ci82MVBqUXNSaWRYdjN0Mk1MVEk4UkduNzJWcGxvVVExNDZHV2xGTGVVVDY1L1ZVMjNsZFUrUWt2eFNMK3U1bW4KRUJIdXUyVmtBUWVYcUJXWkpPTmFSZG9Ia1YzK2Fyc0U4Qld0SWVtZHJ0MWN6bHFjc1VzMWdGdG1COGI3RHB3UwpHKzRoZDlzZG0weDBNS2hoOFVGOUQrZytUd0tCZ1FEQnN4dUpoZ1hnck14S0dVMHJ5MW9WWTJtMGpDUEpJcTkzCmNZRUZGY3lYZ0U4OWlDVmlob3dVVE8yMXpTZ3o0SVVKNUxoc2M5N3VIVER1VXdwcVI5NFBsdjlyaXJvakowM1UKT3FyWHgwbWdNN2xibVM1L1RwS0czZG1QblZ0WEZMektISFgyWDVnUW56emYvdXVKK3NtbDVLQW5WN0VZc1oxcgpkVXJvRm8zcnp3S0JnUUNZdjM5aUdzeEdLaVpMRWZqTjY0UmthRFBwdTFFOTZhSnE0K1dRVmV1VnF3V2ptTGhFClJGWHdCTm5MVjRnWTRIYVUzTFF4N1RvNVl5RnhmclBRV2FSMGI4RFdEVitIRWt5ekJJNnM3bmFZL3YzY0Q3YTIKYnlrS2FPaFlkVEZTUzFmMkJ5UHdGczl2K3NKNWNOb3dxNWhNUWJrNks5RXd4QWJqaXN5M0NjSTJOd0tCZ1FDbwo2c2pZNVVlNjV2WkFxRS9rSVRJdDlNUDU3enhGNnptWnNDSVRqUzhkNzRjcTRjKzRYQjFNbHNtMkFYTk55ajQ2Cm9uc3lHTm9RVE9TZThVdmo0MGlEeitwdW5rdzAyOUhEZ21YNlJwQ3VaRzBBdEZVWU1DMFg3K0FLbmU5SndZdmgKdFhBcHFyT3h5eXdMS3dPOUVEZEp0RmIxK0VNNGhhd0NTZ2RJM21KbGdRS0JnRzIxeEJNRXRzMFBVN3lDYTZ0YwpadDc1NUV4aEdkR3F5MmtHYmtmdzBEaHBQQVVUZmdncVF3NVBYdGVIS1ZBSDlKaG5kVnBBZFFxNmZ1MER1MDNKCkl0cGpxNWluZXVoR0x0alpMR1Nhd0dwY0FUU3h4Z3dCM0l0Z29LKzBCRFhteWxId0lEcUc5Z2crRU5KK0VhL0MKeTFOMmV0ZG1sQ01hNjM4cVJlNFlTWk55Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= diff --git a/examples/backend-tls/backend-certs-configmap.yaml b/examples/backend-tls/backend-certs-configmap.yaml new file mode 100644 index 0000000000..83be724906 --- /dev/null +++ b/examples/backend-tls/backend-certs-configmap.yaml @@ -0,0 +1,27 @@ +kind: ConfigMap +apiVersion: v1 +metadata: + name: backend-certs +data: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIDbTCCAlWgAwIBAgIUPA3fFnkLl63GZ7noUjb5NoLhSYkwDQYJKoZIhvcNAQEL + BQAwRjEfMB0GA1UEAwwWc2VjdXJlLWFwcC5leGFtcGxlLmNvbTELMAkGA1UEBhMC + VVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lzY28wHhcNMjQwMTE4MTgwMTAxWhcNMjUw + MTA4MTgwMTAxWjBGMR8wHQYDVQQDDBZzZWN1cmUtYXBwLmV4YW1wbGUuY29tMQsw + CQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzCCASIwDQYJKoZIhvcN + AQEBBQADggEPADCCAQoCggEBAJGgn81BrqzmI4aQmGrg7RgkO5oYwlThQ9X/xVHB + YVFptjRPAZz9g92g5birI/NZ43C6nEbZrJrSCqN3wgvV84jJmBAgpAvW+LhF4caa + nhAnecJCcTbwrd542vCDoDRsNV5ffbpESgC4FxPGkRVbSa0KHQz8qCLqS2+uaB7X + t76iw6y4pQ3klobVp1XtUpzZMGMBqZFnsAdl+PWMmSTvqjixkSlfcUY6Crnk9W6d + Sns5cpzKdUs+2ZkBe6VkBgSs8xbaz8Y2YC1GhRqGlxYLT3WBaIlSCKPuRrGjwE3r + AsW6gSL919H1O1a+MjQuLuQ4lnCbCpNzM9OV1JISMWfwifMCAwEAAaNTMFEwHQYD + VR0OBBYEFOEzjs7FrQr1bW3mKkUgI+5Fo9XaMB8GA1UdIwQYMBaAFOEzjs7FrQr1 + bW3mKkUgI+5Fo9XaMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB + AG/eX4pctINIrHvRyHOusdac5iXSJbQRZgWvP1F2p95qoIDESciAU1Sh1oJv+As5 + IlJOZPJNuZFpDLjc8kzSoEbc1Q5+QyTBlyNNsagWYYwK0CEJ6KJt80vytffmdOIg + z8/a+2Ax829vcn1w1SUi5V6ea/l8K74f2SL/zSSHgtEiz8V0TlvT7J6wurgmnk4t + yQRmsXlDGefuijMNCVf7jWwLx2BODfKoEA1pJkthnNvdizlikmz+9elxhV9bRf3Y + NnubytWPfO1oeHjVGvxVjCouIYine+VlskvwHmMi/dYod6yd7aFYu4CU3g/hjwKo + LY2WNv5j3JhDnEYK9Zj3z7A= + -----END CERTIFICATE----- diff --git a/examples/backend-tls/ca.cert b/examples/backend-tls/ca.cert new file mode 100644 index 0000000000..403d3355ab --- /dev/null +++ b/examples/backend-tls/ca.cert @@ -0,0 +1,21 @@ +-----BEGIN CERTIFICATE----- +MIIDbTCCAlWgAwIBAgIUPA3fFnkLl63GZ7noUjb5NoLhSYkwDQYJKoZIhvcNAQEL +BQAwRjEfMB0GA1UEAwwWc2VjdXJlLWFwcC5leGFtcGxlLmNvbTELMAkGA1UEBhMC +VVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lzY28wHhcNMjQwMTE4MTgwMTAxWhcNMjUw +MTA4MTgwMTAxWjBGMR8wHQYDVQQDDBZzZWN1cmUtYXBwLmV4YW1wbGUuY29tMQsw +CQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAJGgn81BrqzmI4aQmGrg7RgkO5oYwlThQ9X/xVHB +YVFptjRPAZz9g92g5birI/NZ43C6nEbZrJrSCqN3wgvV84jJmBAgpAvW+LhF4caa +nhAnecJCcTbwrd542vCDoDRsNV5ffbpESgC4FxPGkRVbSa0KHQz8qCLqS2+uaB7X +t76iw6y4pQ3klobVp1XtUpzZMGMBqZFnsAdl+PWMmSTvqjixkSlfcUY6Crnk9W6d +Sns5cpzKdUs+2ZkBe6VkBgSs8xbaz8Y2YC1GhRqGlxYLT3WBaIlSCKPuRrGjwE3r +AsW6gSL919H1O1a+MjQuLuQ4lnCbCpNzM9OV1JISMWfwifMCAwEAAaNTMFEwHQYD +VR0OBBYEFOEzjs7FrQr1bW3mKkUgI+5Fo9XaMB8GA1UdIwQYMBaAFOEzjs7FrQr1 +bW3mKkUgI+5Fo9XaMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB +AG/eX4pctINIrHvRyHOusdac5iXSJbQRZgWvP1F2p95qoIDESciAU1Sh1oJv+As5 +IlJOZPJNuZFpDLjc8kzSoEbc1Q5+QyTBlyNNsagWYYwK0CEJ6KJt80vytffmdOIg +z8/a+2Ax829vcn1w1SUi5V6ea/l8K74f2SL/zSSHgtEiz8V0TlvT7J6wurgmnk4t +yQRmsXlDGefuijMNCVf7jWwLx2BODfKoEA1pJkthnNvdizlikmz+9elxhV9bRf3Y +NnubytWPfO1oeHjVGvxVjCouIYine+VlskvwHmMi/dYod6yd7aFYu4CU3g/hjwKo +LY2WNv5j3JhDnEYK9Zj3z7A= +-----END CERTIFICATE----- diff --git a/examples/backend-tls/gateway.yaml b/examples/backend-tls/gateway.yaml new file mode 100644 index 0000000000..72a754546f --- /dev/null +++ b/examples/backend-tls/gateway.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: gateway +spec: + gatewayClassName: nginx + listeners: + - name: http + port: 80 + protocol: HTTP + - name: https + port: 443 + protocol: HTTPS + tls: + mode: Terminate + certificateRefs: + - kind: Secret + name: app-secret diff --git a/examples/backend-tls/policy.yaml b/examples/backend-tls/policy.yaml new file mode 100644 index 0000000000..bbf7859b54 --- /dev/null +++ b/examples/backend-tls/policy.yaml @@ -0,0 +1,17 @@ +apiVersion: gateway.networking.k8s.io/v1alpha2 +kind: BackendTLSPolicy +metadata: + name: backend-tls + namespace: default +spec: + targetRef: + group: '' + kind: Service + name: secure-app + namespace: default + tls: + caCertRefs: + - name: backend-certs + group: '' + kind: ConfigMap + hostname: secure-app.example.com diff --git a/examples/backend-tls/secure-app-routes.yaml b/examples/backend-tls/secure-app-routes.yaml new file mode 100644 index 0000000000..3c388c4749 --- /dev/null +++ b/examples/backend-tls/secure-app-routes.yaml @@ -0,0 +1,35 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: secure-app-tls-redirect +spec: + parentRefs: + - name: gateway + sectionName: http + hostnames: + - "secure-app.example.com" + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + port: 443 +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: secure-app +spec: + parentRefs: + - name: gateway + sectionName: https + hostnames: + - "secure-app.example.com" + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: secure-app + port: 8443 diff --git a/examples/backend-tls/secure-app.yaml b/examples/backend-tls/secure-app.yaml new file mode 100644 index 0000000000..7514282862 --- /dev/null +++ b/examples/backend-tls/secure-app.yaml @@ -0,0 +1,78 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: secure-app +spec: + replicas: 1 + selector: + matchLabels: + app: secure-app + template: + metadata: + labels: + app: secure-app + spec: + containers: + - name: secure-app + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8443 + volumeMounts: + - name: secret + mountPath: /etc/nginx/ssl + readOnly: true + - name: config-volume + mountPath: /etc/nginx/conf.d + volumes: + - name: secret + secret: + secretName: app-tls-secret + - name: config-volume + configMap: + name: secure-config +--- +apiVersion: v1 +kind: Service +metadata: + name: secure-app +spec: + ports: + - port: 8443 + targetPort: 8443 + protocol: TCP + name: https + selector: + app: secure-app +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: secure-config +data: + app.conf: |- + server { + listen 8443 ssl; + listen [::]:8443 ssl; + + server_name secure-app.example.com; + + ssl_certificate /etc/nginx/ssl/tls.crt; + ssl_certificate_key /etc/nginx/ssl/tls.key; + + proxy_ssl_server_name on; + + default_type text/plain; + + location / { + return 200 "hello from pod $hostname\n"; + } + } +--- +apiVersion: v1 +kind: Secret +metadata: + name: app-tls-secret +type: Opaque +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwRENDQXJpZ0F3SUJBZ0lVVDQwYTFYd3doUHVBdDJNMkdZZUovYXluZlFBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRWZNQjBHQTFVRUF3d1djMlZqZFhKbExXRndjQzVsZUdGdGNHeGxMbU52YlRFTE1Ba0dBMVVFQmhNQwpWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1YzJselkyOHdIaGNOTWpRd01URTRNVGd3TVRBeFdoY05NalV3Ck1URTNNVGd3TVRBeFdqQi9NUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVcKTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzV6YVhOamJ6RU9NQXdHQTFVRUNnd0ZUa2RKVGxneEVqQVFCZ05WQkFzTQpDVTVIU1U1WUlFUmxkakVmTUIwR0ExVUVBd3dXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdmJUQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMeUx0eURNbTZ4M0ZEUFJsOGZ0azNweCtrRWQKYTVpTGZOQ3lDbUVjYktBQVBDNEhZckl5b1B5QXpSTlJCMWErekE0UTlrbzJZRG5vR0dkeFJaMEdydldKZUV2Mgo3MWlHNGxhbHRVTS9WOWNvSktQY0UyTEI0R3R6cFA3ckdIWXNvRDlOUXFpV3YwZ0lOdE42MjdrWGg4UW41V1hYCk92Y2FkS2h0bjJER3RvU0VzT3dpNzR5NEt3SmFkWnlwLzJaM0hPakRTNjVIVmxydmUxUXpBMVRzTEp6S3cva3gKbHBSR0lWK0lhUjZXbXZsaVFVdDJxWFg0L3hGeVVEM2Vic05TeXpHUk5mQ0NOTWxlWlV3MTR3ZUdhOEVnc2tDcQprOGdYSmpFZXQxMlR4OGxkY3BpVWlxYVpkOStYZjJmUS8yL2Y5c1IzM3Q4K0VVUWpoZ2ZIbHlsLzV1RUNBd0VBCkFhTjlNSHN3SHdZRFZSMGpCQmd3Rm9BVTRUT096c1d0Q3ZWdGJlWXFSU0FqN2tXajFkb3dDUVlEVlIwVEJBSXcKQURBTEJnTlZIUThFQkFNQ0JQQXdJUVlEVlIwUkJCb3dHSUlXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdgpiVEFkQmdOVkhRNEVGZ1FVZmtWREFFWmIwcjRTZ2swck10a0FvQ2c2RjRnd0RRWUpLb1pJaHZjTkFRRUxCUUFECmdnRUJBQWFiQit6RzVSODl6WitBT2RsRy9wWE9nYjF6VkJsQ0dMSkhyYTl1cTMvcXRPR1VacDlnd2dZSWJ4VnkKUkVLbWVRa05pV0haSDNCSlNTZ3czbE9abGNxcW5xbUJ2OFAxTUxDZ3JqbDJSN1d2NVhkb2RlQkJxc0lvZkNxVgp3ZG51THJUU3RTbmd2MGhDcldBNlBmTnlQeXMzSGJva1k3RExNREhuNmhBQWcwMUNDT0pWWGpNZjFqLzNIMFNCClBQSWxtek5aRUpEd0JMR2hyb1V3aUY3NkNUV1Fudi8yc1pvWHMwUlFiRTY3TmNraXc2Z0svaWRwVTVzMmlkOEQKVExjVjNxenVFaE1ZeUlua0ZWNEJLZlFkTWxDQnE1QWdyU1Jqb2FoaCszbFRwYVpUalJGUGFVd3VZYXVsQXRzNgpra1ROaGltWWQ3Ym1aVk5MK2I0MzhmN1RMaGc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQzhpN2Nnekp1c2R4UXoKMFpmSDdaTjZjZnBCSFd1WWkzelFzZ3BoSEd5Z0FEd3VCMkt5TXFEOGdNMFRVUWRXdnN3T0VQWktObUE1NkJobgpjVVdkQnE3MWlYaEw5dTlZaHVKV3BiVkRQMWZYS0NTajNCTml3ZUJyYzZUKzZ4aDJMS0EvVFVLb2xyOUlDRGJUCmV0dTVGNGZFSitWbDF6cjNHblNvYlo5Z3hyYUVoTERzSXUrTXVDc0NXbldjcWY5bWR4em93MHV1UjFaYTczdFUKTXdOVTdDeWN5c1A1TVphVVJpRmZpR2tlbHByNVlrRkxkcWwxK1A4UmNsQTkzbTdEVXNzeGtUWHdnalRKWG1WTQpOZU1IaG12QklMSkFxcFBJRnlZeEhyZGRrOGZKWFhLWWxJcW1tWGZmbDM5bjBQOXYzL2JFZDk3ZlBoRkVJNFlICng1Y3BmK2JoQWdNQkFBRUNnZ0VBUXJucGJleXJmVTVKTW91Ty9UenBrQkJ0UWdVZzhvUVBBS2E1d0tONEYrbnQKWWxiUHlZUGNjSEErNDRLdUo3ZHZiTno0NU11NG8xV3Q2Vkh2a29KdWdjd01iRW53YTdLVXdKaDFmVjZaL2pXaApQZkpoVS9hTUwwcm1qaWJ5YWNRaVZEVEtEZk1Ici96a05sVEpGUWlzVGpIV1lBUGJSTjh5Z1BjR3pBK1hRVzg5CmxsOFdoeWwrZndjRVAzNTM4TUdKYUpHVmV4Q2d5cVRyKzZwQ29yRUpFL1pSNytiMTNyejRsbmxpZXVYa2pCVkcKSnYvUVI0RVhTSDhuRit3K2FvWTFQaGd2QnFJTnFQZjJMT1V0MzNiN3FDTkFmSVBRdFZFejJsN3NqbmJlcElTTwpvTkhNUFY0N21XTzY2dzhFMzJVMjNjVEtUbytJcWovM0d1eGhweXlYaHdLQmdRRDVNRDhmY3ZyM0xVZHkvZ3I0Ci82MVBqUXNSaWRYdjN0Mk1MVEk4UkduNzJWcGxvVVExNDZHV2xGTGVVVDY1L1ZVMjNsZFUrUWt2eFNMK3U1bW4KRUJIdXUyVmtBUWVYcUJXWkpPTmFSZG9Ia1YzK2Fyc0U4Qld0SWVtZHJ0MWN6bHFjc1VzMWdGdG1COGI3RHB3UwpHKzRoZDlzZG0weDBNS2hoOFVGOUQrZytUd0tCZ1FEQnN4dUpoZ1hnck14S0dVMHJ5MW9WWTJtMGpDUEpJcTkzCmNZRUZGY3lYZ0U4OWlDVmlob3dVVE8yMXpTZ3o0SVVKNUxoc2M5N3VIVER1VXdwcVI5NFBsdjlyaXJvakowM1UKT3FyWHgwbWdNN2xibVM1L1RwS0czZG1QblZ0WEZMektISFgyWDVnUW56emYvdXVKK3NtbDVLQW5WN0VZc1oxcgpkVXJvRm8zcnp3S0JnUUNZdjM5aUdzeEdLaVpMRWZqTjY0UmthRFBwdTFFOTZhSnE0K1dRVmV1VnF3V2ptTGhFClJGWHdCTm5MVjRnWTRIYVUzTFF4N1RvNVl5RnhmclBRV2FSMGI4RFdEVitIRWt5ekJJNnM3bmFZL3YzY0Q3YTIKYnlrS2FPaFlkVEZTUzFmMkJ5UHdGczl2K3NKNWNOb3dxNWhNUWJrNks5RXd4QWJqaXN5M0NjSTJOd0tCZ1FDbwo2c2pZNVVlNjV2WkFxRS9rSVRJdDlNUDU3enhGNnptWnNDSVRqUzhkNzRjcTRjKzRYQjFNbHNtMkFYTk55ajQ2Cm9uc3lHTm9RVE9TZThVdmo0MGlEeitwdW5rdzAyOUhEZ21YNlJwQ3VaRzBBdEZVWU1DMFg3K0FLbmU5SndZdmgKdFhBcHFyT3h5eXdMS3dPOUVEZEp0RmIxK0VNNGhhd0NTZ2RJM21KbGdRS0JnRzIxeEJNRXRzMFBVN3lDYTZ0YwpadDc1NUV4aEdkR3F5MmtHYmtmdzBEaHBQQVVUZmdncVF3NVBYdGVIS1ZBSDlKaG5kVnBBZFFxNmZ1MER1MDNKCkl0cGpxNWluZXVoR0x0alpMR1Nhd0dwY0FUU3h4Z3dCM0l0Z29LKzBCRFhteWxId0lEcUc5Z2crRU5KK0VhL0MKeTFOMmV0ZG1sQ01hNjM4cVJlNFlTWk55Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= diff --git a/internal/framework/gatewayclass/validate.go b/internal/framework/gatewayclass/validate.go index 266e6ca5bc..828f8eb06e 100644 --- a/internal/framework/gatewayclass/validate.go +++ b/internal/framework/gatewayclass/validate.go @@ -17,10 +17,11 @@ const ( ) var gatewayCRDs = map[string]apiVersion{ - "gatewayclasses.gateway.networking.k8s.io": {}, - "gateways.gateway.networking.k8s.io": {}, - "httproutes.gateway.networking.k8s.io": {}, - "referencegrants.gateway.networking.k8s.io": {}, + "gatewayclasses.gateway.networking.k8s.io": {}, + "gateways.gateway.networking.k8s.io": {}, + "httproutes.gateway.networking.k8s.io": {}, + "referencegrants.gateway.networking.k8s.io": {}, + "backendtlspolicies.gateway.networking.k8s.io": {}, } type apiVersion struct { diff --git a/internal/mode/static/config/config.go b/internal/mode/static/config/config.go index cb1ee1efb2..3e7cbd8559 100644 --- a/internal/mode/static/config/config.go +++ b/internal/mode/static/config/config.go @@ -28,16 +28,18 @@ type Config struct { GatewayClassName string // LeaderElection contains the configuration for leader election. LeaderElection LeaderElection - // UpdateGatewayClassStatus enables updating the status of the GatewayClass resource. - UpdateGatewayClassStatus bool - // Plus indicates whether NGINX Plus is being used. - Plus bool // MetricsConfig specifies the metrics config. MetricsConfig MetricsConfig // HealthConfig specifies the health probe config. HealthConfig HealthConfig // TelemetryReportPeriod is the period at which telemetry reports are sent. TelemetryReportPeriod time.Duration + // UpdateGatewayClassStatus enables updating the status of the GatewayClass resource. + UpdateGatewayClassStatus bool + // Plus indicates whether NGINX Plus is being used. + Plus bool + // EnableExperimentalFeatures enables experimental features. + EnableExperimentalFeatures bool } // GatewayPodConfig contains information about this Pod. diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go index 5967190cb3..08c3cd0f67 100644 --- a/internal/mode/static/manager.go +++ b/internal/mode/static/manager.go @@ -28,6 +28,7 @@ import ( metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" k8spredicate "sigs.k8s.io/controller-runtime/pkg/predicate" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" @@ -62,6 +63,7 @@ var scheme = runtime.NewScheme() func init() { utilruntime.Must(gatewayv1beta1.AddToScheme(scheme)) utilruntime.Must(gatewayv1.AddToScheme(scheme)) + utilruntime.Must(gatewayv1alpha2.AddToScheme(scheme)) utilruntime.Must(apiv1.AddToScheme(scheme)) utilruntime.Must(discoveryV1.AddToScheme(scheme)) utilruntime.Must(ngfAPI.AddToScheme(scheme)) @@ -198,7 +200,11 @@ func StartManager(cfg config.Config) error { metricsCollector: handlerCollector, }) - objects, objectLists := prepareFirstEventBatchPreparerArgs(cfg.GatewayClassName, cfg.GatewayNsName) + objects, objectLists := prepareFirstEventBatchPreparerArgs( + cfg.GatewayClassName, + cfg.GatewayNsName, + cfg.EnableExperimentalFeatures, + ) firstBatchPreparer := events.NewFirstEventBatchPreparerImpl(mgr.GetCache(), objects, objectLists) eventLoop := events.NewEventLoop( eventCh, @@ -378,6 +384,18 @@ func registerControllers( }, } + if cfg.EnableExperimentalFeatures { + backendTLSObjs := []ctlrCfg{ + { + objectType: &gatewayv1alpha2.BackendTLSPolicy{}, + }, + { + objectType: &apiv1.ConfigMap{}, + }, + } + controllerRegCfgs = append(controllerRegCfgs, backendTLSObjs...) + } + if cfg.ConfigName != "" { controllerRegCfgs = append(controllerRegCfgs, ctlrCfg{ @@ -441,6 +459,7 @@ func createTelemetryJob( func prepareFirstEventBatchPreparerArgs( gcName string, gwNsName *types.NamespacedName, + enableExperimentalFeatures bool, ) ([]client.Object, []client.ObjectList) { objects := []client.Object{ &gatewayv1.GatewayClass{ObjectMeta: metav1.ObjectMeta{Name: gcName}}, @@ -465,6 +484,10 @@ func prepareFirstEventBatchPreparerArgs( partialObjectMetadataList, } + if enableExperimentalFeatures { + objectLists = append(objectLists, &gatewayv1alpha2.BackendTLSPolicyList{}, &apiv1.ConfigMapList{}) + } + if gwNsName == nil { objectLists = append(objectLists, &gatewayv1.GatewayList{}) } else { diff --git a/internal/mode/static/manager_test.go b/internal/mode/static/manager_test.go index 3191a2e01d..532807d374 100644 --- a/internal/mode/static/manager_test.go +++ b/internal/mode/static/manager_test.go @@ -13,6 +13,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client" metricsserver "sigs.k8s.io/controller-runtime/pkg/metrics/server" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" gatewayv1beta1 "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/config" @@ -35,6 +36,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) { gwNsName *types.NamespacedName expectedObjects []client.Object expectedObjectLists []client.ObjectList + experimentalEnabled bool }{ { name: "gwNsName is nil", @@ -73,13 +75,36 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) { partialObjectMetadataList, }, }, + { + name: "gwNsName is not nil and experimental enabled", + gwNsName: &types.NamespacedName{ + Namespace: "test", + Name: "my-gateway", + }, + expectedObjects: []client.Object{ + &gatewayv1.GatewayClass{ObjectMeta: metav1.ObjectMeta{Name: "nginx"}}, + &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "my-gateway", Namespace: "test"}}, + }, + expectedObjectLists: []client.ObjectList{ + &apiv1.ServiceList{}, + &apiv1.SecretList{}, + &apiv1.NamespaceList{}, + &apiv1.ConfigMapList{}, + &discoveryV1.EndpointSliceList{}, + &gatewayv1.HTTPRouteList{}, + &gatewayv1beta1.ReferenceGrantList{}, + partialObjectMetadataList, + &gatewayv1alpha2.BackendTLSPolicyList{}, + }, + experimentalEnabled: true, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - objects, objectLists := prepareFirstEventBatchPreparerArgs(gcName, test.gwNsName) + objects, objectLists := prepareFirstEventBatchPreparerArgs(gcName, test.gwNsName, test.experimentalEnabled) g.Expect(objects).To(ConsistOf(test.expectedObjects)) g.Expect(objectLists).To(ConsistOf(test.expectedObjectLists)) diff --git a/internal/mode/static/nginx/config/generator.go b/internal/mode/static/nginx/config/generator.go index ecbb5cece2..b20c51caed 100644 --- a/internal/mode/static/nginx/config/generator.go +++ b/internal/mode/static/nginx/config/generator.go @@ -1,6 +1,7 @@ package config import ( + "encoding/base64" "path/filepath" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file" @@ -70,6 +71,10 @@ func (g GeneratorImpl) Generate(conf dataplane.Configuration) []file.File { files = append(files, generateConfigVersion(conf.Version)) + for id, bundle := range conf.CertBundles { + files = append(files, generateCertBundle(id, bundle)) + } + return files } @@ -90,6 +95,23 @@ func generatePEMFileName(id dataplane.SSLKeyPairID) string { return filepath.Join(secretsFolder, string(id)+".pem") } +func generateCertBundle(id dataplane.CertBundleID, cert []byte) file.File { + data := make([]byte, base64.StdEncoding.DecodedLen(len(cert))) + _, err := base64.StdEncoding.Decode(data, cert) + if err != nil { + data = cert + } + return file.File{ + Content: data, + Path: generateCertBundleFileName(id), + Type: file.TypeSecret, + } +} + +func generateCertBundleFileName(id dataplane.CertBundleID) string { + return filepath.Join(secretsFolder, string(id)+".crt") +} + func (g GeneratorImpl) generateHTTPConfig(conf dataplane.Configuration) file.File { var c []byte for _, execute := range g.getExecuteFuncs() { diff --git a/internal/mode/static/nginx/config/generator_test.go b/internal/mode/static/nginx/config/generator_test.go index c42d44cb49..2e4ab29b72 100644 --- a/internal/mode/static/nginx/config/generator_test.go +++ b/internal/mode/static/nginx/config/generator_test.go @@ -59,6 +59,9 @@ func TestGenerate(t *testing.T) { Key: []byte("test-key"), }, }, + CertBundles: map[dataplane.CertBundleID]dataplane.CertBundle{ + "test-certbundle": []byte("test-cert"), + }, } g := NewWithT(t) @@ -67,7 +70,7 @@ func TestGenerate(t *testing.T) { files := generator.Generate(conf) - g.Expect(files).To(HaveLen(3)) + g.Expect(files).To(HaveLen(4)) g.Expect(files[0]).To(Equal(file.File{ Type: file.TypeSecret, @@ -89,4 +92,6 @@ func TestGenerate(t *testing.T) { g.Expect(files[2].Path).To(Equal("/etc/nginx/conf.d/config-version.conf")) configVersion := string(files[2].Content) g.Expect(configVersion).To(ContainSubstring(fmt.Sprintf("return 200 %d", conf.Version))) + + g.Expect(files[3].Path).To(Equal("/etc/nginx/secrets/test-certbundle.crt")) } diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index ae0d53546e..66d7aa05b4 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -13,10 +13,11 @@ type Server struct { // Location holds all configuration for an HTTP location. type Location struct { Return *Return - Rewrites []string + ProxySSLVerify *ProxySSLVerify Path string ProxyPass string HTTPMatchVar string + Rewrites []string ProxySetHeaders []Header } @@ -86,3 +87,10 @@ type MapParameter struct { Value string Result string } + +// ProxySSLVerify holds the proxied HTTPS server verification configuration. +type ProxySSLVerify struct { + CertPath string + Hostname string + VerifyOn bool +} diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index b4cdeb4779..45a0b7cbcc 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -247,7 +247,6 @@ func updateLocationsForFilters( rewrites := createRewritesValForRewriteFilter(filters.RequestURLRewrite, path) proxySetHeaders := generateProxySetHeaders(&matchRule.Filters) - proxyPass := createProxyPass(matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite) for i := range buildLocations { if rewrites != nil { if rewrites.Rewrite != "" { @@ -255,12 +254,44 @@ func updateLocationsForFilters( } } buildLocations[i].ProxySetHeaders = proxySetHeaders + buildLocations[i].ProxySSLVerify = convertProxyTLSFromBackends(matchRule.BackendGroup.Backends) + proxyPass := createProxyPass( + matchRule.BackendGroup, + matchRule.Filters.RequestURLRewrite, + buildLocations[i].ProxySSLVerify != nil, + ) buildLocations[i].ProxyPass = proxyPass } return buildLocations } +func convertProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVerify { + if len(backends) == 0 { + return nil + } + for _, b := range backends { + proxyVerify := convertBackendTLS(b.VerifyTLS) + if proxyVerify != nil { + // If any backend has a backend TLS policy defined, then we use that for the proxy SSL verification. + // If multiple backends in the group have a backend TLS policy defined, then we use the first one we find. + return proxyVerify + } + } + return nil +} + +func convertBackendTLS(v *dataplane.VerifyTLS) *http.ProxySSLVerify { + if v == nil || v.Hostname == "" { + return nil + } + return &http.ProxySSLVerify{ + CertPath: generateCertBundleFileName(v.CertBundleID), + Hostname: v.Hostname, + VerifyOn: v.CertBundleID != "", + } +} + func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return { if filter == nil { return nil @@ -427,18 +458,27 @@ func isPathOnlyMatch(match dataplane.Match) bool { return match.Method == nil && len(match.Headers) == 0 && len(match.QueryParams) == 0 } -func createProxyPass(backendGroup dataplane.BackendGroup, filter *dataplane.HTTPURLRewriteFilter) string { +func createProxyPass( + backendGroup dataplane.BackendGroup, + filter *dataplane.HTTPURLRewriteFilter, + enableTLS bool, +) string { var requestURI string if filter == nil || filter.Path == nil { requestURI = "$request_uri" } + protocol := "http" + if enableTLS { + protocol = "https" + } + backendName := backendGroupName(backendGroup) if backendGroupNeedsSplit(backendGroup) { - return "http://$" + convertStringToSafeVariableName(backendName) + requestURI + return protocol + "://$" + convertStringToSafeVariableName(backendName) + requestURI } - return "http://" + backendName + requestURI + return protocol + "://" + backendName + requestURI } func createMatchLocation(path string) http.Location { diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index a56c55820e..567496a341 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -52,7 +52,15 @@ server { {{- end }} proxy_http_version 1.1; proxy_pass {{ $l.ProxyPass }}; + {{- if $l.ProxySSLVerify }} + proxy_ssl_name {{ $l.ProxySSLVerify.Hostname }}; + {{- if $l.ProxySSLVerify.VerifyOn }} + proxy_ssl_verify on; + proxy_ssl_trusted_certificate {{ $l.ProxySSLVerify.CertPath }}; + {{- end }} + {{- end }} {{- end }} + } {{ end }} } diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 2040bcfef1..2fbd8e6561 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -203,6 +203,22 @@ func TestCreateServers(t *testing.T) { }, } + btpGroup := dataplane.BackendGroup{ + Source: hrNsName, + RuleIdx: 3, + Backends: []dataplane.Backend{ + { + UpstreamName: "test_btp_80", + Valid: true, + Weight: 1, + VerifyTLS: &dataplane.VerifyTLS{ + CertBundleID: "test-btp", + Hostname: "test-btp.example.com", + }, + }, + }, + } + filterGroup1 := dataplane.BackendGroup{Source: hrNsName, RuleIdx: 3} filterGroup2 := dataplane.BackendGroup{Source: hrNsName, RuleIdx: 4} @@ -281,6 +297,16 @@ func TestCreateServers(t *testing.T) { }, }, }, + { + Path: "/backend-tls-policy", + PathType: dataplane.PathTypePrefix, + MatchRules: []dataplane.MatchRule{ + { + Match: dataplane.Match{}, + BackendGroup: btpGroup, + }, + }, + }, { Path: "/redirect-implicit-port", PathType: dataplane.PathTypePrefix, @@ -505,19 +531,19 @@ func TestCreateServers(t *testing.T) { exactMatches := []httpMatch{ { Method: "GET", - RedirectPath: "@rule11-route0", + RedirectPath: "@rule12-route0", }, } redirectHeaderMatches := []httpMatch{ { Headers: []string{"redirect:this"}, - RedirectPath: "@rule5-route0", + RedirectPath: "@rule6-route0", }, } rewriteHeaderMatches := []httpMatch{ { Headers: []string{"rewrite:this"}, - RedirectPath: "@rule7-route0", + RedirectPath: "@rule8-route0", }, } rewriteProxySetHeaders := []http.Header{ @@ -541,7 +567,7 @@ func TestCreateServers(t *testing.T) { invalidFilterHeaderMatches := []httpMatch{ { Headers: []string{"filter:this"}, - RedirectPath: "@rule9-route0", + RedirectPath: "@rule10-route0", }, } @@ -590,6 +616,26 @@ func TestCreateServers(t *testing.T) { ProxyPass: "http://invalid-backend-ref$request_uri", ProxySetHeaders: baseHeaders, }, + { + Path: "/backend-tls-policy/", + ProxyPass: "https://test_btp_80$request_uri", + ProxySetHeaders: baseHeaders, + ProxySSLVerify: &http.ProxySSLVerify{ + Hostname: "test-btp.example.com", + CertPath: "/etc/nginx/secrets/test-btp.crt", + VerifyOn: true, + }, + }, + { + Path: "= /backend-tls-policy", + ProxyPass: "https://test_btp_80$request_uri", + ProxySetHeaders: baseHeaders, + ProxySSLVerify: &http.ProxySSLVerify{ + Hostname: "test-btp.example.com", + CertPath: "/etc/nginx/secrets/test-btp.crt", + VerifyOn: true, + }, + }, { Path: "/redirect-implicit-port/", Return: &http.Return{ @@ -619,7 +665,7 @@ func TestCreateServers(t *testing.T) { }, }, { - Path: "@rule5-route0", + Path: "@rule6-route0", Return: &http.Return{ Body: "$scheme://foo.example.com:8080$request_uri", Code: 302, @@ -646,7 +692,7 @@ func TestCreateServers(t *testing.T) { ProxySetHeaders: rewriteProxySetHeaders, }, { - Path: "@rule7-route0", + Path: "@rule8-route0", Rewrites: []string{"^/rewrite-with-headers(.*)$ /prefix-replacement$1 break"}, ProxyPass: "http://test_foo_80", ProxySetHeaders: rewriteProxySetHeaders, @@ -672,7 +718,7 @@ func TestCreateServers(t *testing.T) { }, }, { - Path: "@rule9-route0", + Path: "@rule10-route0", Return: &http.Return{ Code: http.StatusInternalServerError, }, @@ -691,7 +737,7 @@ func TestCreateServers(t *testing.T) { ProxySetHeaders: baseHeaders, }, { - Path: "@rule11-route0", + Path: "@rule12-route0", ProxyPass: "http://test_foo_80$request_uri", ProxySetHeaders: baseHeaders, }, @@ -1670,7 +1716,7 @@ func TestCreateProxyPass(t *testing.T) { } for _, tc := range tests { - result := createProxyPass(tc.grp, tc.rewrite) + result := createProxyPass(tc.grp, tc.rewrite, false) g.Expect(result).To(Equal(tc.expected)) } } @@ -1791,3 +1837,79 @@ func TestGenerateProxySetHeaders(t *testing.T) { }) } } + +func TestConvertBackendTLSFromGroup(t *testing.T) { + g := NewWithT(t) + + tests := []struct { + expected *http.ProxySSLVerify + msg string + grp []dataplane.Backend + }{ + { + msg: "tls enabled, one backend", + grp: []dataplane.Backend{ + { + UpstreamName: "my-upstream", + Valid: true, + Weight: 1, + VerifyTLS: &dataplane.VerifyTLS{ + CertBundleID: "default-my-cert", + Hostname: "my-hostname", + }, + }, + }, + expected: &http.ProxySSLVerify{ + CertPath: "/etc/nginx/secrets/default-my-cert.crt", + Hostname: "my-hostname", + VerifyOn: true, + }, + }, + { + msg: "tls disabled", + grp: []dataplane.Backend{ + { + UpstreamName: "my-upstream", + Valid: true, + Weight: 1, + VerifyTLS: &dataplane.VerifyTLS{ + CertBundleID: "", + Hostname: "", + }, + }, + }, + expected: nil, + }, + { + msg: "tls enabled, multiple backends", + grp: []dataplane.Backend{ + { + UpstreamName: "my-upstream", + Valid: true, + Weight: 1, + VerifyTLS: &dataplane.VerifyTLS{ + CertBundleID: "default-my-cert", + Hostname: "my-hostname", + }, + }, + { + UpstreamName: "my-upstream", + Valid: true, + Weight: 2, + }, + }, + expected: &http.ProxySSLVerify{ + CertPath: "/etc/nginx/secrets/default-my-cert.crt", + Hostname: "my-hostname", + VerifyOn: true, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.msg, func(t *testing.T) { + result := convertProxyTLSFromBackends(tc.grp) + g.Expect(result).To(Equal(tc.expected)) + }) + } +} diff --git a/internal/mode/static/state/change_processor.go b/internal/mode/static/state/change_processor.go index 385ae0e938..07a8c26416 100644 --- a/internal/mode/static/state/change_processor.go +++ b/internal/mode/static/state/change_processor.go @@ -17,6 +17,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/apiutil" v1 "sigs.k8s.io/gateway-api/apis/v1" gwapivalidation "sigs.k8s.io/gateway-api/apis/v1/validation" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/gatewayclass" @@ -102,14 +103,16 @@ type ChangeProcessorImpl struct { // NewChangeProcessorImpl creates a new ChangeProcessorImpl for the Gateway resource with the configured namespace name. func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { clusterStore := graph.ClusterState{ - GatewayClasses: make(map[types.NamespacedName]*v1.GatewayClass), - Gateways: make(map[types.NamespacedName]*v1.Gateway), - HTTPRoutes: make(map[types.NamespacedName]*v1.HTTPRoute), - Services: make(map[types.NamespacedName]*apiv1.Service), - Namespaces: make(map[types.NamespacedName]*apiv1.Namespace), - ReferenceGrants: make(map[types.NamespacedName]*v1beta1.ReferenceGrant), - Secrets: make(map[types.NamespacedName]*apiv1.Secret), - CRDMetadata: make(map[types.NamespacedName]*metav1.PartialObjectMetadata), + GatewayClasses: make(map[types.NamespacedName]*v1.GatewayClass), + Gateways: make(map[types.NamespacedName]*v1.Gateway), + HTTPRoutes: make(map[types.NamespacedName]*v1.HTTPRoute), + Services: make(map[types.NamespacedName]*apiv1.Service), + Namespaces: make(map[types.NamespacedName]*apiv1.Namespace), + ReferenceGrants: make(map[types.NamespacedName]*v1beta1.ReferenceGrant), + Secrets: make(map[types.NamespacedName]*apiv1.Secret), + CRDMetadata: make(map[types.NamespacedName]*metav1.PartialObjectMetadata), + BackendTLSPolicies: make(map[types.NamespacedName]*v1alpha2.BackendTLSPolicy), + ConfigMaps: make(map[types.NamespacedName]*apiv1.ConfigMap), } extractGVK := func(obj client.Object) schema.GroupVersionKind { @@ -152,6 +155,11 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { store: newObjectStoreMapAdapter(clusterStore.ReferenceGrants), predicate: nil, }, + { + gvk: extractGVK(&v1alpha2.BackendTLSPolicy{}), + store: newObjectStoreMapAdapter(clusterStore.BackendTLSPolicies), + predicate: nil, + }, { gvk: extractGVK(&apiv1.Namespace{}), store: newObjectStoreMapAdapter(clusterStore.Namespaces), @@ -172,6 +180,11 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { store: newObjectStoreMapAdapter(clusterStore.Secrets), predicate: funcPredicate{stateChanged: isReferenced}, }, + { + gvk: extractGVK(&apiv1.ConfigMap{}), + store: newObjectStoreMapAdapter(clusterStore.ConfigMaps), + predicate: funcPredicate{stateChanged: isReferenced}, + }, { gvk: extractGVK(&apiext.CustomResourceDefinition{}), store: newObjectStoreMapAdapter(clusterStore.CRDMetadata), diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go index dba175af51..70f41d8a0b 100644 --- a/internal/mode/static/state/change_processor_test.go +++ b/internal/mode/static/state/change_processor_test.go @@ -184,6 +184,7 @@ func createScheme() *runtime.Scheme { utilruntime.Must(v1.AddToScheme(scheme)) utilruntime.Must(v1beta1.AddToScheme(scheme)) + utilruntime.Must(v1alpha2.AddToScheme(scheme)) utilruntime.Must(apiv1.AddToScheme(scheme)) utilruntime.Must(discoveryV1.AddToScheme(scheme)) utilruntime.Must(apiext.AddToScheme(scheme)) @@ -573,11 +574,11 @@ var _ = Describe("ChangeProcessor", func() { } expGraph.Routes[hr1Name].ParentRefs[0].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, - FailedCondition: staticConds.NewRouteInvalidGateway(), + FailedCondition: staticConds.NewRouteNoMatchingParent(), } expGraph.Routes[hr1Name].ParentRefs[1].Attachment = &graph.ParentRefAttachmentStatus{ AcceptedHostnames: map[string][]string{}, - FailedCondition: staticConds.NewRouteInvalidGateway(), + FailedCondition: staticConds.NewRouteNoMatchingParent(), } expGraph.ReferencedSecrets = nil diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index d48a304b32..cd6b52e647 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" + v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" @@ -35,6 +36,7 @@ func BuildConfiguration( httpServers, sslServers := buildServers(g.Gateway.Listeners) backendGroups := buildBackendGroups(append(httpServers, sslServers...)) keyPairs := buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners) + certBundles := buildCertBundles(g.ReferencedConfigMaps, backendGroups) config := Configuration{ HTTPServers: httpServers, @@ -43,6 +45,7 @@ func BuildConfiguration( BackendGroups: backendGroups, SSLKeyPairs: keyPairs, Version: configVersion, + CertBundles: certBundles, } return config @@ -72,6 +75,44 @@ func buildSSLKeyPairs( return keyPairs } +func buildCertBundles( + configMaps map[types.NamespacedName]*graph.ConfigMap, + backendGroups []BackendGroup, +) map[CertBundleID]CertBundle { + bundles := make(map[CertBundleID]CertBundle) + refByBG := make(map[CertBundleID]bool) + + // We only need to build the cert bundles if there are valid backend groups that reference them. + if len(backendGroups) == 0 { + return bundles + } + for _, bg := range backendGroups { + if bg.Backends == nil { + continue + } + for _, b := range bg.Backends { + if !b.Valid || b.VerifyTLS == nil { + continue + } + refByBG[b.VerifyTLS.CertBundleID] = true + } + } + + for cmName, cm := range configMaps { + id := generateCertBundleID(cmName) + if !refByBG[id] { + continue + } + if cm.Source.Data != nil || len(cm.Source.Data) > 0 { + bundles[id] = CertBundle(cm.Source.Data["ca.crt"]) + } else if cm.Source.BinaryData != nil || len(cm.Source.BinaryData) > 0 { + bundles[id] = CertBundle(cm.Source.BinaryData["ca.crt"]) + } + } + + return bundles +} + func buildBackendGroups(servers []VirtualServer) []BackendGroup { type key struct { nsname types.NamespacedName @@ -122,6 +163,7 @@ func newBackendGroup(refs []graph.BackendRef, sourceNsName types.NamespacedName, UpstreamName: ref.ServicePortReference(), Weight: ref.Weight, Valid: ref.Valid, + VerifyTLS: convertBackendTLS(ref.BackendTLSPolicy), }) } @@ -132,6 +174,20 @@ func newBackendGroup(refs []graph.BackendRef, sourceNsName types.NamespacedName, } } +func convertBackendTLS(btp *v1alpha2.BackendTLSPolicy) *VerifyTLS { + if btp == nil { + return nil + } + verify := &VerifyTLS{} + if btp.Spec.TLS.CACertRefs != nil && len(btp.Spec.TLS.CACertRefs) > 0 { + // We only support one CACertRef, take the first one and ignore anything else + b := btp.Spec.TLS.CACertRefs[0] + verify.CertBundleID = generateCertBundleID(types.NamespacedName{Namespace: btp.Namespace, Name: string(b.Name)}) + } + verify.Hostname = string(btp.Spec.TLS.Hostname) + return verify +} + func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) { rulesForProtocol := map[v1.ProtocolType]portPathRules{ v1.HTTPProtocolType: make(portPathRules), @@ -490,3 +546,10 @@ func listenerHostnameMoreSpecific(host1, host2 *v1.Hostname) bool { func generateSSLKeyPairID(secret types.NamespacedName) SSLKeyPairID { return SSLKeyPairID(fmt.Sprintf("ssl_keypair_%s_%s", secret.Namespace, secret.Name)) } + +// generateCertBundleID generates an ID for the certificate bundle based on the ConfigMap namespaced name. +// It is guaranteed to be unique per unique namespaced name. +// The ID is safe to use as a file name. +func generateCertBundleID(configMap types.NamespacedName) CertBundleID { + return CertBundleID(fmt.Sprintf("cert_bundle_%s_%s", configMap.Namespace, configMap.Name)) +} diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index f797dd9a66..4115798dd2 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" + v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" @@ -313,6 +314,84 @@ func TestBuildConfiguration(t *testing.T) { pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/third", pathType: prefix}, ) + httpsHR8, expHTTPSHR8Groups, httpsRouteHR8 := createTestResources( + "https-hr-8", + "foo.example.com", + "listener-443-1", + pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/", pathType: prefix}, + ) + + httpsRouteHR8.Rules[0].BackendRefs[0].BackendTLSPolicy = &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "foo", + Namespace: (*v1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []v1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap-1", + Group: "", + }, + }, + }, + }, + } + + expHTTPSHR8Groups[0].Backends[0].VerifyTLS = &VerifyTLS{ + CertBundleID: generateCertBundleID(types.NamespacedName{Namespace: "test", Name: "configmap-1"}), + Hostname: "foo.example.com", + } + + httpsHR9, expHTTPSHR9Groups, httpsRouteHR9 := createTestResources( + "https-hr-9", + "foo.example.com", + "listener-443-1", + pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/", pathType: prefix}, + ) + + httpsRouteHR9.Rules[0].BackendRefs[0].BackendTLSPolicy = &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp2", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "foo", + Namespace: (*v1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []v1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap-2", + Group: "", + }, + }, + }, + }, + } + + expHTTPSHR9Groups[0].Backends[0].VerifyTLS = &VerifyTLS{ + CertBundleID: generateCertBundleID(types.NamespacedName{Namespace: "test", Name: "configmap-2"}), + Hostname: "foo.example.com", + } + secret1NsName := types.NamespacedName{Namespace: "test", Name: "secret-1"} secret1 := &graph.Secret{ Source: &apiv1.Secret{ @@ -425,6 +504,31 @@ func TestBuildConfiguration(t *testing.T) { }, } + referencedConfigMaps := map[types.NamespacedName]*graph.ConfigMap{ + {Namespace: "test", Name: "configmap-1"}: { + Source: &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap-1", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": "cert-1", + }, + }, + }, + {Namespace: "test", Name: "configmap-2"}: { + Source: &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap-2", + Namespace: "test", + }, + BinaryData: map[string][]byte{ + "ca.crt": []byte("cert-2"), + }, + }, + }, + } + tests := []struct { graph *graph.Graph msg string @@ -446,6 +550,7 @@ func TestBuildConfiguration(t *testing.T) { HTTPServers: []VirtualServer{}, SSLServers: []VirtualServer{}, SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "no listeners and routes", }, @@ -477,6 +582,7 @@ func TestBuildConfiguration(t *testing.T) { }, SSLServers: []VirtualServer{}, SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "http listener with no routes", }, @@ -539,6 +645,7 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-1"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "http and https listeners with no valid routes", }, @@ -601,6 +708,7 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-2"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "https listeners with no routes", }, @@ -633,6 +741,7 @@ func TestBuildConfiguration(t *testing.T) { HTTPServers: []VirtualServer{}, SSLServers: []VirtualServer{}, SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "invalid https listener with resolved secret", }, @@ -704,6 +813,7 @@ func TestBuildConfiguration(t *testing.T) { Upstreams: []Upstream{fooUpstream}, BackendGroups: []BackendGroup{expHR1Groups[0], expHR2Groups[0]}, SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "one http listener with two routes for different hostnames", }, @@ -823,6 +933,7 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-2"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "two https listeners each with routes for different hostnames", }, @@ -982,6 +1093,7 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-1"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "one http and one https listener with two routes with the same hostname with and without collisions", }, @@ -1194,6 +1306,7 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-1"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "multiple http and https listener; different ports", @@ -1325,6 +1438,7 @@ func TestBuildConfiguration(t *testing.T) { Upstreams: []Upstream{fooUpstream}, BackendGroups: []BackendGroup{expHR5Groups[0], expHR5Groups[1]}, SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "one http listener with one route with filters", }, @@ -1426,6 +1540,7 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-1"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "one http and one https listener with routes with valid and invalid rules", }, @@ -1489,6 +1604,7 @@ func TestBuildConfiguration(t *testing.T) { Upstreams: []Upstream{fooUpstream}, BackendGroups: []BackendGroup{expHR7Groups[0], expHR7Groups[1]}, SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{}, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "duplicate paths with different types", }, @@ -1576,9 +1692,162 @@ func TestBuildConfiguration(t *testing.T) { Key: []byte("privateKey-2"), }, }, + CertBundles: map[CertBundleID]CertBundle{}, }, msg: "two https listeners with different hostnames but same route; chooses listener with more specific hostname", }, + { + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ + Source: &v1.GatewayClass{}, + Valid: true, + }, + Gateway: &graph.Gateway{ + Source: &v1.Gateway{}, + Listeners: []*graph.Listener{ + { + Name: "listener-443", + Source: listener443, + Valid: true, + Routes: map[types.NamespacedName]*graph.Route{ + {Namespace: "test", Name: "https-hr-8"}: httpsRouteHR8, + }, + ResolvedSecret: &secret1NsName, + }, + }, + }, + Routes: map[types.NamespacedName]*graph.Route{ + {Namespace: "test", Name: "https-hr-8"}: httpsRouteHR8, + }, + ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + secret1NsName: secret1, + }, + ReferencedConfigMaps: referencedConfigMaps, + }, + expConf: Configuration{ + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{ + { + IsDefault: true, + Port: 443, + }, + { + Hostname: "foo.example.com", + PathRules: []PathRule{ + { + Path: "/", + PathType: PathTypePrefix, + MatchRules: []MatchRule{ + { + BackendGroup: expHTTPSHR8Groups[0], + Source: &httpsHR8.ObjectMeta, + }, + { + BackendGroup: expHTTPSHR8Groups[1], + Source: &httpsHR8.ObjectMeta, + }, + }, + }, + }, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + }, + { + Hostname: wildcardHostname, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + }, + }, + Upstreams: []Upstream{fooUpstream}, + BackendGroups: []BackendGroup{expHTTPSHR8Groups[0], expHTTPSHR8Groups[1]}, + SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ + "ssl_keypair_test_secret-1": { + Cert: []byte("cert-1"), + Key: []byte("privateKey-1"), + }, + }, + CertBundles: map[CertBundleID]CertBundle{ + "cert_bundle_test_configmap-1": []byte("cert-1"), + }, + }, + msg: "https listener with httproute with backend that has a backend TLS policy attached", + }, + { + graph: &graph.Graph{ + GatewayClass: &graph.GatewayClass{ + Source: &v1.GatewayClass{}, + Valid: true, + }, + Gateway: &graph.Gateway{ + Source: &v1.Gateway{}, + Listeners: []*graph.Listener{ + { + Name: "listener-443", + Source: listener443, + Valid: true, + Routes: map[types.NamespacedName]*graph.Route{ + {Namespace: "test", Name: "https-hr-9"}: httpsRouteHR9, + }, + ResolvedSecret: &secret1NsName, + }, + }, + }, + Routes: map[types.NamespacedName]*graph.Route{ + {Namespace: "test", Name: "https-hr-9"}: httpsRouteHR9, + }, + ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ + secret1NsName: secret1, + }, + ReferencedConfigMaps: referencedConfigMaps, + }, + expConf: Configuration{ + HTTPServers: []VirtualServer{}, + SSLServers: []VirtualServer{ + { + IsDefault: true, + Port: 443, + }, + { + Hostname: "foo.example.com", + PathRules: []PathRule{ + { + Path: "/", + PathType: PathTypePrefix, + MatchRules: []MatchRule{ + { + BackendGroup: expHTTPSHR9Groups[0], + Source: &httpsHR9.ObjectMeta, + }, + { + BackendGroup: expHTTPSHR9Groups[1], + Source: &httpsHR9.ObjectMeta, + }, + }, + }, + }, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + }, + { + Hostname: wildcardHostname, + SSL: &SSL{KeyPairID: "ssl_keypair_test_secret-1"}, + Port: 443, + }, + }, + Upstreams: []Upstream{fooUpstream}, + BackendGroups: []BackendGroup{expHTTPSHR9Groups[0], expHTTPSHR9Groups[1]}, + SSLKeyPairs: map[SSLKeyPairID]SSLKeyPair{ + "ssl_keypair_test_secret-1": { + Cert: []byte("cert-1"), + Key: []byte("privateKey-1"), + }, + }, + CertBundles: map[CertBundleID]CertBundle{ + "cert_bundle_test_configmap-2": []byte("cert-2"), + }, + }, + msg: "https listener with httproute with backend that has a backend TLS policy with binaryData attached", + }, } for _, test := range tests { @@ -1593,6 +1862,7 @@ func TestBuildConfiguration(t *testing.T) { g.Expect(result.SSLServers).To(ConsistOf(test.expConf.SSLServers)) g.Expect(result.SSLKeyPairs).To(Equal(test.expConf.SSLKeyPairs)) g.Expect(result.Version).To(Equal(1)) + g.Expect(result.CertBundles).To(Equal(test.expConf.CertBundles)) }) } } @@ -2155,3 +2425,65 @@ func TestHostnameMoreSpecific(t *testing.T) { }) } } + +func TestConvertBackendTLS(t *testing.T) { + btpCaCertRefs := &v1alpha2.BackendTLSPolicy{ + Spec: v1alpha2.BackendTLSPolicySpec{ + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []v1.LocalObjectReference{ + { + Name: "ca-cert", + }, + }, + Hostname: "example.com", + }, + }, + } + + btpWellKnownCerts := &v1alpha2.BackendTLSPolicy{ + Spec: v1alpha2.BackendTLSPolicySpec{ + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "example.com", + }, + }, + } + + expectedWithCertPath := &VerifyTLS{ + CertBundleID: generateCertBundleID(types.NamespacedName{Namespace: btpCaCertRefs.Namespace, Name: "ca-cert"}), + Hostname: "example.com", + } + + expectedWithWellKnownCerts := &VerifyTLS{ + Hostname: "example.com", + } + + tests := []struct { + btp *v1alpha2.BackendTLSPolicy + expected *VerifyTLS + msg string + }{ + { + btp: nil, + expected: nil, + msg: "nil backend tls policy", + }, + { + btp: btpCaCertRefs, + expected: expectedWithCertPath, + msg: "normal case with cert path", + }, + { + btp: btpWellKnownCerts, + expected: expectedWithWellKnownCerts, + msg: "normal case no cert path", + }, + } + + for _, tc := range tests { + t.Run(tc.msg, func(t *testing.T) { + g := NewWithT(t) + + g.Expect(convertBackendTLS(tc.btp)).To(Equal(tc.expected)) + }) + } +} diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 465e421ea3..53a657e404 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -23,6 +23,8 @@ const ( type Configuration struct { // SSLKeyPairs holds all unique SSLKeyPairs. SSLKeyPairs map[SSLKeyPairID]SSLKeyPair + // CertBundles holds all unique Certificate Bundles. + CertBundles map[CertBundleID]CertBundle // HTTPServers holds all HTTPServers. HTTPServers []VirtualServer // SSLServers holds all SSLServers. @@ -39,6 +41,13 @@ type Configuration struct { // The ID is safe to use as a file name. type SSLKeyPairID string +// CertBundleID is a unique identifier for a Certificate bundle. +// The ID is safe to use as a file name. +type CertBundleID string + +// CertBundle is a Certificate bundle. +type CertBundle []byte + // SSLKeyPair is an SSL private/public key pair. type SSLKeyPair struct { // Cert is the certificate. @@ -222,6 +231,8 @@ func (bg *BackendGroup) Name() string { // Backend represents a Backend for a routing rule. type Backend struct { + // VerifyTLS holds the backend TLS verification configuration. + VerifyTLS *VerifyTLS // UpstreamName is the name of the upstream for this backend. UpstreamName string // Weight is the weight of the BackendRef. @@ -231,3 +242,9 @@ type Backend struct { // Valid indicates whether the Backend is valid. Valid bool } + +// VerifyTLS holds the backend TLS verification configuration. +type VerifyTLS struct { + CertBundleID CertBundleID + Hostname string +} diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index f8eab82d80..11a6ccd6d4 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -7,6 +7,7 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" @@ -14,6 +15,8 @@ import ( // BackendRef is an internal representation of a backendRef in an HTTPRoute. type BackendRef struct { + // BackendTLSPolicy is the BackendTLSPolicy of the Service which is referenced by the backendRef. + BackendTLSPolicy *gatewayv1alpha2.BackendTLSPolicy // SvcNsName is the NamespacedName of the Service referenced by the backendRef. SvcNsName types.NamespacedName // ServicePort is the ServicePort of the Service which is referenced by the backendRef. @@ -37,9 +40,11 @@ func addBackendRefsToRouteRules( routes map[types.NamespacedName]*Route, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, + backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, + configMapResolver *configMapResolver, ) { for _, r := range routes { - addBackendRefsToRules(r, refGrantResolver, services) + addBackendRefsToRules(r, refGrantResolver, services, backendTLSPolicies, configMapResolver) } } @@ -50,6 +55,8 @@ func addBackendRefsToRules( route *Route, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, + backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, + configMapResolver *configMapResolver, ) { if !route.Valid { return @@ -73,7 +80,15 @@ func addBackendRefsToRules( for refIdx, ref := range rule.BackendRefs { refPath := field.NewPath("spec").Child("rules").Index(idx).Child("backendRefs").Index(refIdx) - ref, cond := createBackendRef(ref, route.Source.Namespace, refGrantResolver, services, refPath) + ref, cond := createBackendRef( + ref, + route.Source.Namespace, + refGrantResolver, + services, + refPath, + backendTLSPolicies, + configMapResolver, + ) backendRefs = append(backendRefs, ref) if cond != nil { @@ -91,6 +106,8 @@ func createBackendRef( refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, refPath *field.Path, + backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, + configMapResolver *configMapResolver, ) (BackendRef, *conditions.Condition) { // Data plane will handle invalid ref by responding with 500. // Because of that, we always need to add a BackendRef to group.Backends, even if the ref is invalid. @@ -130,16 +147,125 @@ func createBackendRef( return backendRef, &cond } + backendTLSPolicy, err := findBackendTLSPolicyForService( + backendTLSPolicies, + ref, + sourceNamespace, + configMapResolver, + ) + if err != nil { + backendRef = BackendRef{ + SvcNsName: svcNsName, + ServicePort: svcPort, + Weight: weight, + Valid: false, + } + + cond := staticConds.NewRouteBackendRefUnsupportedValue(err.Error()) + return backendRef, &cond + } + backendRef = BackendRef{ - SvcNsName: svcNsName, - ServicePort: svcPort, - Valid: true, - Weight: weight, + SvcNsName: svcNsName, + BackendTLSPolicy: backendTLSPolicy, + ServicePort: svcPort, + Valid: true, + Weight: weight, } return backendRef, nil } +func findBackendTLSPolicyForService( + backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, + ref gatewayv1.HTTPBackendRef, + routeNamespace string, + configMapResolver *configMapResolver, +) (*gatewayv1alpha2.BackendTLSPolicy, error) { + var beTLSPolicy *gatewayv1alpha2.BackendTLSPolicy + refNs := routeNamespace + if ref.Namespace != nil { + refNs = string(*ref.Namespace) + } + + for _, btp := range backendTLSPolicies { + btpNs := btp.Namespace + if btp.Spec.TargetRef.Namespace != nil { + btpNs = string(*btp.Spec.TargetRef.Namespace) + } + if btp.Spec.TargetRef.Name == ref.Name && btpNs == refNs { + beTLSPolicy = btp + break + } + } + + if beTLSPolicy == nil { + return nil, nil + } + + err := validateBackendTLSPolicy(beTLSPolicy, configMapResolver) + + return beTLSPolicy, err +} + +func validateBackendTLSPolicy( + btp *gatewayv1alpha2.BackendTLSPolicy, + configMapResolver *configMapResolver, +) error { + if err := validateBackendTLSHostname(btp); err != nil { + return err + } + if btp.Spec.TLS.CACertRefs != nil && len(btp.Spec.TLS.CACertRefs) > 0 { + return validateBackendTLSCACertRef(btp, configMapResolver) + } else if btp.Spec.TLS.WellKnownCACerts != nil { + return validateBackendTLSWellKnownCACerts(btp) + } + return fmt.Errorf("must specify either CACertRefs or WellKnownCACerts") +} + +func validateBackendTLSHostname(btp *gatewayv1alpha2.BackendTLSPolicy) error { + h := string(btp.Spec.TLS.Hostname) + + if err := validateHostname(h); err != nil { + path := field.NewPath("tls.hostname") + valErr := field.Invalid(path, btp.Spec.TLS.Hostname, err.Error()) + return valErr + } + return nil +} + +func validateBackendTLSCACertRef(btp *gatewayv1alpha2.BackendTLSPolicy, configMapResolver *configMapResolver) error { + if len(btp.Spec.TLS.CACertRefs) != 1 { + path := field.NewPath("tls.cacertrefs") + valErr := field.TooMany(path, len(btp.Spec.TLS.CACertRefs), 1) + return valErr + } + if btp.Spec.TLS.CACertRefs[0].Kind != "ConfigMap" { + path := field.NewPath("tls.cacertrefs[0].kind") + valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"ConfigMap"}) + return valErr + } + if btp.Spec.TLS.CACertRefs[0].Group != "" && btp.Spec.TLS.CACertRefs[0].Group != "core" { + path := field.NewPath("tls.cacertrefs[0].group") + valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"", "core"}) + return valErr + } + nsName := types.NamespacedName{Namespace: btp.Namespace, Name: string(btp.Spec.TLS.CACertRefs[0].Name)} + if err := configMapResolver.resolve(nsName); err != nil { + path := field.NewPath("tls.cacertrefs[0]") + return field.Invalid(path, btp.Spec.TLS.CACertRefs[0], err.Error()) + } + return nil +} + +func validateBackendTLSWellKnownCACerts(btp *gatewayv1alpha2.BackendTLSPolicy) error { + if *btp.Spec.TLS.WellKnownCACerts != gatewayv1alpha2.WellKnownCACertSystem { + path := field.NewPath("tls.wellknowncacerts") + return field.Invalid(path, btp.Spec.TLS.WellKnownCACerts, "unsupported value") + } + return nil +} + // getServiceAndPortFromRef extracts the NamespacedName of the Service and the port from a BackendRef. // It can return an error and an empty v1.ServicePort in two cases: // 1. The Service referenced from the BackendRef does not exist in the cluster/state. diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index d2205c752f..09f9bdb53d 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -10,6 +10,7 @@ import ( "k8s.io/apimachinery/pkg/util/validation/field" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" @@ -421,6 +422,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { services := map[types.NamespacedName]*v1.Service{ {Namespace: "test", Name: "svc1"}: svc1, } + policies := map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{} tests := []struct { name string @@ -534,11 +536,13 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, } + configMapResolver := newConfigMapResolver(nil) + for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) - addBackendRefsToRules(test.route, resolver, services) + addBackendRefsToRules(test.route, resolver, services, policies, configMapResolver) var actual []BackendRef if test.route.Rules != nil { @@ -552,22 +556,67 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { } func TestCreateBackend(t *testing.T) { - svc1 := &v1.Service{ + createService := func(name string) *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "test", + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + }, + }, + }, + } + } + svc1 := createService("service1") + svc2 := createService("service2") + svc3 := createService("service3") + svc1NamespacedName := types.NamespacedName{Namespace: "test", Name: "service1"} + svc2NamespacedName := types.NamespacedName{Namespace: "test", Name: "service2"} + svc3NamespacedName := types.NamespacedName{Namespace: "test", Name: "service3"} + + btp := &v1alpha2.BackendTLSPolicy{ ObjectMeta: metav1.ObjectMeta{ + Name: "btp", Namespace: "test", - Name: "service1", }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Port: 80, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "service2", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), }, }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), + }, }, } - svc1NamespacedName := types.NamespacedName{ - Namespace: "test", - Name: "service1", + btp2 := &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp2", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "service3", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertType("unknown"))), + }, + }, } tests := []struct { @@ -669,21 +718,77 @@ func TestCreateBackend(t *testing.T) { ), name: "service doesn't exist", }, + { + ref: gatewayv1.HTTPBackendRef{ + BackendRef: getModifiedRef(func(backend gatewayv1.BackendRef) gatewayv1.BackendRef { + backend.Name = "service2" + return backend + }), + }, + expectedBackend: BackendRef{ + SvcNsName: svc2NamespacedName, + ServicePort: svc1.Spec.Ports[0], + Weight: 5, + Valid: true, + BackendTLSPolicy: btp, + }, + expectedServicePortReference: "test_service2_80", + expectedCondition: nil, + name: "normal case with policy", + }, + { + ref: gatewayv1.HTTPBackendRef{ + BackendRef: getModifiedRef(func(backend gatewayv1.BackendRef) gatewayv1.BackendRef { + backend.Name = "service3" + return backend + }), + }, + expectedBackend: BackendRef{ + SvcNsName: svc3NamespacedName, + ServicePort: svc1.Spec.Ports[0], + Weight: 5, + Valid: false, + }, + expectedServicePortReference: "", + expectedCondition: helpers.GetPointer( + staticConds.NewRouteBackendRefUnsupportedValue( + "tls.wellknowncacerts: Invalid value: \"unknown\": unsupported value", + ), + ), + name: "invalid policy", + }, } services := map[types.NamespacedName]*v1.Service{ client.ObjectKeyFromObject(svc1): svc1, + client.ObjectKeyFromObject(svc2): svc2, + client.ObjectKeyFromObject(svc3): svc3, + } + policies := map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{ + client.ObjectKeyFromObject(btp): btp, + client.ObjectKeyFromObject(btp2): btp2, } + sourceNamespace := "test" refPath := field.NewPath("test") + configMapResolver := newConfigMapResolver(nil) + for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) - backend, cond := createBackendRef(test.ref, sourceNamespace, resolver, services, refPath) + backend, cond := createBackendRef( + test.ref, + sourceNamespace, + resolver, + services, + refPath, + policies, + configMapResolver, + ) g.Expect(helpers.Diff(test.expectedBackend, backend)).To(BeEmpty()) g.Expect(cond).To(Equal(test.expectedCondition)) @@ -710,7 +815,6 @@ func TestGetServicePort(t *testing.T) { }, }, } - g := NewWithT(t) // ports exist for _, p := range []int32{80, 81, 82} { @@ -724,3 +828,242 @@ func TestGetServicePort(t *testing.T) { g.Expect(err).Should(HaveOccurred()) g.Expect(port.Port).To(Equal(int32(0))) } + +func TestValidateBackendTLSPolicy(t *testing.T) { + targetRefNormalCase := &v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "service1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + } + + localObjectRefNormalCase := []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "", + }, + } + + localObjectRefInvalidName := []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "invalid", + Group: "", + }, + } + + localObjectRefInvalidKind := []gatewayv1.LocalObjectReference{ + { + Kind: "Secret", + Name: "secret", + Group: "", + }, + } + + localObjectRefInvalidGroup := []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "bhu", + }, + } + + localObjectRefTooManyCerts := append(localObjectRefNormalCase, localObjectRefInvalidName...) + + tests := []struct { + tlsPolicy *v1alpha2.BackendTLSPolicy + name string + expectError bool + }{ + { + name: "normal case with ca cert refs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "foo.test.com", + }, + }, + }, + expectError: false, + }, + { + name: "normal case with well known certs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), + Hostname: "foo.test.com", + }, + }, + }, + expectError: false, + }, + { + name: "no hostname invalid case", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "", + }, + }, + }, + expectError: true, + }, + { + name: "invalid ca cert ref name", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefInvalidName, + Hostname: "foo.test.com", + }, + }, + }, + expectError: true, + }, + { + name: "invalid ca cert ref kind", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefInvalidKind, + Hostname: "foo.test.com", + }, + }, + }, + expectError: true, + }, + { + name: "invalid ca cert ref group", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefInvalidGroup, + Hostname: "foo.test.com", + }, + }, + }, + expectError: true, + }, + { + name: "invalid case with well known certs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertType("unknown"))), + Hostname: "foo.test.com", + }, + }, + }, + expectError: true, + }, + { + name: "invalid case neither TLS config option chosen", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.test.com", + }, + }, + }, + expectError: true, + }, + { + name: "invalid case with too many ca cert refs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefTooManyCerts, + Hostname: "foo.test.com", + }, + }, + }, + expectError: true, + }, + } + + configMaps := map[types.NamespacedName]*v1.ConfigMap{ + {Namespace: "test", Name: "configmap"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": caBlock, + }, + }, + {Namespace: "test", Name: "invalid"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": "invalid", + }, + }, + } + configMapResolver := newConfigMapResolver(configMaps) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + err := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver) + + if test.expectError { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} diff --git a/internal/mode/static/state/graph/config_maps.go b/internal/mode/static/state/graph/config_maps.go new file mode 100644 index 0000000000..555b4c91a6 --- /dev/null +++ b/internal/mode/static/state/graph/config_maps.go @@ -0,0 +1,125 @@ +package graph + +import ( + "crypto/x509" + "encoding/base64" + "encoding/pem" + "errors" + "fmt" + + apiv1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" +) + +const CAKey = "ca.crt" + +// ConfigMap represents a ConfigMap resource. +type ConfigMap struct { + // Source holds the actual ConfigMap resource. Can be nil if the ConfigMap does not exist. + Source *apiv1.ConfigMap +} + +type configMapEntry struct { + ConfigMap + // err holds the corresponding error if the ConfigMap is invalid or does not exist. + err error +} + +// configMapResolver wraps the cluster ConfigMaps so that they can be resolved (includes validation). All resolved +// ConfigMaps are saved to be used later. +type configMapResolver struct { + clusterConfigMaps map[types.NamespacedName]*apiv1.ConfigMap + resolvedConfigMaps map[types.NamespacedName]*configMapEntry +} + +func newConfigMapResolver(configMaps map[types.NamespacedName]*apiv1.ConfigMap) *configMapResolver { + return &configMapResolver{ + clusterConfigMaps: configMaps, + resolvedConfigMaps: make(map[types.NamespacedName]*configMapEntry), + } +} + +func (r *configMapResolver) resolve(nsname types.NamespacedName) error { + if s, resolved := r.resolvedConfigMaps[nsname]; resolved { + return s.err + } + + cm, exist := r.clusterConfigMaps[nsname] + + var validationErr error + + if !exist { + validationErr = errors.New("configMap does not exist") + } + + if exist { + + var caCrtPresent bool + + if cm.Data != nil { + if _, exists := cm.Data[CAKey]; exists { + validationErr = validateCA([]byte(cm.Data[CAKey])) + caCrtPresent = true + } + } + + if cm.BinaryData != nil { + if _, exists := cm.BinaryData[CAKey]; exists { + validationErr = validateCA(cm.BinaryData[CAKey]) + caCrtPresent = true + } + } + + if !caCrtPresent { + validationErr = fmt.Errorf("configMap does not have the data or binaryData field %v", CAKey) + } + } + + r.resolvedConfigMaps[nsname] = &configMapEntry{ + ConfigMap: ConfigMap{ + Source: cm, + }, + err: validationErr, + } + + return validationErr +} + +func (r *configMapResolver) getResolvedConfigMaps() map[types.NamespacedName]*ConfigMap { + if len(r.resolvedConfigMaps) == 0 { + return nil + } + + resolved := make(map[types.NamespacedName]*ConfigMap) + + for nsname, entry := range r.resolvedConfigMaps { + // create iteration variable inside the loop to fix implicit memory aliasing + configMap := entry.ConfigMap + resolved[nsname] = &configMap + } + + return resolved +} + +// validateCA validates the ca.crt entry in the ConfigMap. If it is valid, the function returns nil. +func validateCA(caData []byte) error { + data := make([]byte, base64.StdEncoding.DecodedLen(len(caData))) + _, err := base64.StdEncoding.Decode(data, caData) + if err != nil { + data = caData + } + block, _ := pem.Decode(data) + if block == nil { + return fmt.Errorf("the data field %s must hold a valid CERTIFICATE PEM block", CAKey) + } + if block.Type != "CERTIFICATE" { + return fmt.Errorf("the data field %s must hold a valid CERTIFICATE PEM block, but got '%s'", CAKey, block.Type) + } + + _, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return fmt.Errorf("failed to validate certificate: %w", err) + } + + return nil +} diff --git a/internal/mode/static/state/graph/config_maps_test.go b/internal/mode/static/state/graph/config_maps_test.go new file mode 100644 index 0000000000..4e9430c510 --- /dev/null +++ b/internal/mode/static/state/graph/config_maps_test.go @@ -0,0 +1,221 @@ +package graph + +import ( + "encoding/base64" + "testing" + + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +const ( + caBlock = `-----BEGIN CERTIFICATE----- +MIIDSDCCAjACCQDKWvrpwiIyCDANBgkqhkiG9w0BAQsFADBmMQswCQYDVQQGEwJV +UzELMAkGA1UECAwCQ0ExFjAUBgNVBAcMDVNhbiBGcmFuc2lzY28xDjAMBgNVBAoM +BU5HSU5YMQwwCgYDVQQLDANLSUMxFDASBgNVBAMMC2V4YW1wbGUuY29tMB4XDTIw +MTExMjIxMjg0MloXDTMwMTExMDIxMjg0MlowZjELMAkGA1UEBhMCVVMxCzAJBgNV +BAgMAkNBMRYwFAYDVQQHDA1TYW4gRnJhbnNpc2NvMQ4wDAYDVQQKDAVOR0lOWDEM +MAoGA1UECwwDS0lDMRQwEgYDVQQDDAtleGFtcGxlLmNvbTCCASIwDQYJKoZIhvcN +AQEBBQADggEPADCCAQoCggEBAMrlKMqrHfMR4mgaL2zZG2DYYfKCFVmINjlYuOeC +FDTcRgQKtu2YcCxZYBADwHZxEf6NIKtVsMWLhSNS/Nc0BmtiQM/IExhlCiDC6Sl8 +ONrI3w7qJzN6IUERB6tVlQt07rgM0V26UTYu0Ikv1Y8trfLYPZckzBkorQjpcium +qoP2BJf4yyc9LqpxtlWKxelkunVL5ijMEzpj9gEE26TEHbsdEbhoR8g0OeHZqH7e +mXCnSIBR0A/o/s6noGNX+F19lY7Tgw77jOuQQ5Ysi+7nhN2lKvcC819RX7oMpgvt +V5B3nI0mF6BaznjeTs4yQcr1Sm3UTVBwX9ZuvL7RbIXkUm8CAwEAATANBgkqhkiG +9w0BAQsFAAOCAQEAgm04w6OIWGj6tka9ccccnblF0oZzeEAIywjvR5sDcPdvLIeM +eesJy6rFH4DBmMygpcIxJGrSOzZlF3LMvw7zK4stqNtm1HiprF8bzxfTffVYncg6 +hVKErHtZ2FZRj/2TMJ01aRDZSuVbL6UJiokpU6xxT7yy0dFZkKrjUR349gKxRqJw +Am2as0bhi51EqK1GEx3m4c0un2vNh5qP2hv6e/Qze6P96vefNaSk9QMFfuB1kSAk +fGpkiL7bjmjnhKwAmf8jDWDZltB6S56Qy2QjPR8JoOusbYxar4c6EcIwVHv6mdgP +yZxWqQsgtSfFx+Pwon9IPKuq0jQYgeZPSxRMLA== +-----END CERTIFICATE----- +` + caBlockInvalidType = `-----BEGIN PRIVATE KEY----- +MIIJQgIBADANBgkqhkiG9w0BAQEFAASCCSwwggkoAgEAAoICAQCas08k/NzwAGNC +RgTPPF/gKd2K2gP13jvPmpPf1BMFyn+bGEyHRP81cqSHKoatigrR/+rvwTnzbt/X +pyjSelom3OhIOje64Kqi7uaFmGESxjz1C02IbVNLfNyNi1WCaX5U3Wf7u3F+K6Lf +tCSnvg75lkXje9ZiYib6o5/X/ZzZDQ0ryqg9+7CjufDmDfRFs47rp1Lj+VS3+PDP +kGn6f/jD1Q/o0tn44KIjU/gv1F+NnYIpZDixBZwtWQqeVqv5ngiYmhXFTfYCDzFL +34iEcZqWoN99X8zW8itUMVaS2DKcYp29/Gpj9q+Ub9VOnGX1Y2MJ9hUKZJBv++n9 +M3trwJrXkh5XDz7ya4TyP+8sSuIyJ4VQsv1/d0ZSshFw2/6p9NDUABOcBa9RZmrS +shp4sxtiY3xQBOZoAEajFFEwZeILsI7cz9UrISXbXbLOOoIr3aEbPbHfSPPP5oJn +106srUJVnGdYIUY1dGbzMgNttHd+5SlvxPUPM/WlucMSb4CXpJEIIAcYqNlZFznA +ojMYwKVaHFWvY0QUVNg6iMgNBTNnSAK/p23OrzvOVdKIinomXMyAF2ctvJ4Q5qPl +RNakP+W8pNZ+T+sNJ1HAZ4WZ53sAbTioi6c1LIcr99pvo9v7oEWkV5fPGjhLp0Rw +sK7wCos2u+C0E1tMK3KlnwmQ740J0QIDAQABAoICAATSkCYMB+snZ/C59A5tyGNZ +isF4WGVCv0SSggeZOdqVXHL+R+xzly0YXM6l4brpMbsoKi+9K0xOaYX0fQ5KqCLM +AiW2QuR9enRH1EHX5TbLnTzaVFlrZwxUYR+8dzbwiPKmUEaFql0PiS1GFVpxT1Ay +gg08YAuDGcn4bdQy4L/Xa1CxKZt9DB2ef0b8ql+94DeyaKAYtq5hgUhHLTaU5LFe +I/fTEt5ySjuls3fyO+RTQ6p8qFPEZAD55J3Y/9VxOr1fGEylSIT56kR+PGg8jmAh +tbXX1a/hrr4aJ6O+P52maVpx0vM4znJnJhQkRf1nUsANvswrJGGJTdsJztAmGe2Q +BMwMi78B9veg7bB85Orn/ZaumiOkgaK2Qsv8wQXCIbQ1yBypzKDggjHJDL999LsI +rvNDErraz/1CyFaenp+mXLMikODq4j1ArHrF+J9YbkGGZYejPwrxXN6i6w3HngJP +C8MxxBRKs9Oi6q746hjQrepnYO5HgFA2CclS1bvC9B7UPgy6kRNjD6XSxO7Wcjyr +eI34xj9UuotTtw9Gf0CjY2s2ggjkHipRryQVyPNB4yP7+4P/y8DTyHjfTkSJRV7G +CDHLpcvECd2d5oLTxlzOGM9fCTalMyN7c84Y6VqsViOoLU1Lvph/+B2rT++ZKNqS +qyYgYZJs8/59O+1i6Yz9AoIBAQDKQCdYbLVnZ22ozJA4N/N+5aehpSZkpC/DiZ10 +mwi8RVqaOIoxbvsw80ZwoBn5fcv2H6pCAqzUav9jT23NOqNCKpVzLWjgNHtO7aiU +KT5cCHCcpHvnWgBLrM9EsrSdra2HuiwDIrzxlnpkOITdzI/oXUer1dPOt3yc0Bz+ +lAKw/54bu3qNYWH1gteSdAWYt/AK5bbBD9Q3bogAt8zN9XOfDEx+GPqClCa2L9yC +tMuVcPyk078mS+7iEyJzWC4PIZtMikVMMOXi344DnNWh8bolWnrpfB6hr9R4nqzw +P7Mn4VyZDZApNzkBIvsoyFvkEh7uOrOaz9DYmp3OrNtVN06jAoIBAQDD0CTFAajw +0kKRNLoSvVD3ANBDvZCAnflX2V5sppqvhwuxwDLLsadj0juHNOH1G6WJjsbW0HFs +aPmuDLyWPh4AVE13+GUuYMFOVXGHWONZjGRgQyPhE7W9sWH3RMs+GHzX5OdCMT9G +Bq/YZ04i2FQDGLVH8cnwgjzeC7lXetrJOrrLK8sj43vQQuQ/ZKc4VUdFCQoinX6F +LovHi42VyCWzu1r8kOz3RHuo+cncyVvtRnpo/XFuIO9TuKbHE3hg5TSXdLfYC+0l +apirUU5Sq2kO5uQZIruEum+bZCpdd/8Ua8ynfSeg8oG5edhX9UAu7+qgss8IrzfX +3b06ca7bQFD7AoIBAQCdTWBMqeA9WHg1vUS+NOYxYDUMyAIgbIKptrK8KoiUxew9 +3pO89vBvlgbHOf55yZmFCAPH64S4ga+4ceKYqG6p26z5M+xJ1QfCz505/wn9UqMj +cdrciWeJdBKQ/9zydk5tLiNlHPOPgtYWdM8CI0QaGdLQlzJxqMxGuqaSalPdjjJO +p3Yd2Av0g5te0NY5fXY5Q4jsh38qzdEBnfKwjaMrpMkpmgvc25VwRbFgB3X/+SzG +ldop0w0s0G0PARpxslWzJifXpoBmADHYJXcSyYtZ2hGW326DmtnKJr+i7ChPcDww +3hettsGjXK2zfoHZ1S4xY36lfdSVY0wxnsfIc4e5AoIBAG/NKSFe6EHQG3fi/hbz +BwZw7XiwBJCbIiHZl4M7wPhViATOc3JAFg31nE1/kUAsr+CRp9BBJXG7okuRNCAo +iWKwv6avKb5IOjbqrC6WPwEDGtCnpRW+9ja/z+qp2c2zl5yBMtVlXvYxnTdXDJLy +p005T1ArqpxrECvLz+A14jOhF8QnVg5AtZHcj4vugVe1wUKWfbXz7KhIQkEF2ipa +I8SyRaoNaW9pJ538ORiZ06XvZrcJdjlmDp/jvz3NTR8t31BWsR1m+dkyOsceXjTv +b8W1aSk83opTFKRJlbLWb8sOHcTHvde0fwMSocbe3e2uyG1GitUvjhfvoDp9bFP9 +Lf8CggEAGFhWv/+Iur0Sq5DZskChe9BEJp7P/I8VmvI8bT/0LRepkvFt6iAQjyAP +07EQ2ujeQ6BrGeGwNoA3ha49KarBX6OE26pRxUWFLU8Ab74yfycZVAIeUwG2e6p7 +uQy9GGkjWWQ+0eL5UwTjj8D/bors+6rgfUH1iarZ/HxP2boxdJJrj59+R5/DRg7M +zIpoWIuspSbo6AVK8H778qfb6f95oAxRgbahq3jpR0O1ZpDJxja7PC1Bs/hsabjH +atIGfDRw+YXfJBgy43hfbJXTLZJ2cLaKA6xc3HbGEuLwtx9MktjY/4xuUS5aOY35 +UdxohGqleWFMQ3UNLOvc9Fk+q72ryg== +-----END PRIVATE KEY----- +` +) + +func TestValidateCA(t *testing.T) { + base64Data := make([]byte, base64.StdEncoding.EncodedLen(len(caBlock))) + base64.StdEncoding.Encode(base64Data, []byte(caBlock)) + + tests := []struct { + name string + data []byte + errorExpected bool + }{ + { + name: "valid base64", + data: base64Data, + errorExpected: false, + }, + { + name: "valid plain text", + data: []byte(caBlock), + errorExpected: false, + }, + { + name: "invalid pem", + data: []byte("invalid"), + errorExpected: true, + }, + { + name: "invalid type", + data: []byte(caBlockInvalidType), + errorExpected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + err := validateCA(test.data) + if test.errorExpected { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} + +func TestResolve(t *testing.T) { + configMaps := map[types.NamespacedName]*v1.ConfigMap{ + {Namespace: "test", Name: "configmap1"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap1", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": caBlock, + }, + }, + {Namespace: "test", Name: "configmap2"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap2", + Namespace: "test", + }, + BinaryData: map[string][]byte{ + "ca.crt": []byte(caBlock), + }, + }, + {Namespace: "test", Name: "invalid"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": "invalid", + }, + }, + {Namespace: "test", Name: "nocaentry"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "nocaentry", + Namespace: "test", + }, + Data: map[string]string{ + "noca.crt": "something else", + }, + }, + } + + configMapResolver := newConfigMapResolver(configMaps) + + tests := []struct { + name string + nsname types.NamespacedName + errorExpected bool + }{ + { + name: "valid configmap1", + nsname: types.NamespacedName{Namespace: "test", Name: "configmap1"}, + errorExpected: false, + }, + { + name: "valid configmap2", + nsname: types.NamespacedName{Namespace: "test", Name: "configmap2"}, + errorExpected: false, + }, + { + name: "invalid configmap", + nsname: types.NamespacedName{Namespace: "test", Name: "invalid"}, + errorExpected: true, + }, + { + name: "non-existent configmap", + nsname: types.NamespacedName{Namespace: "test", Name: "non-existent"}, + errorExpected: true, + }, + { + name: "configmap missing ca entry", + nsname: types.NamespacedName{Namespace: "test", Name: "nocaentry"}, + errorExpected: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + err := configMapResolver.resolve(test.nsname) + if test.errorExpected { + g.Expect(err).To(HaveOccurred()) + } else { + g.Expect(err).ToNot(HaveOccurred()) + } + }) + } +} diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go index 556c38496b..7f6c883303 100644 --- a/internal/mode/static/state/graph/graph.go +++ b/internal/mode/static/state/graph/graph.go @@ -7,6 +7,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" @@ -15,14 +16,16 @@ import ( // ClusterState includes cluster resources necessary to build the Graph. type ClusterState struct { - GatewayClasses map[types.NamespacedName]*gatewayv1.GatewayClass - Gateways map[types.NamespacedName]*gatewayv1.Gateway - HTTPRoutes map[types.NamespacedName]*gatewayv1.HTTPRoute - Services map[types.NamespacedName]*v1.Service - Namespaces map[types.NamespacedName]*v1.Namespace - ReferenceGrants map[types.NamespacedName]*v1beta1.ReferenceGrant - Secrets map[types.NamespacedName]*v1.Secret - CRDMetadata map[types.NamespacedName]*metav1.PartialObjectMetadata + GatewayClasses map[types.NamespacedName]*gatewayv1.GatewayClass + Gateways map[types.NamespacedName]*gatewayv1.Gateway + HTTPRoutes map[types.NamespacedName]*gatewayv1.HTTPRoute + Services map[types.NamespacedName]*v1.Service + Namespaces map[types.NamespacedName]*v1.Namespace + ReferenceGrants map[types.NamespacedName]*v1beta1.ReferenceGrant + Secrets map[types.NamespacedName]*v1.Secret + CRDMetadata map[types.NamespacedName]*metav1.PartialObjectMetadata + BackendTLSPolicies map[types.NamespacedName]*v1alpha2.BackendTLSPolicy + ConfigMaps map[types.NamespacedName]*v1.ConfigMap } // Graph is a Graph-like representation of Gateway API resources. @@ -51,6 +54,8 @@ type Graph struct { // ReferencedServices includes the NamespacedNames of all the Services that are referenced by at least one HTTPRoute. // Storing the whole resource is not necessary, compared to the similar maps above. ReferencedServices map[types.NamespacedName]struct{} + // ReferencedConfigMaps includes ConfigMaps that have been referenced by any BackendTLSPolicies. + ReferencedConfigMaps map[types.NamespacedName]*ConfigMap } // ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of each port. @@ -62,6 +67,9 @@ func (g *Graph) IsReferenced(resourceType client.Object, nsname types.Namespaced case *v1.Secret: _, exists := g.ReferencedSecrets[nsname] return exists + case *v1.ConfigMap: + _, exists := g.ReferencedConfigMaps[nsname] + return exists case *v1.Namespace: // `existed` is needed as it checks the graph's ReferencedNamespaces which stores all the namespaces that // match the Gateway listener's label selector when the graph was created. This covers the case when @@ -111,6 +119,7 @@ func BuildGraph( gc := buildGatewayClass(processedGwClasses.Winner, state.CRDMetadata) secretResolver := newSecretResolver(state.Secrets) + configMapResolver := newConfigMapResolver(state.ConfigMaps) processedGws := processGateways(state.Gateways, gcName) @@ -119,7 +128,7 @@ func BuildGraph( routes := buildRoutesForGateways(validators.HTTPFieldsValidator, state.HTTPRoutes, processedGws.GetAllNsNames()) bindRoutesToListeners(routes, gw, state.Namespaces) - addBackendRefsToRouteRules(routes, refGrantResolver, state.Services) + addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, state.BackendTLSPolicies, configMapResolver) referencedNamespaces := buildReferencedNamespaces(state.Namespaces, gw) @@ -134,6 +143,7 @@ func BuildGraph( ReferencedSecrets: secretResolver.getResolvedSecrets(), ReferencedNamespaces: referencedNamespaces, ReferencedServices: referencedServices, + ReferencedConfigMaps: configMapResolver.getResolvedConfigMaps(), } return g diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go index ed10c01d51..726cf845aa 100644 --- a/internal/mode/static/state/graph/graph_test.go +++ b/internal/mode/static/state/graph/graph_test.go @@ -12,6 +12,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" @@ -90,21 +91,60 @@ func TestBuildGraph(t *testing.T) { hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 + btp := &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp", + Namespace: "service", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "foo", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("service")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []v1alpha2.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "", + }, + }, + }, + }, + } + hr1Refs := []BackendRef{ { - SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, - ServicePort: v1.ServicePort{Port: 80}, - Valid: true, - Weight: 1, + SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, + ServicePort: v1.ServicePort{Port: 80}, + Valid: true, + Weight: 1, + BackendTLSPolicy: btp, + }, + } + + cm := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "service", + }, + Data: map[string]string{ + "ca.crt": caBlock, }, } hr3Refs := []BackendRef{ { - SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, - ServicePort: v1.ServicePort{Port: 80}, - Valid: true, - Weight: 1, + SvcNsName: types.NamespacedName{Namespace: "service", Name: "foo"}, + ServicePort: v1.ServicePort{Port: 80}, + Valid: true, + Weight: 1, + BackendTLSPolicy: btp, }, } @@ -261,6 +301,12 @@ func TestBuildGraph(t *testing.T) { Secrets: map[types.NamespacedName]*v1.Secret{ client.ObjectKeyFromObject(secret): secret, }, + BackendTLSPolicies: map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{ + client.ObjectKeyFromObject(btp): btp, + }, + ConfigMaps: map[types.NamespacedName]*v1.ConfigMap{ + client.ObjectKeyFromObject(cm): cm, + }, } } @@ -350,6 +396,11 @@ func TestBuildGraph(t *testing.T) { ReferencedServices: map[types.NamespacedName]struct{}{ client.ObjectKeyFromObject(svc): {}, }, + ReferencedConfigMaps: map[types.NamespacedName]*ConfigMap{ + client.ObjectKeyFromObject(cm): { + Source: cm, + }, + }, } } @@ -494,6 +545,25 @@ func TestIsReferenced(t *testing.T) { }, } + baseConfigMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "configmap", + }, + } + sameNamespaceDifferentNameConfigMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "configmap-different-name", + }, + } + differentNamespaceSameNameConfigMap := &v1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test-different-namespace", + Name: "configmap", + }, + } + graph := &Graph{ Gateway: gw, ReferencedSecrets: map[types.NamespacedName]*Secret{ @@ -507,6 +577,11 @@ func TestIsReferenced(t *testing.T) { ReferencedServices: map[types.NamespacedName]struct{}{ client.ObjectKeyFromObject(serviceInGraph): {}, }, + ReferencedConfigMaps: map[types.NamespacedName]*ConfigMap{ + client.ObjectKeyFromObject(baseConfigMap): { + Source: baseConfigMap, + }, + }, } tests := []struct { @@ -603,6 +678,24 @@ func TestIsReferenced(t *testing.T) { }, // Edge cases + { + name: "ConfigMap in graph's ReferencedConfigMaps passes", + resource: baseConfigMap, + graph: graph, + expected: true, + }, + { + name: "ConfigMap not in ReferencedConfigMaps with same Namespace and different Name fails", + resource: sameNamespaceDifferentNameConfigMap, + graph: graph, + expected: false, + }, + { + name: "ConfigMap not in ReferencedConfigMaps with different Namespace and same Name fails", + resource: differentNamespaceSameNameConfigMap, + graph: graph, + expected: false, + }, { name: "Resource is not supported by IsReferenced", resource: &gatewayv1.HTTPRoute{}, diff --git a/internal/mode/static/state/graph/httproute.go b/internal/mode/static/state/graph/httproute.go index c8f42997e2..d9135237d3 100644 --- a/internal/mode/static/state/graph/httproute.go +++ b/internal/mode/static/state/graph/httproute.go @@ -279,7 +279,19 @@ func bindRouteToListeners(r *Route, gw *Gateway, namespaces map[types.Namespaced path := field.NewPath("spec").Child("parentRefs").Index(ref.Idx) - // Case 1: Attachment is not possible due to unsupported configuration + attachableListeners, listenerExists := findAttachableListeners( + getSectionName(routeRef.SectionName), + gw.Listeners, + ) + + // Case 1: Attachment is not possible because the specified SectionName does not match any Listeners in the + // Gateway. + if !listenerExists { + attachment.FailedCondition = staticConds.NewRouteNoMatchingParent() + continue + } + + // Case 2: Attachment is not possible due to unsupported configuration if routeRef.Port != nil { valErr := field.Forbidden(path.Child("port"), "cannot be set") @@ -287,7 +299,7 @@ func bindRouteToListeners(r *Route, gw *Gateway, namespaces map[types.Namespaced continue } - // Case 2: the parentRef references an ignored Gateway resource. + // Case 3: the parentRef references an ignored Gateway resource. referencesWinningGw := ref.Gateway.Namespace == gw.Source.Namespace && ref.Gateway.Name == gw.Source.Name @@ -296,18 +308,18 @@ func bindRouteToListeners(r *Route, gw *Gateway, namespaces map[types.Namespaced continue } - // Case 3: Attachment is not possible because Gateway is invalid + // Case 4: Attachment is not possible because Gateway is invalid if !gw.Valid { attachment.FailedCondition = staticConds.NewRouteInvalidGateway() continue } - // Case 4 - winning Gateway + // Case 5 - winning Gateway // Try to attach Route to all matching listeners - cond, attached := tryToAttachRouteToListeners(ref.Attachment, routeRef.SectionName, r, gw, namespaces) + cond, attached := tryToAttachRouteToListeners(ref.Attachment, attachableListeners, r, gw, namespaces) if !attached { attachment.FailedCondition = cond continue @@ -327,17 +339,11 @@ func bindRouteToListeners(r *Route, gw *Gateway, namespaces map[types.Namespaced // (2) If it fails to attach the route, it will return false and the failure condition. func tryToAttachRouteToListeners( refStatus *ParentRefAttachmentStatus, - sectionName *v1.SectionName, + attachableListeners []*Listener, route *Route, gw *Gateway, namespaces map[types.NamespacedName]*apiv1.Namespace, ) (conditions.Condition, bool) { - attachableListeners, listenerExists := findAttachableListeners(getSectionName(sectionName), gw.Listeners) - - if !listenerExists { - return staticConds.NewRouteNoMatchingParent(), false - } - if len(attachableListeners) == 0 { return staticConds.NewRouteInvalidListener(), false } diff --git a/site/content/includes/installation/install-gateway-api-resources.md b/site/content/includes/installation/install-gateway-api-resources.md index 3636897b2f..3df1da91f5 100644 --- a/site/content/includes/installation/install-gateway-api-resources.md +++ b/site/content/includes/installation/install-gateway-api-resources.md @@ -10,6 +10,14 @@ To install the Gateway API resources, run the following: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml ``` +Alternatively, you can install the Gateway API resources from the experimental channel. We support a subset of the +additional features provided by the experimental channel. Please note that these APIs are not suitable for production +use. To install from the experimental channel, run the following: + +```shell +kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml +``` + If you are running on Kubernetes 1.23 or 1.24, you also need to install the validating webhook. To do so, run: ```shell diff --git a/site/content/includes/installation/uninstall-gateway-api-resources.md b/site/content/includes/installation/uninstall-gateway-api-resources.md index 87b648d572..a402fadf4f 100644 --- a/site/content/includes/installation/uninstall-gateway-api-resources.md +++ b/site/content/includes/installation/uninstall-gateway-api-resources.md @@ -10,6 +10,12 @@ docs: kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml ``` + Alternatively, if you installed the Gateway APIs from the experimental channel, run the following: + + ```shell + kubectl delete -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml + ``` + If you are running on Kubernetes 1.23 or 1.24, you also need to delete the validating webhook. To do so, run: ```shell diff --git a/site/content/installation/installing-ngf/helm.md b/site/content/installation/installing-ngf/helm.md index 3e80c5a77b..efd18a3d28 100644 --- a/site/content/installation/installing-ngf/helm.md +++ b/site/content/installation/installing-ngf/helm.md @@ -109,6 +109,16 @@ To disable the creation of a Service: ``` +#### Experimental features + +We support a subset of the additional features provided by the Gateway API experimental channel. Please note that these APIs are not suitable for production use. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric: + +```shell +helm install ngf oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set nginxGateway.experimentalFeatures.enable=true +``` + +{{}}Requires the Gateway APIs installed from the experimental channel.{{}} + ## Upgrade NGINX Gateway Fabric {{}}For guidance on zero downtime upgrades, see the [Delay Pod Termination](#configure-delayed-pod-termination-for-zero-downtime-upgrades) section below.{{}} @@ -127,6 +137,12 @@ To upgrade your Gateway API resources, take the following steps: kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml ``` + or, if you installed the from the experimental channel: + + ```shell + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml + ``` + ### Upgrade NGINX Gateway Fabric CRDs Helm's upgrade process does not automatically upgrade the NGINX Gateway Fabric CRDs (Custom Resource Definitions). diff --git a/site/content/installation/installing-ngf/manifests.md b/site/content/installation/installing-ngf/manifests.md index bd2361dae1..198bde5619 100644 --- a/site/content/installation/installing-ngf/manifests.md +++ b/site/content/installation/installing-ngf/manifests.md @@ -73,6 +73,28 @@ Deploying NGINX Gateway Fabric with Kubernetes manifests takes only a few steps. Update the nginx-plus-gateway.yaml file to include your chosen image from the F5 Container registry or your custom container image. +#### Enable experimental features + +We support a subset of the additional features provided by the Gateway API experimental channel. Please note that these APIs are not suitable for production use.To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric, edit the `deploy/manifests/nginx-gateway.yaml` to add the `experimental-features-enable` flag to the nginx-gateway deployment spec: + +```yaml +<...> +kind: Deployment +metadata: + name: nginx-gateway +<...> +spec: + <...> + template: + <...> + spec: + containers: + - args: + <...> + - --experimental-features-enable +``` + +{{}}Requires the Gateway APIs installed from the experimental channel.{{}} ### 4. Verify the Deployment @@ -106,6 +128,12 @@ To upgrade NGINX Gateway Fabric and get the latest features and improvements, ta kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml ``` + or, if you installed the from the experimental channel: + + ```shell + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml + ``` + - If you are running on Kubernetes 1.23 or 1.24, you also need to update the validating webhook: ```shell diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index 1bd314749b..03e6bcb7dd 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -9,16 +9,17 @@ docs: "DOCS-000" ## Summary {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| [GatewayClass](#gatewayclass) | Supported | Not supported | Not supported | v1 | -| [Gateway](#gateway) | Supported | Not supported | Not supported | v1 | -| [HTTPRoute](#httproute) | Supported | Partially supported | Not supported | v1 | -| [ReferenceGrant](#referencegrant) | Supported | N/A | Not supported | v1beta1 | -| [TLSRoute](#tlsroute) | Not supported | Not supported | Not supported | N/A | -| [TCPRoute](#tcproute) | Not supported | Not supported | Not supported | N/A | -| [UDPRoute](#udproute) | Not supported | Not supported | Not supported | N/A | -| [Custom policies](#custom-policies) | Not supported | N/A | Not supported | N/A | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| ------------------------------------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| [GatewayClass](#gatewayclass) | Supported | Not supported | Not supported | v1 | +| [Gateway](#gateway) | Supported | Not supported | Not supported | v1 | +| [HTTPRoute](#httproute) | Supported | Partially supported | Not supported | v1 | +| [ReferenceGrant](#referencegrant) | Supported | N/A | Not supported | v1beta1 | +| [TLSRoute](#tlsroute) | Not supported | Not supported | Not supported | N/A | +| [TCPRoute](#tcproute) | Not supported | Not supported | Not supported | N/A | +| [UDPRoute](#udproute) | Not supported | Not supported | Not supported | N/A | +| [BackendTLSPolicy](#backendtlspolicy) | Supported | Supported | Not supported | v1alpha2 | +| [Custom policies](#custom-policies) | Not supported | N/A | Not supported | N/A | {{< /bootstrap-table >}} --- @@ -46,9 +47,9 @@ For a description of each field, visit the [Gateway API documentation](https://g ### GatewayClass {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| GatewayClass | Supported | Not supported | Not supported | v1 | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| ------------ | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| GatewayClass | Supported | Not supported | Not supported | v1 | {{< /bootstrap-table >}} NGINX Gateway Fabric supports a single GatewayClass resource configured with the `--gatewayclass` flag of the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) command. @@ -74,9 +75,9 @@ NGINX Gateway Fabric supports a single GatewayClass resource configured with the ### Gateway {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| Gateway | Supported | Not supported | Not supported | v1 | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| -------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| Gateway | Supported | Not supported | Not supported | v1 | {{< /bootstrap-table >}} NGINX Gateway Fabric supports a single Gateway resource. The Gateway resource must reference NGINX Gateway Fabric's corresponding GatewayClass. @@ -135,9 +136,9 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman ### HTTPRoute {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| HTTPRoute | Supported | Partially supported | Not supported | v1 | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| --------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| HTTPRoute | Supported | Partially supported | Not supported | v1 | {{< /bootstrap-table >}} **Fields**: @@ -182,9 +183,9 @@ See the [static-mode]({{< relref "/reference/cli-help.md#static-mode">}}) comman ### ReferenceGrant {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| ReferenceGrant | Supported | N/A | Not supported | v1beta1 | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| -------------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| ReferenceGrant | Supported | N/A | Not supported | v1beta1 | {{< /bootstrap-table >}} Fields: @@ -204,9 +205,9 @@ Fields: ### TLSRoute {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| TLSRoute | Not supported | Not supported | Not supported | N/A | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| -------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| TLSRoute | Not supported | Not supported | Not supported | N/A | {{< /bootstrap-table >}} --- @@ -214,9 +215,9 @@ Fields: ### TCPRoute {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| TCPRoute | Not supported | Not supported | Not supported | N/A | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| -------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| TCPRoute | Not supported | Not supported | Not supported | N/A | {{< /bootstrap-table >}} --- @@ -224,18 +225,42 @@ Fields: ### UDPRoute {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| -| UDPRoute | Not supported | Not supported | Not supported | N/A | +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| -------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| UDPRoute | Not supported | Not supported | Not supported | N/A | {{< /bootstrap-table >}} --- +### BackendTLSPolicy + +{{< bootstrap-table "table table-striped table-bordered" >}} +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| ---------------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | +| BackendTLSPolicy | Supported | Supported | Not supported | v1alpha2 | +{{< /bootstrap-table >}} + +Fields: + +- `spec` + - `targetRef` + - `group` - supported. + - `kind` - supports `Service`. + - `name` - supported. + - `namespace` - supported. + - `tls` + - `caCertRefs` - supports single reference to a `ConfigMap`, with the CA certificate in a key named `ca.crt`. + - `name`- supported. + - `group` - supported. + - `kind` - supports `ConfigMap`. + - `hostname` - supported. + - `wellKnownCerts` - supports `System`. + ### Custom Policies {{< bootstrap-table "table table-striped table-bordered" >}} -| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | -|-------------------------------------|--------------------|------------------------|---------------------------------------|-------------| +| Resource | Core Support Level | Extended Support Level | Implementation-Specific Support Level | API Version | +| --------------- | ------------------ | ---------------------- | ------------------------------------- | ----------- | | Custom policies | Not supported | N/A | Not supported | N/A | {{< /bootstrap-table >}} diff --git a/site/content/reference/cli-help.md b/site/content/reference/cli-help.md index a018b50ca8..aa72ee363a 100644 --- a/site/content/reference/cli-help.md +++ b/site/content/reference/cli-help.md @@ -25,6 +25,7 @@ _Usage_: | _gatewayclass_ | _string_ | The name of the GatewayClass resource. Every NGINX Gateway Fabric must have a unique corresponding GatewayClass resource. | | _gateway_ | _string_ | The namespaced name of the Gateway resource to use. Must be of the form: `NAMESPACE/NAME`. If not specified, the control plane will process all Gateways for the configured GatewayClass. Among them, it will choose the oldest resource by creation timestamp. If the timestamps are equal, it will choose the resource that appears first in alphabetical order by {namespace}/{name}. | | _nginx-plus_ | _bool_ | Enable support for NGINX Plus. | +| _experimental-features-enable_ | _bool_ | Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. Requires the Gateway APIs installed from the experimental channel. | | _config_ | _string_ | The name of the NginxGateway resource to be used for this controller's dynamic configuration. Lives in the same namespace as the controller. | | _service_ | _string_ | The name of the service that fronts this NGINX Gateway Fabric pod. Lives in the same namespace as the controller. | | _metrics-disable_ | _bool_ | Disable exposing metrics in the Prometheus format (Default: `false`). | From 955feebaa5c559868677965c32eb3f40120cb85c Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Fri, 19 Jan 2024 19:41:30 +0000 Subject: [PATCH 02/13] Add status updater for backendtlspolicy, move logic --- deploy/helm-chart/templates/rbac.yaml | 1 + deploy/manifests/nginx-gateway.yaml | 1 + internal/framework/status/backend_tls.go | 45 +++ internal/framework/status/setters.go | 72 ++++ internal/framework/status/statuses.go | 26 +- internal/framework/status/updater.go | 16 + internal/mode/static/build_statuses.go | 32 ++ internal/mode/static/build_statuses_test.go | 2 + .../static/state/conditions/conditions.go | 56 +++ .../static/state/dataplane/configuration.go | 34 +- .../state/dataplane/configuration_test.go | 135 ++++--- .../mode/static/state/graph/backend_refs.go | 102 ++---- .../static/state/graph/backend_refs_test.go | 335 +++--------------- .../static/state/graph/backend_tls_policy.go | 149 ++++++++ .../state/graph/backend_tls_policy_test.go | 261 ++++++++++++++ .../mode/static/state/graph/config_maps.go | 41 ++- internal/mode/static/state/graph/graph.go | 36 +- .../mode/static/state/graph/graph_test.go | 83 +++-- 18 files changed, 917 insertions(+), 510 deletions(-) create mode 100644 internal/framework/status/backend_tls.go create mode 100644 internal/mode/static/state/graph/backend_tls_policy.go create mode 100644 internal/mode/static/state/graph/backend_tls_policy_test.go diff --git a/deploy/helm-chart/templates/rbac.yaml b/deploy/helm-chart/templates/rbac.yaml index 85a4d02802..4e6c798b1e 100644 --- a/deploy/helm-chart/templates/rbac.yaml +++ b/deploy/helm-chart/templates/rbac.yaml @@ -87,6 +87,7 @@ rules: - httproutes/status - gateways/status - gatewayclasses/status + - backendtlspolicies/status verbs: - update - apiGroups: diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index fdc5ed740d..5d39190807 100644 --- a/deploy/manifests/nginx-gateway.yaml +++ b/deploy/manifests/nginx-gateway.yaml @@ -87,6 +87,7 @@ rules: - httproutes/status - gateways/status - gatewayclasses/status + - backendtlspolicies/status verbs: - update - apiGroups: diff --git a/internal/framework/status/backend_tls.go b/internal/framework/status/backend_tls.go new file mode 100644 index 0000000000..7dae216846 --- /dev/null +++ b/internal/framework/status/backend_tls.go @@ -0,0 +1,45 @@ +package status + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" +) + +// prepareBackendTLSPolicyStatus prepares the status for a BackendTLSPolicy resource. +func prepareBackendTLSPolicyStatus( + oldStatus v1alpha2.PolicyStatus, + status BackendTLSPolicyStatus, + gatewayCtlrName string, + transitionTime metav1.Time, +) v1alpha2.PolicyStatus { + // maxAncestors is the max number of ancestor statuses which is the sum of all new ancestor statuses and all old + // ancestor statuses. + maxAncestors := len(status.AncestorStatuses) + len(oldStatus.Ancestors) + ancestors := make([]v1alpha2.PolicyAncestorStatus, 0, maxAncestors) + + // keep all the ancestor statuses that belong to other controllers + for _, os := range oldStatus.Ancestors { + if string(os.ControllerName) != gatewayCtlrName { + ancestors = append(ancestors, os) + } + } + + for _, as := range status.AncestorStatuses { + // reassign the iteration variable inside the loop to fix implicit memory aliasing + as := as + a := v1alpha2.PolicyAncestorStatus{ + AncestorRef: v1.ParentReference{ + Namespace: (*v1.Namespace)(&as.GatewayNsName.Namespace), + Name: v1alpha2.ObjectName(as.GatewayNsName.Name), + }, + ControllerName: v1alpha2.GatewayController(gatewayCtlrName), + Conditions: convertConditions(as.Conditions, status.ObservedGeneration, transitionTime), + } + ancestors = append(ancestors, a) + } + + return v1alpha2.PolicyStatus{ + Ancestors: ancestors, + } +} diff --git a/internal/framework/status/setters.go b/internal/framework/status/setters.go index fc67136bd0..0ca2be80aa 100644 --- a/internal/framework/status/setters.go +++ b/internal/framework/status/setters.go @@ -6,6 +6,7 @@ import ( v1 "k8s.io/apimachinery/pkg/apis/meta/v1" "sigs.k8s.io/controller-runtime/pkg/client" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" ) @@ -85,6 +86,25 @@ func newHTTPRouteStatusSetter(gatewayCtlrName string, clock Clock, rs HTTPRouteS } } +func newBackendTLSPolicyStatusSetter( + gatewayCtlrName string, + clock Clock, + bs BackendTLSPolicyStatus, +) func(client.Object) bool { + return func(object client.Object) bool { + btp := object.(*gatewayv1alpha2.BackendTLSPolicy) + status := prepareBackendTLSPolicyStatus(btp.Status, bs, gatewayCtlrName, clock.Now()) + + if btpStatusEqual(gatewayCtlrName, btp.Status, status) { + return false + } + + btp.Status = status + + return true + } +} + func gwStatusEqual(prev, cur gatewayv1.GatewayStatus) bool { addressesEqual := slices.EqualFunc(prev.Addresses, cur.Addresses, func(a1, a2 gatewayv1.GatewayStatusAddress) bool { if !equalPointers[gatewayv1.AddressType](a1.Type, a2.Type) { @@ -181,6 +201,58 @@ func routeParentStatusEqual(p1, p2 gatewayv1.RouteParentStatus) bool { return conditionsEqual(p1.Conditions, p2.Conditions) } +func btpStatusEqual(gatewayCtlrName string, prev, cur gatewayv1alpha2.PolicyStatus) bool { + // Since other controllers may update BackendTLSPolicy status we can't assume anything about the order of the + // statuses, and we have to ignore statuses written by other controllers when checking for equality. + // Therefore, we can't use slices.EqualFunc here because it cares about the order. + + // First, we check if the prev status has any PolicyAncestorStatuses that are no longer present in the cur status. + for _, prevAncestor := range prev.Ancestors { + if prevAncestor.ControllerName != gatewayv1.GatewayController(gatewayCtlrName) { + continue + } + + exists := slices.ContainsFunc(cur.Ancestors, func(curAncestor gatewayv1alpha2.PolicyAncestorStatus) bool { + return btpAncestorStatusEqual(prevAncestor, curAncestor) + }) + + if !exists { + return false + } + } + + // Then, we check if the cur status has any PolicyAncestorStatuses that are no longer present in the prev status. + for _, curParent := range cur.Ancestors { + exists := slices.ContainsFunc(prev.Ancestors, func(prevAncestor gatewayv1alpha2.PolicyAncestorStatus) bool { + return btpAncestorStatusEqual(curParent, prevAncestor) + }) + + if !exists { + return false + } + } + + return true +} + +func btpAncestorStatusEqual(p1, p2 gatewayv1alpha2.PolicyAncestorStatus) bool { + if p1.ControllerName != p2.ControllerName { + return false + } + + if p1.AncestorRef.Name != p2.AncestorRef.Name { + return false + } + + if !equalPointers(p1.AncestorRef.Namespace, p2.AncestorRef.Namespace) { + return false + } + + // we ignore the rest of the AncestorRef fields because we do not set them + + return conditionsEqual(p1.Conditions, p2.Conditions) +} + func conditionsEqual(prev, cur []v1.Condition) bool { return slices.EqualFunc(prev, cur, func(c1, c2 v1.Condition) bool { if c1.ObservedGeneration != c2.ObservedGeneration { diff --git a/internal/framework/status/statuses.go b/internal/framework/status/statuses.go index 92102d6aea..4b2727418b 100644 --- a/internal/framework/status/statuses.go +++ b/internal/framework/status/statuses.go @@ -16,9 +16,10 @@ type Status interface { // GatewayAPIStatuses holds the status-related information about Gateway API resources. type GatewayAPIStatuses struct { - GatewayClassStatuses GatewayClassStatuses - GatewayStatuses GatewayStatuses - HTTPRouteStatuses HTTPRouteStatuses + GatewayClassStatuses GatewayClassStatuses + GatewayStatuses GatewayStatuses + HTTPRouteStatuses HTTPRouteStatuses + BackendTLSPolicyStatuses BackendTLSPolicyStatuses } func (g GatewayAPIStatuses) APIGroup() string { @@ -51,6 +52,10 @@ type GatewayStatuses map[types.NamespacedName]GatewayStatus // GatewayClassStatuses holds the statuses of GatewayClasses where the key is the namespaced name of a GatewayClass. type GatewayClassStatuses map[types.NamespacedName]GatewayClassStatus +// BackendTLSPolicyStatuses holds the statuses of BackendTLSPolicies where the key is the namespaced name of a +// BackendTLSPolicy. +type BackendTLSPolicyStatuses map[types.NamespacedName]BackendTLSPolicyStatus + // GatewayStatus holds the status of the winning Gateway resource. type GatewayStatus struct { // ListenerStatuses holds the statuses of listeners defined on the Gateway. @@ -85,6 +90,14 @@ type HTTPRouteStatus struct { ObservedGeneration int64 } +// BackendTLSPolicyStatus holds the status-related information about a BackendTLSPolicy resource. +type BackendTLSPolicyStatus struct { + // AncestorStatuses holds the statuses for parentRefs of the BackendTLSPolicy. + AncestorStatuses []AncestorStatus + // ObservedGeneration is the generation of the resource that was processed. + ObservedGeneration int64 +} + // ParentStatus holds status-related information related to how the HTTPRoute binds to a specific parentRef. type ParentStatus struct { // GatewayNsName is the Namespaced name of the Gateway, which the parentRef references. @@ -102,3 +115,10 @@ type GatewayClassStatus struct { // ObservedGeneration is the generation of the resource that was processed. ObservedGeneration int64 } + +type AncestorStatus struct { + // GatewayNsName is the Namespaced name of the Gateway, which the ancestorRef references. + GatewayNsName types.NamespacedName + // Conditions is the list of conditions that are relevant to the ancestor. + Conditions []conditions.Condition +} diff --git a/internal/framework/status/updater.go b/internal/framework/status/updater.go index c2ff173ba0..8b8e451915 100644 --- a/internal/framework/status/updater.go +++ b/internal/framework/status/updater.go @@ -13,6 +13,7 @@ import ( "k8s.io/apimachinery/pkg/util/wait" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller" @@ -204,6 +205,21 @@ func (upd *UpdaterImpl) updateGatewayAPI(ctx context.Context, statuses GatewayAP newHTTPRouteStatusSetter(upd.cfg.GatewayCtlrName, upd.cfg.Clock, rs), ) } + + for nsname, bs := range statuses.BackendTLSPolicyStatuses { + select { + case <-ctx.Done(): + return + default: + } + + upd.writeStatuses( + ctx, + nsname, + &v1alpha2.BackendTLSPolicy{}, + newBackendTLSPolicyStatusSetter(upd.cfg.GatewayCtlrName, upd.cfg.Clock, bs), + ) + } } func (upd *UpdaterImpl) writeStatuses( diff --git a/internal/mode/static/build_statuses.go b/internal/mode/static/build_statuses.go index 9527bf145b..a252b84b28 100644 --- a/internal/mode/static/build_statuses.go +++ b/internal/mode/static/build_statuses.go @@ -29,6 +29,8 @@ func buildGatewayAPIStatuses( statuses.GatewayStatuses = buildGatewayStatuses(graph.Gateway, graph.IgnoredGateways, gwAddresses, nginxReloadRes) + statuses.BackendTLSPolicyStatuses = buildBackendTLSPolicyStatuses(graph.BackendTLSPolicies) + for nsname, r := range graph.Routes { parentStatuses := make([]status.ParentStatus, 0, len(r.ParentRefs)) @@ -190,3 +192,33 @@ func buildGatewayStatus( ObservedGeneration: gateway.Source.Generation, } } + +func buildBackendTLSPolicyStatuses(backendTLSPolicies map[types.NamespacedName]*graph.BackendTLSPolicy, +) status.BackendTLSPolicyStatuses { + statuses := make(status.BackendTLSPolicyStatuses, len(backendTLSPolicies)) + ignoreStatus := false + + for nsname, backendTLSPolicy := range backendTLSPolicies { + if backendTLSPolicy.IsReferenced { + if !backendTLSPolicy.Valid { + for i := range backendTLSPolicy.Conditions { + if backendTLSPolicy.Conditions[i].Reason == string(staticConds.BackendTLSPolicyReasonIgnored) { + // We should not report the status of an ignored BackendTLSPolicy. + ignoreStatus = true + } + } + } + if !ignoreStatus { + statuses[nsname] = status.BackendTLSPolicyStatus{ + AncestorStatuses: []status.AncestorStatus{ + { + GatewayNsName: backendTLSPolicy.Gateway, + Conditions: conditions.DeduplicateConditions(backendTLSPolicy.Conditions), + }, + }, + } + } + } + } + return statuses +} diff --git a/internal/mode/static/build_statuses_test.go b/internal/mode/static/build_statuses_test.go index c3ee37cfbc..46defb8fad 100644 --- a/internal/mode/static/build_statuses_test.go +++ b/internal/mode/static/build_statuses_test.go @@ -202,6 +202,7 @@ func TestBuildStatuses(t *testing.T) { }, }, }, + BackendTLSPolicyStatuses: status.BackendTLSPolicyStatuses{}, } g := NewWithT(t) @@ -304,6 +305,7 @@ func TestBuildStatusesNginxErr(t *testing.T) { }, }, }, + BackendTLSPolicyStatuses: status.BackendTLSPolicyStatuses{}, } g := NewWithT(t) diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go index 25ade0b204..c1205b375f 100644 --- a/internal/mode/static/state/conditions/conditions.go +++ b/internal/mode/static/state/conditions/conditions.go @@ -10,6 +10,14 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" ) +// BackendTLSPolicyConditionType is a type of condition associated with a BackendTLSPolicy. +// This type should be used with the BackendTLSPolicyStatus.Conditions field. +type BackendTLSPolicyConditionType string + +// BackendTLSPolicyConditionReason defines the set of reasons that explain why a particular BackendTLSPolicy condition +// type has been raised. +type BackendTLSPolicyConditionReason string + const ( // ListenerReasonUnsupportedValue is used with the "Accepted" condition when a value of a field in a Listener // is invalid or not supported. @@ -57,6 +65,22 @@ const ( RouteMessageFailedNginxReload = GatewayMessageFailedNginxReload + ". NGINX may still be configured " + "for this HTTPRoute. However, future updates to this resource will not be configured until the Gateway " + "is programmed again" + + // BackendTLSPolicyConditionAttached is the condition type indicating the BackendTLS policy is valid and attached to + // the Gateway. + BackendTLSPolicyConditionAttached BackendTLSPolicyConditionType = "Attached" + + // BackendTLSPolicyReasonAttached is the condition reason for the BackendTLSPolicy Attached condition. + BackendTLSPolicyReasonAttached BackendTLSPolicyConditionReason = "BackendTLSPolicyAttached" + + // BackendTLSPolicyReasonIgnored is the condition reason for the BackendTLSPolicy being ignored by this Gateway. + BackendTLSPolicyReasonIgnored BackendTLSPolicyConditionReason = "BackendTLSPolicyIgnored" + + // BackendTLSPolicyConditionValid is the condition type indicating whether the BackendTLS policy is valid. + BackendTLSPolicyConditionValid BackendTLSPolicyConditionType = "Valid" + + // BackendTLSPolicyReasonInvalid is the condition reason for the BackendTLSPolicy Valid condition being False. + BackendTLSPolicyReasonInvalid BackendTLSPolicyConditionReason = "BackendTLSPolicyInvalid" ) // NewTODO returns a Condition that can be used as a placeholder for a condition that is not yet implemented. @@ -545,3 +569,35 @@ func NewNginxGatewayInvalid(msg string) conditions.Condition { Message: msg, } } + +// NewBackendTLSPolicyAttached returns a Condition that indicates that the BackendTLSPolicy config is valid and attached +// to the Gateway. +func NewBackendTLSPolicyAttached() conditions.Condition { + return conditions.Condition{ + Type: string(BackendTLSPolicyConditionAttached), + Status: metav1.ConditionTrue, + Reason: string(BackendTLSPolicyReasonAttached), + Message: "BackendTLSPolicy is attached to the Gateway", + } +} + +// NewBackendTLSPolicyIgnored returns a Condition that indicates that the BackendTLSPolicy config cannot be attached to +// the Gateway and will be ignored. +func NewBackendTLSPolicyIgnored(msg string) conditions.Condition { + return conditions.Condition{ + Type: string(BackendTLSPolicyConditionAttached), + Status: metav1.ConditionFalse, + Reason: string(BackendTLSPolicyReasonIgnored), + Message: msg, + } +} + +// NewBackendTLSPolicyInvalid returns a Condition that indicates that the BackendTLSPolicy config is invalid. +func NewBackendTLSPolicyInvalid(msg string) conditions.Condition { + return conditions.Condition{ + Type: string(BackendTLSPolicyConditionValid), + Status: metav1.ConditionFalse, + Reason: string(BackendTLSPolicyReasonInvalid), + Message: msg, + } +} diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index cd6b52e647..1a67907252 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -9,7 +9,6 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" - v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/graph" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" @@ -36,7 +35,7 @@ func BuildConfiguration( httpServers, sslServers := buildServers(g.Gateway.Listeners) backendGroups := buildBackendGroups(append(httpServers, sslServers...)) keyPairs := buildSSLKeyPairs(g.ReferencedSecrets, g.Gateway.Listeners) - certBundles := buildCertBundles(g.ReferencedConfigMaps, backendGroups) + certBundles := buildCertBundles(g.ReferencedCaCertConfigMaps, backendGroups) config := Configuration{ HTTPServers: httpServers, @@ -76,11 +75,11 @@ func buildSSLKeyPairs( } func buildCertBundles( - configMaps map[types.NamespacedName]*graph.ConfigMap, + caCertConfigMaps map[types.NamespacedName]*graph.CaCertConfigMap, backendGroups []BackendGroup, ) map[CertBundleID]CertBundle { bundles := make(map[CertBundleID]CertBundle) - refByBG := make(map[CertBundleID]bool) + refByBG := make(map[CertBundleID]struct{}) // We only need to build the cert bundles if there are valid backend groups that reference them. if len(backendGroups) == 0 { @@ -94,19 +93,16 @@ func buildCertBundles( if !b.Valid || b.VerifyTLS == nil { continue } - refByBG[b.VerifyTLS.CertBundleID] = true + refByBG[b.VerifyTLS.CertBundleID] = struct{}{} } } - for cmName, cm := range configMaps { + for cmName, cm := range caCertConfigMaps { id := generateCertBundleID(cmName) - if !refByBG[id] { - continue - } - if cm.Source.Data != nil || len(cm.Source.Data) > 0 { - bundles[id] = CertBundle(cm.Source.Data["ca.crt"]) - } else if cm.Source.BinaryData != nil || len(cm.Source.BinaryData) > 0 { - bundles[id] = CertBundle(cm.Source.BinaryData["ca.crt"]) + if _, exists := refByBG[id]; exists { + if cm.CACert != nil || len(cm.CACert) > 0 { + bundles[id] = CertBundle(cm.CACert) + } } } @@ -174,17 +170,15 @@ func newBackendGroup(refs []graph.BackendRef, sourceNsName types.NamespacedName, } } -func convertBackendTLS(btp *v1alpha2.BackendTLSPolicy) *VerifyTLS { - if btp == nil { +func convertBackendTLS(btp *graph.BackendTLSPolicy) *VerifyTLS { + if btp == nil || !btp.Valid { return nil } verify := &VerifyTLS{} - if btp.Spec.TLS.CACertRefs != nil && len(btp.Spec.TLS.CACertRefs) > 0 { - // We only support one CACertRef, take the first one and ignore anything else - b := btp.Spec.TLS.CACertRefs[0] - verify.CertBundleID = generateCertBundleID(types.NamespacedName{Namespace: btp.Namespace, Name: string(b.Name)}) + if btp.CaCertRef.Name != "" { + verify.CertBundleID = generateCertBundleID(btp.CaCertRef) } - verify.Hostname = string(btp.Spec.TLS.Hostname) + verify.Hostname = string(btp.Source.Spec.TLS.Hostname) return verify } diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index 4115798dd2..c49eb341a6 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -321,31 +321,35 @@ func TestBuildConfiguration(t *testing.T) { pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/", pathType: prefix}, ) - httpsRouteHR8.Rules[0].BackendRefs[0].BackendTLSPolicy = &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "btp", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "foo", - Namespace: (*v1.Namespace)(helpers.GetPointer("test")), - }, - }, - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "foo.example.com", - CACertRefs: []v1.LocalObjectReference{ - { - Kind: "ConfigMap", - Name: "configmap-1", - Group: "", + httpsRouteHR8.Rules[0].BackendRefs[0].BackendTLSPolicy = &graph.BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "foo", + Namespace: (*v1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []v1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap-1", + Group: "", + }, }, }, }, }, + CaCertRef: types.NamespacedName{Namespace: "test", Name: "configmap-1"}, + Valid: true, } expHTTPSHR8Groups[0].Backends[0].VerifyTLS = &VerifyTLS{ @@ -360,31 +364,35 @@ func TestBuildConfiguration(t *testing.T) { pathAndType{path: "/", pathType: prefix}, pathAndType{path: "/", pathType: prefix}, ) - httpsRouteHR9.Rules[0].BackendRefs[0].BackendTLSPolicy = &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "btp2", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "foo", - Namespace: (*v1.Namespace)(helpers.GetPointer("test")), - }, + httpsRouteHR9.Rules[0].BackendRefs[0].BackendTLSPolicy = &graph.BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp2", + Namespace: "test", }, - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "foo.example.com", - CACertRefs: []v1.LocalObjectReference{ - { - Kind: "ConfigMap", - Name: "configmap-2", - Group: "", + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "foo", + Namespace: (*v1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []v1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap-2", + Group: "", + }, }, }, }, }, + CaCertRef: types.NamespacedName{Namespace: "test", Name: "configmap-2"}, + Valid: true, } expHTTPSHR9Groups[0].Backends[0].VerifyTLS = &VerifyTLS{ @@ -504,7 +512,7 @@ func TestBuildConfiguration(t *testing.T) { }, } - referencedConfigMaps := map[types.NamespacedName]*graph.ConfigMap{ + referencedConfigMaps := map[types.NamespacedName]*graph.CaCertConfigMap{ {Namespace: "test", Name: "configmap-1"}: { Source: &apiv1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ @@ -515,6 +523,7 @@ func TestBuildConfiguration(t *testing.T) { "ca.crt": "cert-1", }, }, + CACert: []byte("cert-1"), }, {Namespace: "test", Name: "configmap-2"}: { Source: &apiv1.ConfigMap{ @@ -526,6 +535,7 @@ func TestBuildConfiguration(t *testing.T) { "ca.crt": []byte("cert-2"), }, }, + CACert: []byte("cert-2"), }, } @@ -1722,7 +1732,7 @@ func TestBuildConfiguration(t *testing.T) { ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, }, - ReferencedConfigMaps: referencedConfigMaps, + ReferencedCaCertConfigMaps: referencedConfigMaps, }, expConf: Configuration{ HTTPServers: []VirtualServer{}, @@ -1798,7 +1808,7 @@ func TestBuildConfiguration(t *testing.T) { ReferencedSecrets: map[types.NamespacedName]*graph.Secret{ secret1NsName: secret1, }, - ReferencedConfigMaps: referencedConfigMaps, + ReferencedCaCertConfigMaps: referencedConfigMaps, }, expConf: Configuration{ HTTPServers: []VirtualServer{}, @@ -2427,30 +2437,39 @@ func TestHostnameMoreSpecific(t *testing.T) { } func TestConvertBackendTLS(t *testing.T) { - btpCaCertRefs := &v1alpha2.BackendTLSPolicy{ - Spec: v1alpha2.BackendTLSPolicySpec{ - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: []v1.LocalObjectReference{ - { - Name: "ca-cert", + btpCaCertRefs := &graph.BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + Spec: v1alpha2.BackendTLSPolicySpec{ + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []v1.LocalObjectReference{ + { + Name: "ca-cert", + }, }, + Hostname: "example.com", }, - Hostname: "example.com", }, }, + Valid: true, + CaCertRef: types.NamespacedName{Namespace: "test", Name: "ca-cert"}, } - btpWellKnownCerts := &v1alpha2.BackendTLSPolicy{ - Spec: v1alpha2.BackendTLSPolicySpec{ - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "example.com", + btpWellKnownCerts := &graph.BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + Spec: v1alpha2.BackendTLSPolicySpec{ + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "example.com", + }, }, }, + Valid: true, } expectedWithCertPath := &VerifyTLS{ - CertBundleID: generateCertBundleID(types.NamespacedName{Namespace: btpCaCertRefs.Namespace, Name: "ca-cert"}), - Hostname: "example.com", + CertBundleID: generateCertBundleID( + types.NamespacedName{Namespace: "test", Name: "ca-cert"}, + ), + Hostname: "example.com", } expectedWithWellKnownCerts := &VerifyTLS{ @@ -2458,7 +2477,7 @@ func TestConvertBackendTLS(t *testing.T) { } tests := []struct { - btp *v1alpha2.BackendTLSPolicy + btp *graph.BackendTLSPolicy expected *VerifyTLS msg string }{ diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 11a6ccd6d4..e0d4bd2a61 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -7,7 +7,6 @@ import ( "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" - gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" @@ -16,7 +15,7 @@ import ( // BackendRef is an internal representation of a backendRef in an HTTPRoute. type BackendRef struct { // BackendTLSPolicy is the BackendTLSPolicy of the Service which is referenced by the backendRef. - BackendTLSPolicy *gatewayv1alpha2.BackendTLSPolicy + BackendTLSPolicy *BackendTLSPolicy // SvcNsName is the NamespacedName of the Service referenced by the backendRef. SvcNsName types.NamespacedName // ServicePort is the ServicePort of the Service which is referenced by the backendRef. @@ -40,11 +39,10 @@ func addBackendRefsToRouteRules( routes map[types.NamespacedName]*Route, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, - backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, - configMapResolver *configMapResolver, + backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, ) { for _, r := range routes { - addBackendRefsToRules(r, refGrantResolver, services, backendTLSPolicies, configMapResolver) + addBackendRefsToRules(r, refGrantResolver, services, backendTLSPolicies) } } @@ -55,8 +53,7 @@ func addBackendRefsToRules( route *Route, refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, - backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, - configMapResolver *configMapResolver, + backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, ) { if !route.Valid { return @@ -87,7 +84,6 @@ func addBackendRefsToRules( services, refPath, backendTLSPolicies, - configMapResolver, ) backendRefs = append(backendRefs, ref) @@ -106,8 +102,7 @@ func createBackendRef( refGrantResolver *referenceGrantResolver, services map[types.NamespacedName]*v1.Service, refPath *field.Path, - backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, - configMapResolver *configMapResolver, + backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, ) (BackendRef, *conditions.Condition) { // Data plane will handle invalid ref by responding with 500. // Because of that, we always need to add a BackendRef to group.Backends, even if the ref is invalid. @@ -151,7 +146,6 @@ func createBackendRef( backendTLSPolicies, ref, sourceNamespace, - configMapResolver, ) if err != nil { backendRef = BackendRef{ @@ -177,95 +171,41 @@ func createBackendRef( } func findBackendTLSPolicyForService( - backendTLSPolicies map[types.NamespacedName]*gatewayv1alpha2.BackendTLSPolicy, + backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, ref gatewayv1.HTTPBackendRef, routeNamespace string, - configMapResolver *configMapResolver, -) (*gatewayv1alpha2.BackendTLSPolicy, error) { - var beTLSPolicy *gatewayv1alpha2.BackendTLSPolicy +) (*BackendTLSPolicy, error) { + var beTLSPolicy *BackendTLSPolicy + var err error + refNs := routeNamespace if ref.Namespace != nil { refNs = string(*ref.Namespace) } for _, btp := range backendTLSPolicies { - btpNs := btp.Namespace - if btp.Spec.TargetRef.Namespace != nil { - btpNs = string(*btp.Spec.TargetRef.Namespace) + btpNs := btp.Source.Namespace + if btp.Source.Spec.TargetRef.Namespace != nil { + btpNs = string(*btp.Source.Spec.TargetRef.Namespace) } - if btp.Spec.TargetRef.Name == ref.Name && btpNs == refNs { + if btp.Source.Spec.TargetRef.Name == ref.Name && btpNs == refNs { beTLSPolicy = btp break } } - if beTLSPolicy == nil { - return nil, nil + if beTLSPolicy != nil { + beTLSPolicy.IsReferenced = true + if !beTLSPolicy.Valid { + err = fmt.Errorf("The backend TLS policy is invalid: %s", beTLSPolicy.Conditions[0].Message) + } else { + beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewBackendTLSPolicyAttached()) + } } - err := validateBackendTLSPolicy(beTLSPolicy, configMapResolver) - return beTLSPolicy, err } -func validateBackendTLSPolicy( - btp *gatewayv1alpha2.BackendTLSPolicy, - configMapResolver *configMapResolver, -) error { - if err := validateBackendTLSHostname(btp); err != nil { - return err - } - if btp.Spec.TLS.CACertRefs != nil && len(btp.Spec.TLS.CACertRefs) > 0 { - return validateBackendTLSCACertRef(btp, configMapResolver) - } else if btp.Spec.TLS.WellKnownCACerts != nil { - return validateBackendTLSWellKnownCACerts(btp) - } - return fmt.Errorf("must specify either CACertRefs or WellKnownCACerts") -} - -func validateBackendTLSHostname(btp *gatewayv1alpha2.BackendTLSPolicy) error { - h := string(btp.Spec.TLS.Hostname) - - if err := validateHostname(h); err != nil { - path := field.NewPath("tls.hostname") - valErr := field.Invalid(path, btp.Spec.TLS.Hostname, err.Error()) - return valErr - } - return nil -} - -func validateBackendTLSCACertRef(btp *gatewayv1alpha2.BackendTLSPolicy, configMapResolver *configMapResolver) error { - if len(btp.Spec.TLS.CACertRefs) != 1 { - path := field.NewPath("tls.cacertrefs") - valErr := field.TooMany(path, len(btp.Spec.TLS.CACertRefs), 1) - return valErr - } - if btp.Spec.TLS.CACertRefs[0].Kind != "ConfigMap" { - path := field.NewPath("tls.cacertrefs[0].kind") - valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"ConfigMap"}) - return valErr - } - if btp.Spec.TLS.CACertRefs[0].Group != "" && btp.Spec.TLS.CACertRefs[0].Group != "core" { - path := field.NewPath("tls.cacertrefs[0].group") - valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"", "core"}) - return valErr - } - nsName := types.NamespacedName{Namespace: btp.Namespace, Name: string(btp.Spec.TLS.CACertRefs[0].Name)} - if err := configMapResolver.resolve(nsName); err != nil { - path := field.NewPath("tls.cacertrefs[0]") - return field.Invalid(path, btp.Spec.TLS.CACertRefs[0], err.Error()) - } - return nil -} - -func validateBackendTLSWellKnownCACerts(btp *gatewayv1alpha2.BackendTLSPolicy) error { - if *btp.Spec.TLS.WellKnownCACerts != gatewayv1alpha2.WellKnownCACertSystem { - path := field.NewPath("tls.wellknowncacerts") - return field.Invalid(path, btp.Spec.TLS.WellKnownCACerts, "unsupported value") - } - return nil -} - // getServiceAndPortFromRef extracts the NamespacedName of the Service and the port from a BackendRef. // It can return an error and an empty v1.ServicePort in two cases: // 1. The Service referenced from the BackendRef does not exist in the cluster/state. diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index 09f9bdb53d..83555c899a 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -422,7 +422,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { services := map[types.NamespacedName]*v1.Service{ {Namespace: "test", Name: "svc1"}: svc1, } - policies := map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{} + policies := map[types.NamespacedName]*BackendTLSPolicy{} tests := []struct { name string @@ -536,13 +536,11 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, } - configMapResolver := newConfigMapResolver(nil) - for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) - addBackendRefsToRules(test.route, resolver, services, policies, configMapResolver) + addBackendRefsToRules(test.route, resolver, services, policies) var actual []BackendRef if test.route.Rules != nil { @@ -578,43 +576,58 @@ func TestCreateBackend(t *testing.T) { svc2NamespacedName := types.NamespacedName{Namespace: "test", Name: "service2"} svc3NamespacedName := types.NamespacedName{Namespace: "test", Name: "service3"} - btp := &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "btp", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "service2", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), - }, + btp := BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp", + Namespace: "test", }, - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "foo.example.com", - WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "service2", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), + }, }, }, + Valid: true, } - btp2 := &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "btp2", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "service3", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + + btp2 := BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp2", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "service3", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertType("unknown"))), }, }, - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "foo.example.com", - WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertType("unknown"))), + }, + Valid: false, + Conditions: []conditions.Condition{ + { + // Type: conditions.Invalid, + // Status: conditions.ConditionFalse, + // Reason: staticConds.BackendTLSPolicyInvalidWellKnownCACerts, + Message: "unsupported value", }, }, } @@ -730,7 +743,7 @@ func TestCreateBackend(t *testing.T) { ServicePort: svc1.Spec.Ports[0], Weight: 5, Valid: true, - BackendTLSPolicy: btp, + BackendTLSPolicy: &btp, }, expectedServicePortReference: "test_service2_80", expectedCondition: nil, @@ -752,7 +765,7 @@ func TestCreateBackend(t *testing.T) { expectedServicePortReference: "", expectedCondition: helpers.GetPointer( staticConds.NewRouteBackendRefUnsupportedValue( - "tls.wellknowncacerts: Invalid value: \"unknown\": unsupported value", + "The backend TLS policy is invalid: unsupported value", ), ), name: "invalid policy", @@ -764,17 +777,15 @@ func TestCreateBackend(t *testing.T) { client.ObjectKeyFromObject(svc2): svc2, client.ObjectKeyFromObject(svc3): svc3, } - policies := map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{ - client.ObjectKeyFromObject(btp): btp, - client.ObjectKeyFromObject(btp2): btp2, + policies := map[types.NamespacedName]*BackendTLSPolicy{ + client.ObjectKeyFromObject(btp.Source): &btp, + client.ObjectKeyFromObject(btp2.Source): &btp2, } sourceNamespace := "test" refPath := field.NewPath("test") - configMapResolver := newConfigMapResolver(nil) - for _, test := range tests { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) @@ -787,7 +798,6 @@ func TestCreateBackend(t *testing.T) { services, refPath, policies, - configMapResolver, ) g.Expect(helpers.Diff(test.expectedBackend, backend)).To(BeEmpty()) @@ -828,242 +838,3 @@ func TestGetServicePort(t *testing.T) { g.Expect(err).Should(HaveOccurred()) g.Expect(port.Port).To(Equal(int32(0))) } - -func TestValidateBackendTLSPolicy(t *testing.T) { - targetRefNormalCase := &v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Kind: "Service", - Name: "service1", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), - }, - } - - localObjectRefNormalCase := []gatewayv1.LocalObjectReference{ - { - Kind: "ConfigMap", - Name: "configmap", - Group: "", - }, - } - - localObjectRefInvalidName := []gatewayv1.LocalObjectReference{ - { - Kind: "ConfigMap", - Name: "invalid", - Group: "", - }, - } - - localObjectRefInvalidKind := []gatewayv1.LocalObjectReference{ - { - Kind: "Secret", - Name: "secret", - Group: "", - }, - } - - localObjectRefInvalidGroup := []gatewayv1.LocalObjectReference{ - { - Kind: "ConfigMap", - Name: "configmap", - Group: "bhu", - }, - } - - localObjectRefTooManyCerts := append(localObjectRefNormalCase, localObjectRefInvalidName...) - - tests := []struct { - tlsPolicy *v1alpha2.BackendTLSPolicy - name string - expectError bool - }{ - { - name: "normal case with ca cert refs", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: localObjectRefNormalCase, - Hostname: "foo.test.com", - }, - }, - }, - expectError: false, - }, - { - name: "normal case with well known certs", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), - Hostname: "foo.test.com", - }, - }, - }, - expectError: false, - }, - { - name: "no hostname invalid case", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: localObjectRefNormalCase, - Hostname: "", - }, - }, - }, - expectError: true, - }, - { - name: "invalid ca cert ref name", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: localObjectRefInvalidName, - Hostname: "foo.test.com", - }, - }, - }, - expectError: true, - }, - { - name: "invalid ca cert ref kind", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: localObjectRefInvalidKind, - Hostname: "foo.test.com", - }, - }, - }, - expectError: true, - }, - { - name: "invalid ca cert ref group", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: localObjectRefInvalidGroup, - Hostname: "foo.test.com", - }, - }, - }, - expectError: true, - }, - { - name: "invalid case with well known certs", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertType("unknown"))), - Hostname: "foo.test.com", - }, - }, - }, - expectError: true, - }, - { - name: "invalid case neither TLS config option chosen", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "foo.test.com", - }, - }, - }, - expectError: true, - }, - { - name: "invalid case with too many ca cert refs", - tlsPolicy: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "tls-policy", - Namespace: "test", - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: *targetRefNormalCase, - TLS: v1alpha2.BackendTLSPolicyConfig{ - CACertRefs: localObjectRefTooManyCerts, - Hostname: "foo.test.com", - }, - }, - }, - expectError: true, - }, - } - - configMaps := map[types.NamespacedName]*v1.ConfigMap{ - {Namespace: "test", Name: "configmap"}: { - ObjectMeta: metav1.ObjectMeta{ - Name: "configmap", - Namespace: "test", - }, - Data: map[string]string{ - "ca.crt": caBlock, - }, - }, - {Namespace: "test", Name: "invalid"}: { - ObjectMeta: metav1.ObjectMeta{ - Name: "invalid", - Namespace: "test", - }, - Data: map[string]string{ - "ca.crt": "invalid", - }, - }, - } - configMapResolver := newConfigMapResolver(configMaps) - - for _, test := range tests { - t.Run(test.name, func(t *testing.T) { - g := NewWithT(t) - - err := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver) - - if test.expectError { - g.Expect(err).To(HaveOccurred()) - } else { - g.Expect(err).ToNot(HaveOccurred()) - } - }) - } -} diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go new file mode 100644 index 0000000000..da54b4c8e5 --- /dev/null +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -0,0 +1,149 @@ +package graph + +import ( + "fmt" + + "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/validation/field" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" +) + +type BackendTLSPolicy struct { + // Source is the source resource. + Source *v1alpha2.BackendTLSPolicy + // CaCartRef is the name of the ConfigMap that contains the CA certificate. + CaCertRef types.NamespacedName + // Gateway is the name of the Gateway that is being checked for this BackendTLSPolicy. + Gateway types.NamespacedName + // Conditions include Conditions for the BackendTLSPolicy. + Conditions []conditions.Condition + // Valid shows whether the BackendTLSPolicy is valid. + Valid bool + // IsReferenced shows whether the BackendTLSPolicy is referenced by a BackendRef. + IsReferenced bool +} + +func processBackendTLSPolicies( + backendTLSPolicies map[types.NamespacedName]*v1alpha2.BackendTLSPolicy, + configMapResolver *configMapResolver, + ctlrName string, + gateway *Gateway, +) map[types.NamespacedName]*BackendTLSPolicy { + if len(backendTLSPolicies) == 0 { + return nil + } + processedBackendTLSPolicies := make(map[types.NamespacedName]*BackendTLSPolicy, len(backendTLSPolicies)) + for nsname, backendTLSPolicy := range backendTLSPolicies { + processedBackendTLSPolicies[nsname] = &BackendTLSPolicy{ + Source: backendTLSPolicy, + } + valid, caCertRef, conds := validateBackendTLSPolicy(backendTLSPolicy, configMapResolver, ctlrName, gateway) + processedBackendTLSPolicies[nsname].Valid = valid + processedBackendTLSPolicies[nsname].Conditions = conds + processedBackendTLSPolicies[nsname].Gateway = types.NamespacedName{ + Namespace: gateway.Source.Namespace, + Name: gateway.Source.Name, + } + processedBackendTLSPolicies[nsname].CaCertRef = caCertRef + } + return processedBackendTLSPolicies +} + +func validateBackendTLSPolicy( + backendTLSPolicy *v1alpha2.BackendTLSPolicy, + configMapResolver *configMapResolver, + ctlrName string, + gateway *Gateway, +) (bool, types.NamespacedName, []conditions.Condition) { + conds := make([]conditions.Condition, 0) + valid := true + caCertRef := types.NamespacedName{} + if len(backendTLSPolicy.Status.Ancestors) >= 16 { + // check if we already are an ancestor on this policy. If we are, we are safe to continue. + ancestorRef := v1.ParentReference{ + Namespace: helpers.GetPointer((v1.Namespace)(gateway.Source.Namespace)), + Name: v1.ObjectName(gateway.Source.Name), + } + for _, ancestor := range backendTLSPolicy.Status.Ancestors { + if string(ancestor.ControllerName) == ctlrName && ancestor.AncestorRef.Name == ancestorRef.Name && + ancestor.AncestorRef.Namespace == ancestorRef.Namespace { + break + } + } + valid = false + conds = append(conds, staticConds.NewBackendTLSPolicyIgnored("too many ancestors, cannot attach a new Gateway")) + } + if err := validateBackendTLSHostname(backendTLSPolicy); err != nil { + valid = false + conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error()))) + } + if backendTLSPolicy.Spec.TLS.CACertRefs != nil && len(backendTLSPolicy.Spec.TLS.CACertRefs) > 0 { + if err := validateBackendTLSCACertRef(backendTLSPolicy, configMapResolver); err != nil { + valid = false + conds = append(conds, staticConds.NewBackendTLSPolicyInvalid( + fmt.Sprintf("invalid CACertRef: %s", err.Error()))) + } else { + caCertRef = types.NamespacedName{ + Namespace: backendTLSPolicy.Namespace, Name: string(backendTLSPolicy.Spec.TLS.CACertRefs[0].Name), + } + } + } else if backendTLSPolicy.Spec.TLS.WellKnownCACerts != nil { + if err := validateBackendTLSWellKnownCACerts(backendTLSPolicy); err != nil { + valid = false + conds = append(conds, staticConds.NewBackendTLSPolicyInvalid( + fmt.Sprintf("invalid WellKnownCACerts: %s", err.Error()))) + } + } else { + valid = false + conds = append(conds, staticConds.NewBackendTLSPolicyInvalid("CACertRefs and WellKnownCACerts are both nil")) + } + return valid, caCertRef, conds +} + +func validateBackendTLSHostname(btp *v1alpha2.BackendTLSPolicy) error { + h := string(btp.Spec.TLS.Hostname) + + if err := validateHostname(h); err != nil { + path := field.NewPath("tls.hostname") + valErr := field.Invalid(path, btp.Spec.TLS.Hostname, err.Error()) + return valErr + } + return nil +} + +func validateBackendTLSCACertRef(btp *v1alpha2.BackendTLSPolicy, configMapResolver *configMapResolver) error { + if len(btp.Spec.TLS.CACertRefs) != 1 { + path := field.NewPath("tls.cacertrefs") + valErr := field.TooMany(path, len(btp.Spec.TLS.CACertRefs), 1) + return valErr + } + if btp.Spec.TLS.CACertRefs[0].Kind != "ConfigMap" { + path := field.NewPath("tls.cacertrefs[0].kind") + valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"ConfigMap"}) + return valErr + } + if btp.Spec.TLS.CACertRefs[0].Group != "" && btp.Spec.TLS.CACertRefs[0].Group != "core" { + path := field.NewPath("tls.cacertrefs[0].group") + valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"", "core"}) + return valErr + } + nsName := types.NamespacedName{Namespace: btp.Namespace, Name: string(btp.Spec.TLS.CACertRefs[0].Name)} + if err := configMapResolver.resolve(nsName); err != nil { + path := field.NewPath("tls.cacertrefs[0]") + return field.Invalid(path, btp.Spec.TLS.CACertRefs[0], err.Error()) + } + return nil +} + +func validateBackendTLSWellKnownCACerts(btp *v1alpha2.BackendTLSPolicy) error { + if *btp.Spec.TLS.WellKnownCACerts != v1alpha2.WellKnownCACertSystem { + path := field.NewPath("tls.wellknowncacerts") + return field.Invalid(path, btp.Spec.TLS.WellKnownCACerts, "unsupported value") + } + return nil +} diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go new file mode 100644 index 0000000000..587d2f81f2 --- /dev/null +++ b/internal/mode/static/state/graph/backend_tls_policy_test.go @@ -0,0 +1,261 @@ +package graph + +import ( + "testing" + + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" +) + +func TestValidateBackendTLSPolicy(t *testing.T) { + targetRefNormalCase := &v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "service1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + } + + localObjectRefNormalCase := []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "", + }, + } + + localObjectRefInvalidName := []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "invalid", + Group: "", + }, + } + + localObjectRefInvalidKind := []gatewayv1.LocalObjectReference{ + { + Kind: "Secret", + Name: "secret", + Group: "", + }, + } + + localObjectRefInvalidGroup := []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "bhu", + }, + } + + localObjectRefTooManyCerts := append(localObjectRefNormalCase, localObjectRefInvalidName...) + + // TODO: add test for too many ancestors + + tests := []struct { + tlsPolicy *v1alpha2.BackendTLSPolicy + caCertName types.NamespacedName + name string + isValid bool + }{ + { + name: "normal case with ca cert refs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "foo.test.com", + }, + }, + }, + isValid: true, + caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + }, + { + name: "normal case with well known certs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), + Hostname: "foo.test.com", + }, + }, + }, + isValid: true, + }, + { + name: "no hostname invalid case", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "", + }, + }, + }, + isValid: false, + caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + }, + { + name: "invalid ca cert ref name", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefInvalidName, + Hostname: "foo.test.com", + }, + }, + }, + isValid: false, + }, + { + name: "invalid ca cert ref kind", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefInvalidKind, + Hostname: "foo.test.com", + }, + }, + }, + isValid: false, + }, + { + name: "invalid ca cert ref group", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefInvalidGroup, + Hostname: "foo.test.com", + }, + }, + }, + isValid: false, + }, + { + name: "invalid case with well known certs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertType("unknown"))), + Hostname: "foo.test.com", + }, + }, + }, + isValid: false, + }, + { + name: "invalid case neither TLS config option chosen", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.test.com", + }, + }, + }, + isValid: false, + }, + { + name: "invalid case with too many ca cert refs", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefTooManyCerts, + Hostname: "foo.test.com", + }, + }, + }, + isValid: false, + }, + } + + configMaps := map[types.NamespacedName]*v1.ConfigMap{ + {Namespace: "test", Name: "configmap"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "configmap", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": caBlock, + }, + }, + {Namespace: "test", Name: "invalid"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "invalid", + Namespace: "test", + }, + Data: map[string]string{ + "ca.crt": "invalid", + }, + }, + } + + configMapResolver := newConfigMapResolver(configMaps) + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + valid, caCertName, conds := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver, "test", &Gateway{}) + + g.Expect(valid).To(Equal(test.isValid)) + g.Expect(caCertName).To(Equal(test.caCertName)) + if !test.isValid { + g.Expect(conds).To(HaveLen(1)) + } else { + g.Expect(conds).To(HaveLen(0)) + } + }) + } +} diff --git a/internal/mode/static/state/graph/config_maps.go b/internal/mode/static/state/graph/config_maps.go index 555b4c91a6..7aad12a364 100644 --- a/internal/mode/static/state/graph/config_maps.go +++ b/internal/mode/static/state/graph/config_maps.go @@ -13,29 +13,31 @@ import ( const CAKey = "ca.crt" -// ConfigMap represents a ConfigMap resource. -type ConfigMap struct { +// CaCertConfigMap represents a ConfigMap resource that holds CA Cert data. +type CaCertConfigMap struct { // Source holds the actual ConfigMap resource. Can be nil if the ConfigMap does not exist. Source *apiv1.ConfigMap + // CACert holds the actual CA Cert data. + CACert []byte } -type configMapEntry struct { - ConfigMap +type caCertConfigMapEntry struct { // err holds the corresponding error if the ConfigMap is invalid or does not exist. - err error + err error + caCertConfigMap CaCertConfigMap } // configMapResolver wraps the cluster ConfigMaps so that they can be resolved (includes validation). All resolved // ConfigMaps are saved to be used later. type configMapResolver struct { clusterConfigMaps map[types.NamespacedName]*apiv1.ConfigMap - resolvedConfigMaps map[types.NamespacedName]*configMapEntry + resolvedConfigMaps map[types.NamespacedName]*caCertConfigMapEntry } func newConfigMapResolver(configMaps map[types.NamespacedName]*apiv1.ConfigMap) *configMapResolver { return &configMapResolver{ clusterConfigMaps: configMaps, - resolvedConfigMaps: make(map[types.NamespacedName]*configMapEntry), + resolvedConfigMaps: make(map[types.NamespacedName]*caCertConfigMapEntry), } } @@ -47,37 +49,34 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { cm, exist := r.clusterConfigMaps[nsname] var validationErr error + var caCert []byte if !exist { validationErr = errors.New("configMap does not exist") } if exist { - - var caCrtPresent bool - if cm.Data != nil { if _, exists := cm.Data[CAKey]; exists { validationErr = validateCA([]byte(cm.Data[CAKey])) - caCrtPresent = true + caCert = []byte(cm.Data[CAKey]) } } - if cm.BinaryData != nil { if _, exists := cm.BinaryData[CAKey]; exists { validationErr = validateCA(cm.BinaryData[CAKey]) - caCrtPresent = true + caCert = cm.BinaryData[CAKey] } } - - if !caCrtPresent { + if len(caCert) == 0 { validationErr = fmt.Errorf("configMap does not have the data or binaryData field %v", CAKey) } } - r.resolvedConfigMaps[nsname] = &configMapEntry{ - ConfigMap: ConfigMap{ + r.resolvedConfigMaps[nsname] = &caCertConfigMapEntry{ + caCertConfigMap: CaCertConfigMap{ Source: cm, + CACert: caCert, }, err: validationErr, } @@ -85,17 +84,17 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { return validationErr } -func (r *configMapResolver) getResolvedConfigMaps() map[types.NamespacedName]*ConfigMap { +func (r *configMapResolver) getResolvedConfigMaps() map[types.NamespacedName]*CaCertConfigMap { if len(r.resolvedConfigMaps) == 0 { return nil } - resolved := make(map[types.NamespacedName]*ConfigMap) + resolved := make(map[types.NamespacedName]*CaCertConfigMap) for nsname, entry := range r.resolvedConfigMaps { // create iteration variable inside the loop to fix implicit memory aliasing - configMap := entry.ConfigMap - resolved[nsname] = &configMap + caCertConfigMap := entry.caCertConfigMap + resolved[nsname] = &caCertConfigMap } return resolved diff --git a/internal/mode/static/state/graph/graph.go b/internal/mode/static/state/graph/graph.go index 7f6c883303..f6ee5598b9 100644 --- a/internal/mode/static/state/graph/graph.go +++ b/internal/mode/static/state/graph/graph.go @@ -54,8 +54,10 @@ type Graph struct { // ReferencedServices includes the NamespacedNames of all the Services that are referenced by at least one HTTPRoute. // Storing the whole resource is not necessary, compared to the similar maps above. ReferencedServices map[types.NamespacedName]struct{} - // ReferencedConfigMaps includes ConfigMaps that have been referenced by any BackendTLSPolicies. - ReferencedConfigMaps map[types.NamespacedName]*ConfigMap + // ReferencedCaCertConfigMaps includes ConfigMaps that have been referenced by any BackendTLSPolicies. + ReferencedCaCertConfigMaps map[types.NamespacedName]*CaCertConfigMap + // BackendTLSPolicies holds BackendTLSPolicy resources. + BackendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy } // ProtectedPorts are the ports that may not be configured by a listener with a descriptive name of each port. @@ -68,7 +70,7 @@ func (g *Graph) IsReferenced(resourceType client.Object, nsname types.Namespaced _, exists := g.ReferencedSecrets[nsname] return exists case *v1.ConfigMap: - _, exists := g.ReferencedConfigMaps[nsname] + _, exists := g.ReferencedCaCertConfigMaps[nsname] return exists case *v1.Namespace: // `existed` is needed as it checks the graph's ReferencedNamespaces which stores all the namespaces that @@ -126,24 +128,32 @@ func BuildGraph( refGrantResolver := newReferenceGrantResolver(state.ReferenceGrants) gw := buildGateway(processedGws.Winner, secretResolver, gc, refGrantResolver, protectedPorts) + processedBackendTLSPolicies := processBackendTLSPolicies( + state.BackendTLSPolicies, + configMapResolver, + controllerName, + gw, + ) + routes := buildRoutesForGateways(validators.HTTPFieldsValidator, state.HTTPRoutes, processedGws.GetAllNsNames()) bindRoutesToListeners(routes, gw, state.Namespaces) - addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, state.BackendTLSPolicies, configMapResolver) + addBackendRefsToRouteRules(routes, refGrantResolver, state.Services, processedBackendTLSPolicies) referencedNamespaces := buildReferencedNamespaces(state.Namespaces, gw) referencedServices := buildReferencedServices(routes) g := &Graph{ - GatewayClass: gc, - Gateway: gw, - Routes: routes, - IgnoredGatewayClasses: processedGwClasses.Ignored, - IgnoredGateways: processedGws.Ignored, - ReferencedSecrets: secretResolver.getResolvedSecrets(), - ReferencedNamespaces: referencedNamespaces, - ReferencedServices: referencedServices, - ReferencedConfigMaps: configMapResolver.getResolvedConfigMaps(), + GatewayClass: gc, + Gateway: gw, + Routes: routes, + IgnoredGatewayClasses: processedGwClasses.Ignored, + IgnoredGateways: processedGws.Ignored, + ReferencedSecrets: secretResolver.getResolvedSecrets(), + ReferencedNamespaces: referencedNamespaces, + ReferencedServices: referencedServices, + ReferencedCaCertConfigMaps: configMapResolver.getResolvedConfigMaps(), + BackendTLSPolicies: processedBackendTLSPolicies, } return g diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go index 726cf845aa..35ed73fdf3 100644 --- a/internal/mode/static/state/graph/graph_test.go +++ b/internal/mode/static/state/graph/graph_test.go @@ -15,8 +15,10 @@ import ( "sigs.k8s.io/gateway-api/apis/v1alpha2" "sigs.k8s.io/gateway-api/apis/v1beta1" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/controller/index" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/validation/validationfakes" ) @@ -91,31 +93,53 @@ func TestBuildGraph(t *testing.T) { hr2 := createRoute("hr-2", "wrong-gateway", "listener-80-1") hr3 := createRoute("hr-3", "gateway-1", "listener-443-1") // https listener; should not conflict with hr1 - btp := &v1alpha2.BackendTLSPolicy{ + cm := &v1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ - Name: "btp", + Name: "configmap", Namespace: "service", }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "foo", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("service")), + Data: map[string]string{ + "ca.crt": caBlock, + }, + } + + btpAttachedConds := []conditions.Condition{ + staticConds.NewBackendTLSPolicyAttached(), + staticConds.NewBackendTLSPolicyAttached(), + } + + btp := BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp", + Namespace: "service", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "foo", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("service")), + }, }, - }, - TLS: v1alpha2.BackendTLSPolicyConfig{ - Hostname: "foo.example.com", - CACertRefs: []v1alpha2.LocalObjectReference{ - { - Kind: "ConfigMap", - Name: "configmap", - Group: "", + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []v1alpha2.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "", + }, }, }, }, }, + Valid: true, + IsReferenced: true, + Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, + Conditions: btpAttachedConds, + CaCertRef: types.NamespacedName{Namespace: "service", Name: "configmap"}, } hr1Refs := []BackendRef{ @@ -124,17 +148,7 @@ func TestBuildGraph(t *testing.T) { ServicePort: v1.ServicePort{Port: 80}, Valid: true, Weight: 1, - BackendTLSPolicy: btp, - }, - } - - cm := &v1.ConfigMap{ - ObjectMeta: metav1.ObjectMeta{ - Name: "configmap", - Namespace: "service", - }, - Data: map[string]string{ - "ca.crt": caBlock, + BackendTLSPolicy: &btp, }, } @@ -144,7 +158,7 @@ func TestBuildGraph(t *testing.T) { ServicePort: v1.ServicePort{Port: 80}, Valid: true, Weight: 1, - BackendTLSPolicy: btp, + BackendTLSPolicy: &btp, }, } @@ -302,7 +316,7 @@ func TestBuildGraph(t *testing.T) { client.ObjectKeyFromObject(secret): secret, }, BackendTLSPolicies: map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{ - client.ObjectKeyFromObject(btp): btp, + client.ObjectKeyFromObject(btp.Source): btp.Source, }, ConfigMaps: map[types.NamespacedName]*v1.ConfigMap{ client.ObjectKeyFromObject(cm): cm, @@ -396,11 +410,15 @@ func TestBuildGraph(t *testing.T) { ReferencedServices: map[types.NamespacedName]struct{}{ client.ObjectKeyFromObject(svc): {}, }, - ReferencedConfigMaps: map[types.NamespacedName]*ConfigMap{ + ReferencedCaCertConfigMaps: map[types.NamespacedName]*CaCertConfigMap{ client.ObjectKeyFromObject(cm): { Source: cm, + CACert: []byte(caBlock), }, }, + BackendTLSPolicies: map[types.NamespacedName]*BackendTLSPolicy{ + client.ObjectKeyFromObject(btp.Source): &btp, + }, } } @@ -577,9 +595,10 @@ func TestIsReferenced(t *testing.T) { ReferencedServices: map[types.NamespacedName]struct{}{ client.ObjectKeyFromObject(serviceInGraph): {}, }, - ReferencedConfigMaps: map[types.NamespacedName]*ConfigMap{ + ReferencedCaCertConfigMaps: map[types.NamespacedName]*CaCertConfigMap{ client.ObjectKeyFromObject(baseConfigMap): { Source: baseConfigMap, + CACert: []byte(caBlock), }, }, } From a2ae465edf6e3493f6a97442bd5b39232c2c7860 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Mon, 22 Jan 2024 17:19:08 +0000 Subject: [PATCH 03/13] Review feedback 1 --- cmd/gateway/commands.go | 8 ++-- examples/backend-tls/README.md | 24 ++++-------- examples/backend-tls/app-secret.yaml | 8 ---- .../backend-tls/backend-certs-configmap.yaml | 2 +- examples/backend-tls/ca.cert | 21 ---------- examples/backend-tls/gateway.yaml | 8 ---- examples/backend-tls/policy.yaml | 3 +- examples/backend-tls/secure-app-routes.yaml | 19 +-------- examples/backend-tls/secure-app.yaml | 4 +- internal/mode/static/config/config.go | 4 +- internal/mode/static/manager.go | 6 +-- .../static/nginx/config/generator_test.go | 2 + .../mode/static/nginx/config/http/config.go | 5 +-- internal/mode/static/nginx/config/servers.go | 39 +++++++++++++++---- .../static/nginx/config/servers_template.go | 7 +--- .../mode/static/nginx/config/servers_test.go | 22 +++++------ .../state/dataplane/configuration_test.go | 4 ++ .../mode/static/state/graph/backend_refs.go | 1 + .../mode/static/state/graph/config_maps.go | 20 +++++----- .../install-gateway-api-resources.md | 3 +- .../installation/installing-ngf/helm.md | 3 +- .../installation/installing-ngf/manifests.md | 2 +- 22 files changed, 86 insertions(+), 129 deletions(-) delete mode 100644 examples/backend-tls/app-secret.yaml delete mode 100644 examples/backend-tls/ca.cert diff --git a/cmd/gateway/commands.go b/cmd/gateway/commands.go index 3cf5c20c81..3ba3055af3 100644 --- a/cmd/gateway/commands.go +++ b/cmd/gateway/commands.go @@ -172,10 +172,10 @@ func createStaticModeCommand() *cobra.Command { LockName: leaderElectionLockName.String(), Identity: podName, }, - Plus: plus, - TelemetryReportPeriod: period, - Version: version, - EnableExperimentalFeatures: enableExperimental, + Plus: plus, + TelemetryReportPeriod: period, + Version: version, + ExperimentalFeatures: enableExperimental, } if err := static.StartManager(conf); err != nil { diff --git a/examples/backend-tls/README.md b/examples/backend-tls/README.md index 11e11027b2..704ffae7f7 100644 --- a/examples/backend-tls/README.md +++ b/examples/backend-tls/README.md @@ -1,6 +1,8 @@ # Backend TLS Policy Example -In this example, we will create a Backend TLS Policy, attach it to our +In this example, we will create a Backend TLS Policy, attach it to our application service, and then configure routing +rules. The Backend TLS Policy will be picked up by NGF and the connection between NGF and the upstream server will use +HTTPS. ## Running the Example @@ -56,18 +58,9 @@ In this example, we will create a Backend TLS Policy, attach it to our kubectl apply -f policy.yaml ``` -## 3. Configure HTTPS Termination and Routing +## 3. Configure HTTP Termination and Routing -1. Create the Secret with a TLS certificate and key: - - ```shell - kubectl apply -f app-secret.yaml - ``` - - The TLS certificate and key in this Secret are used to terminate the TLS connections for the secure-app application. - > **Important**: This certificate and key are for demo purposes only. - -2. Create the Gateway resource: +1. Create the Gateway resource: ```shell kubectl apply -f gateway.yaml @@ -77,7 +70,7 @@ In this example, we will create a Backend TLS Policy, attach it to our - `http` listener for HTTP traffic - `https` listener for HTTPS traffic. It terminates TLS connections using the `app-secret` we created in step 1. -3. Create the HTTPRoute resources: +2. Create the HTTPRoute resources: ```shell kubectl apply -f secure-app-routes.yaml @@ -85,11 +78,10 @@ In this example, we will create a Backend TLS Policy, attach it to our ## 4. Test the Application -To access the application, we will use `curl` to send requests to the `secure-app` Service over HTTPS. Since our -certificate is self-signed, we will use curl's `--cacert` option to supply the certificate for verification. +To access the application, we will use `curl` to send requests to the `secure-app` Service over HTTP. ```shell -curl --resolve secure-app.example.com:$GW_HTTPS_PORT:$GW_IP https://secure-app.example.com:$GW_HTTPS_PORT/ --cacert ca.cert +curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ ``` ```text diff --git a/examples/backend-tls/app-secret.yaml b/examples/backend-tls/app-secret.yaml deleted file mode 100644 index ffe9a7aaf0..0000000000 --- a/examples/backend-tls/app-secret.yaml +++ /dev/null @@ -1,8 +0,0 @@ -apiVersion: v1 -kind: Secret -metadata: - name: app-secret -type: kubernetes.io/tls -data: - tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwRENDQXJpZ0F3SUJBZ0lVVDQwYTFYd3doUHVBdDJNMkdZZUovYXluZlFBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRWZNQjBHQTFVRUF3d1djMlZqZFhKbExXRndjQzVsZUdGdGNHeGxMbU52YlRFTE1Ba0dBMVVFQmhNQwpWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1YzJselkyOHdIaGNOTWpRd01URTRNVGd3TVRBeFdoY05NalV3Ck1URTNNVGd3TVRBeFdqQi9NUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVcKTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzV6YVhOamJ6RU9NQXdHQTFVRUNnd0ZUa2RKVGxneEVqQVFCZ05WQkFzTQpDVTVIU1U1WUlFUmxkakVmTUIwR0ExVUVBd3dXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdmJUQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMeUx0eURNbTZ4M0ZEUFJsOGZ0azNweCtrRWQKYTVpTGZOQ3lDbUVjYktBQVBDNEhZckl5b1B5QXpSTlJCMWErekE0UTlrbzJZRG5vR0dkeFJaMEdydldKZUV2Mgo3MWlHNGxhbHRVTS9WOWNvSktQY0UyTEI0R3R6cFA3ckdIWXNvRDlOUXFpV3YwZ0lOdE42MjdrWGg4UW41V1hYCk92Y2FkS2h0bjJER3RvU0VzT3dpNzR5NEt3SmFkWnlwLzJaM0hPakRTNjVIVmxydmUxUXpBMVRzTEp6S3cva3gKbHBSR0lWK0lhUjZXbXZsaVFVdDJxWFg0L3hGeVVEM2Vic05TeXpHUk5mQ0NOTWxlWlV3MTR3ZUdhOEVnc2tDcQprOGdYSmpFZXQxMlR4OGxkY3BpVWlxYVpkOStYZjJmUS8yL2Y5c1IzM3Q4K0VVUWpoZ2ZIbHlsLzV1RUNBd0VBCkFhTjlNSHN3SHdZRFZSMGpCQmd3Rm9BVTRUT096c1d0Q3ZWdGJlWXFSU0FqN2tXajFkb3dDUVlEVlIwVEJBSXcKQURBTEJnTlZIUThFQkFNQ0JQQXdJUVlEVlIwUkJCb3dHSUlXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdgpiVEFkQmdOVkhRNEVGZ1FVZmtWREFFWmIwcjRTZ2swck10a0FvQ2c2RjRnd0RRWUpLb1pJaHZjTkFRRUxCUUFECmdnRUJBQWFiQit6RzVSODl6WitBT2RsRy9wWE9nYjF6VkJsQ0dMSkhyYTl1cTMvcXRPR1VacDlnd2dZSWJ4VnkKUkVLbWVRa05pV0haSDNCSlNTZ3czbE9abGNxcW5xbUJ2OFAxTUxDZ3JqbDJSN1d2NVhkb2RlQkJxc0lvZkNxVgp3ZG51THJUU3RTbmd2MGhDcldBNlBmTnlQeXMzSGJva1k3RExNREhuNmhBQWcwMUNDT0pWWGpNZjFqLzNIMFNCClBQSWxtek5aRUpEd0JMR2hyb1V3aUY3NkNUV1Fudi8yc1pvWHMwUlFiRTY3TmNraXc2Z0svaWRwVTVzMmlkOEQKVExjVjNxenVFaE1ZeUlua0ZWNEJLZlFkTWxDQnE1QWdyU1Jqb2FoaCszbFRwYVpUalJGUGFVd3VZYXVsQXRzNgpra1ROaGltWWQ3Ym1aVk5MK2I0MzhmN1RMaGc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= - tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQzhpN2Nnekp1c2R4UXoKMFpmSDdaTjZjZnBCSFd1WWkzelFzZ3BoSEd5Z0FEd3VCMkt5TXFEOGdNMFRVUWRXdnN3T0VQWktObUE1NkJobgpjVVdkQnE3MWlYaEw5dTlZaHVKV3BiVkRQMWZYS0NTajNCTml3ZUJyYzZUKzZ4aDJMS0EvVFVLb2xyOUlDRGJUCmV0dTVGNGZFSitWbDF6cjNHblNvYlo5Z3hyYUVoTERzSXUrTXVDc0NXbldjcWY5bWR4em93MHV1UjFaYTczdFUKTXdOVTdDeWN5c1A1TVphVVJpRmZpR2tlbHByNVlrRkxkcWwxK1A4UmNsQTkzbTdEVXNzeGtUWHdnalRKWG1WTQpOZU1IaG12QklMSkFxcFBJRnlZeEhyZGRrOGZKWFhLWWxJcW1tWGZmbDM5bjBQOXYzL2JFZDk3ZlBoRkVJNFlICng1Y3BmK2JoQWdNQkFBRUNnZ0VBUXJucGJleXJmVTVKTW91Ty9UenBrQkJ0UWdVZzhvUVBBS2E1d0tONEYrbnQKWWxiUHlZUGNjSEErNDRLdUo3ZHZiTno0NU11NG8xV3Q2Vkh2a29KdWdjd01iRW53YTdLVXdKaDFmVjZaL2pXaApQZkpoVS9hTUwwcm1qaWJ5YWNRaVZEVEtEZk1Ici96a05sVEpGUWlzVGpIV1lBUGJSTjh5Z1BjR3pBK1hRVzg5CmxsOFdoeWwrZndjRVAzNTM4TUdKYUpHVmV4Q2d5cVRyKzZwQ29yRUpFL1pSNytiMTNyejRsbmxpZXVYa2pCVkcKSnYvUVI0RVhTSDhuRit3K2FvWTFQaGd2QnFJTnFQZjJMT1V0MzNiN3FDTkFmSVBRdFZFejJsN3NqbmJlcElTTwpvTkhNUFY0N21XTzY2dzhFMzJVMjNjVEtUbytJcWovM0d1eGhweXlYaHdLQmdRRDVNRDhmY3ZyM0xVZHkvZ3I0Ci82MVBqUXNSaWRYdjN0Mk1MVEk4UkduNzJWcGxvVVExNDZHV2xGTGVVVDY1L1ZVMjNsZFUrUWt2eFNMK3U1bW4KRUJIdXUyVmtBUWVYcUJXWkpPTmFSZG9Ia1YzK2Fyc0U4Qld0SWVtZHJ0MWN6bHFjc1VzMWdGdG1COGI3RHB3UwpHKzRoZDlzZG0weDBNS2hoOFVGOUQrZytUd0tCZ1FEQnN4dUpoZ1hnck14S0dVMHJ5MW9WWTJtMGpDUEpJcTkzCmNZRUZGY3lYZ0U4OWlDVmlob3dVVE8yMXpTZ3o0SVVKNUxoc2M5N3VIVER1VXdwcVI5NFBsdjlyaXJvakowM1UKT3FyWHgwbWdNN2xibVM1L1RwS0czZG1QblZ0WEZMektISFgyWDVnUW56emYvdXVKK3NtbDVLQW5WN0VZc1oxcgpkVXJvRm8zcnp3S0JnUUNZdjM5aUdzeEdLaVpMRWZqTjY0UmthRFBwdTFFOTZhSnE0K1dRVmV1VnF3V2ptTGhFClJGWHdCTm5MVjRnWTRIYVUzTFF4N1RvNVl5RnhmclBRV2FSMGI4RFdEVitIRWt5ekJJNnM3bmFZL3YzY0Q3YTIKYnlrS2FPaFlkVEZTUzFmMkJ5UHdGczl2K3NKNWNOb3dxNWhNUWJrNks5RXd4QWJqaXN5M0NjSTJOd0tCZ1FDbwo2c2pZNVVlNjV2WkFxRS9rSVRJdDlNUDU3enhGNnptWnNDSVRqUzhkNzRjcTRjKzRYQjFNbHNtMkFYTk55ajQ2Cm9uc3lHTm9RVE9TZThVdmo0MGlEeitwdW5rdzAyOUhEZ21YNlJwQ3VaRzBBdEZVWU1DMFg3K0FLbmU5SndZdmgKdFhBcHFyT3h5eXdMS3dPOUVEZEp0RmIxK0VNNGhhd0NTZ2RJM21KbGdRS0JnRzIxeEJNRXRzMFBVN3lDYTZ0YwpadDc1NUV4aEdkR3F5MmtHYmtmdzBEaHBQQVVUZmdncVF3NVBYdGVIS1ZBSDlKaG5kVnBBZFFxNmZ1MER1MDNKCkl0cGpxNWluZXVoR0x0alpMR1Nhd0dwY0FUU3h4Z3dCM0l0Z29LKzBCRFhteWxId0lEcUc5Z2crRU5KK0VhL0MKeTFOMmV0ZG1sQ01hNjM4cVJlNFlTWk55Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= diff --git a/examples/backend-tls/backend-certs-configmap.yaml b/examples/backend-tls/backend-certs-configmap.yaml index 83be724906..e8e52235bc 100644 --- a/examples/backend-tls/backend-certs-configmap.yaml +++ b/examples/backend-tls/backend-certs-configmap.yaml @@ -1,7 +1,7 @@ kind: ConfigMap apiVersion: v1 metadata: - name: backend-certs + name: backend-cert data: ca.crt: | -----BEGIN CERTIFICATE----- diff --git a/examples/backend-tls/ca.cert b/examples/backend-tls/ca.cert deleted file mode 100644 index 403d3355ab..0000000000 --- a/examples/backend-tls/ca.cert +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDbTCCAlWgAwIBAgIUPA3fFnkLl63GZ7noUjb5NoLhSYkwDQYJKoZIhvcNAQEL -BQAwRjEfMB0GA1UEAwwWc2VjdXJlLWFwcC5leGFtcGxlLmNvbTELMAkGA1UEBhMC -VVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lzY28wHhcNMjQwMTE4MTgwMTAxWhcNMjUw -MTA4MTgwMTAxWjBGMR8wHQYDVQQDDBZzZWN1cmUtYXBwLmV4YW1wbGUuY29tMQsw -CQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzCCASIwDQYJKoZIhvcN -AQEBBQADggEPADCCAQoCggEBAJGgn81BrqzmI4aQmGrg7RgkO5oYwlThQ9X/xVHB -YVFptjRPAZz9g92g5birI/NZ43C6nEbZrJrSCqN3wgvV84jJmBAgpAvW+LhF4caa -nhAnecJCcTbwrd542vCDoDRsNV5ffbpESgC4FxPGkRVbSa0KHQz8qCLqS2+uaB7X -t76iw6y4pQ3klobVp1XtUpzZMGMBqZFnsAdl+PWMmSTvqjixkSlfcUY6Crnk9W6d -Sns5cpzKdUs+2ZkBe6VkBgSs8xbaz8Y2YC1GhRqGlxYLT3WBaIlSCKPuRrGjwE3r -AsW6gSL919H1O1a+MjQuLuQ4lnCbCpNzM9OV1JISMWfwifMCAwEAAaNTMFEwHQYD -VR0OBBYEFOEzjs7FrQr1bW3mKkUgI+5Fo9XaMB8GA1UdIwQYMBaAFOEzjs7FrQr1 -bW3mKkUgI+5Fo9XaMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB -AG/eX4pctINIrHvRyHOusdac5iXSJbQRZgWvP1F2p95qoIDESciAU1Sh1oJv+As5 -IlJOZPJNuZFpDLjc8kzSoEbc1Q5+QyTBlyNNsagWYYwK0CEJ6KJt80vytffmdOIg -z8/a+2Ax829vcn1w1SUi5V6ea/l8K74f2SL/zSSHgtEiz8V0TlvT7J6wurgmnk4t -yQRmsXlDGefuijMNCVf7jWwLx2BODfKoEA1pJkthnNvdizlikmz+9elxhV9bRf3Y -NnubytWPfO1oeHjVGvxVjCouIYine+VlskvwHmMi/dYod6yd7aFYu4CU3g/hjwKo -LY2WNv5j3JhDnEYK9Zj3z7A= ------END CERTIFICATE----- diff --git a/examples/backend-tls/gateway.yaml b/examples/backend-tls/gateway.yaml index 72a754546f..9d402bd5a1 100644 --- a/examples/backend-tls/gateway.yaml +++ b/examples/backend-tls/gateway.yaml @@ -8,11 +8,3 @@ spec: - name: http port: 80 protocol: HTTP - - name: https - port: 443 - protocol: HTTPS - tls: - mode: Terminate - certificateRefs: - - kind: Secret - name: app-secret diff --git a/examples/backend-tls/policy.yaml b/examples/backend-tls/policy.yaml index bbf7859b54..8d4d75e1d5 100644 --- a/examples/backend-tls/policy.yaml +++ b/examples/backend-tls/policy.yaml @@ -2,7 +2,6 @@ apiVersion: gateway.networking.k8s.io/v1alpha2 kind: BackendTLSPolicy metadata: name: backend-tls - namespace: default spec: targetRef: group: '' @@ -11,7 +10,7 @@ spec: namespace: default tls: caCertRefs: - - name: backend-certs + - name: backend-cert group: '' kind: ConfigMap hostname: secure-app.example.com diff --git a/examples/backend-tls/secure-app-routes.yaml b/examples/backend-tls/secure-app-routes.yaml index 3c388c4749..1fb788f5f6 100644 --- a/examples/backend-tls/secure-app-routes.yaml +++ b/examples/backend-tls/secure-app-routes.yaml @@ -1,28 +1,11 @@ apiVersion: gateway.networking.k8s.io/v1 kind: HTTPRoute -metadata: - name: secure-app-tls-redirect -spec: - parentRefs: - - name: gateway - sectionName: http - hostnames: - - "secure-app.example.com" - rules: - - filters: - - type: RequestRedirect - requestRedirect: - scheme: https - port: 443 ---- -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute metadata: name: secure-app spec: parentRefs: - name: gateway - sectionName: https + sectionName: http hostnames: - "secure-app.example.com" rules: diff --git a/examples/backend-tls/secure-app.yaml b/examples/backend-tls/secure-app.yaml index 7514282862..46ed8d5e8e 100644 --- a/examples/backend-tls/secure-app.yaml +++ b/examples/backend-tls/secure-app.yaml @@ -14,7 +14,7 @@ spec: spec: containers: - name: secure-app - image: nginxdemos/nginx-hello:plain-text + image: nginxdemos/nginx-unprivileged:plain-text ports: - containerPort: 8443 volumeMounts: @@ -59,8 +59,6 @@ data: ssl_certificate /etc/nginx/ssl/tls.crt; ssl_certificate_key /etc/nginx/ssl/tls.key; - proxy_ssl_server_name on; - default_type text/plain; location / { diff --git a/internal/mode/static/config/config.go b/internal/mode/static/config/config.go index 3e7cbd8559..95c2f1377d 100644 --- a/internal/mode/static/config/config.go +++ b/internal/mode/static/config/config.go @@ -38,8 +38,8 @@ type Config struct { UpdateGatewayClassStatus bool // Plus indicates whether NGINX Plus is being used. Plus bool - // EnableExperimentalFeatures enables experimental features. - EnableExperimentalFeatures bool + // ExperimentalFeatures indicates if experimental features are enabled. + ExperimentalFeatures bool } // GatewayPodConfig contains information about this Pod. diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go index 08c3cd0f67..dca0f4a172 100644 --- a/internal/mode/static/manager.go +++ b/internal/mode/static/manager.go @@ -203,7 +203,7 @@ func StartManager(cfg config.Config) error { objects, objectLists := prepareFirstEventBatchPreparerArgs( cfg.GatewayClassName, cfg.GatewayNsName, - cfg.EnableExperimentalFeatures, + cfg.ExperimentalFeatures, ) firstBatchPreparer := events.NewFirstEventBatchPreparerImpl(mgr.GetCache(), objects, objectLists) eventLoop := events.NewEventLoop( @@ -384,13 +384,13 @@ func registerControllers( }, } - if cfg.EnableExperimentalFeatures { + if cfg.ExperimentalFeatures { backendTLSObjs := []ctlrCfg{ { objectType: &gatewayv1alpha2.BackendTLSPolicy{}, }, { - objectType: &apiv1.ConfigMap{}, + objectType: &apiv1.ConfigMap{}, // TODO(ciarams87): Use only metadata predicate }, } controllerRegCfgs = append(controllerRegCfgs, backendTLSObjs...) diff --git a/internal/mode/static/nginx/config/generator_test.go b/internal/mode/static/nginx/config/generator_test.go index 2e4ab29b72..30fd4befc8 100644 --- a/internal/mode/static/nginx/config/generator_test.go +++ b/internal/mode/static/nginx/config/generator_test.go @@ -94,4 +94,6 @@ func TestGenerate(t *testing.T) { g.Expect(configVersion).To(ContainSubstring(fmt.Sprintf("return 200 %d", conf.Version))) g.Expect(files[3].Path).To(Equal("/etc/nginx/secrets/test-certbundle.crt")) + certBundle := string(files[3].Content) + g.Expect(certBundle).To(Equal("test-cert")) } diff --git a/internal/mode/static/nginx/config/http/config.go b/internal/mode/static/nginx/config/http/config.go index 66d7aa05b4..8486b7d464 100644 --- a/internal/mode/static/nginx/config/http/config.go +++ b/internal/mode/static/nginx/config/http/config.go @@ -90,7 +90,6 @@ type MapParameter struct { // ProxySSLVerify holds the proxied HTTPS server verification configuration. type ProxySSLVerify struct { - CertPath string - Hostname string - VerifyOn bool + TrustedCertificate string + Name string } diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 45a0b7cbcc..9a8d7dba86 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -3,6 +3,7 @@ package config import ( "encoding/json" "fmt" + "os" "strings" gotemplate "text/template" @@ -254,7 +255,7 @@ func updateLocationsForFilters( } } buildLocations[i].ProxySetHeaders = proxySetHeaders - buildLocations[i].ProxySSLVerify = convertProxyTLSFromBackends(matchRule.BackendGroup.Backends) + buildLocations[i].ProxySSLVerify = createProxyTLSFromBackends(matchRule.BackendGroup.Backends) proxyPass := createProxyPass( matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite, @@ -266,30 +267,54 @@ func updateLocationsForFilters( return buildLocations } -func convertProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVerify { +func createProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVerify { if len(backends) == 0 { return nil } for _, b := range backends { - proxyVerify := convertBackendTLS(b.VerifyTLS) + proxyVerify := createProxySSLVerify(b.VerifyTLS) if proxyVerify != nil { // If any backend has a backend TLS policy defined, then we use that for the proxy SSL verification. // If multiple backends in the group have a backend TLS policy defined, then we use the first one we find. + // TODO(ciarams87): Fix this return proxyVerify } } return nil } -func convertBackendTLS(v *dataplane.VerifyTLS) *http.ProxySSLVerify { +func createProxySSLVerify(v *dataplane.VerifyTLS) *http.ProxySSLVerify { if v == nil || v.Hostname == "" { return nil } + var trustedCert string + if v.CertBundleID != "" { + trustedCert = generateCertBundleFileName(v.CertBundleID) + } else { + trustedCert = getRootCAPath() + } return &http.ProxySSLVerify{ - CertPath: generateCertBundleFileName(v.CertBundleID), - Hostname: v.Hostname, - VerifyOn: v.CertBundleID != "", + TrustedCertificate: trustedCert, + Name: v.Hostname, + } +} + +// TODO(ciarams87): Move this logic earlier +func getRootCAPath() string { + certFiles := []string{ + "/etc/ssl/cert.pem", // Alpine Linux + "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 + "/etc/ssl/ca-bundle.pem", // OpenSUSE + "/etc/pki/tls/cacert.pem", // OpenELEC + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 + } + for _, certFile := range certFiles { + if _, err := os.Stat(certFile); err == nil { + return certFile + } } + return "" } func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return { diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index 567496a341..cf54cb7f96 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -53,14 +53,11 @@ server { proxy_http_version 1.1; proxy_pass {{ $l.ProxyPass }}; {{- if $l.ProxySSLVerify }} - proxy_ssl_name {{ $l.ProxySSLVerify.Hostname }}; - {{- if $l.ProxySSLVerify.VerifyOn }} proxy_ssl_verify on; - proxy_ssl_trusted_certificate {{ $l.ProxySSLVerify.CertPath }}; - {{- end }} + proxy_ssl_name {{ $l.ProxySSLVerify.Hostname }}; + proxy_ssl_trusted_certificate {{ $l.ProxySSLVerify.TrustedCertificate }}; {{- end }} {{- end }} - } {{ end }} } diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 2fbd8e6561..d74e0b2383 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -621,9 +621,8 @@ func TestCreateServers(t *testing.T) { ProxyPass: "https://test_btp_80$request_uri", ProxySetHeaders: baseHeaders, ProxySSLVerify: &http.ProxySSLVerify{ - Hostname: "test-btp.example.com", - CertPath: "/etc/nginx/secrets/test-btp.crt", - VerifyOn: true, + Name: "test-btp.example.com", + TrustedCertificate: "/etc/nginx/secrets/test-btp.crt", }, }, { @@ -631,9 +630,8 @@ func TestCreateServers(t *testing.T) { ProxyPass: "https://test_btp_80$request_uri", ProxySetHeaders: baseHeaders, ProxySSLVerify: &http.ProxySSLVerify{ - Hostname: "test-btp.example.com", - CertPath: "/etc/nginx/secrets/test-btp.crt", - VerifyOn: true, + Name: "test-btp.example.com", + TrustedCertificate: "/etc/nginx/secrets/test-btp.crt", }, }, { @@ -1860,9 +1858,8 @@ func TestConvertBackendTLSFromGroup(t *testing.T) { }, }, expected: &http.ProxySSLVerify{ - CertPath: "/etc/nginx/secrets/default-my-cert.crt", - Hostname: "my-hostname", - VerifyOn: true, + TrustedCertificate: "/etc/nginx/secrets/default-my-cert.crt", + Name: "my-hostname", }, }, { @@ -1899,16 +1896,15 @@ func TestConvertBackendTLSFromGroup(t *testing.T) { }, }, expected: &http.ProxySSLVerify{ - CertPath: "/etc/nginx/secrets/default-my-cert.crt", - Hostname: "my-hostname", - VerifyOn: true, + TrustedCertificate: "/etc/nginx/secrets/default-my-cert.crt", + Name: "my-hostname", }, }, } for _, tc := range tests { t.Run(tc.msg, func(t *testing.T) { - result := convertProxyTLSFromBackends(tc.grp) + result := createProxyTLSFromBackends(tc.grp) g.Expect(result).To(Equal(tc.expected)) }) } diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index c49eb341a6..fa77852672 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -2439,6 +2439,10 @@ func TestHostnameMoreSpecific(t *testing.T) { func TestConvertBackendTLS(t *testing.T) { btpCaCertRefs := &graph.BackendTLSPolicy{ Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "btp", + Namespace: "test", + }, Spec: v1alpha2.BackendTLSPolicySpec{ TLS: v1alpha2.BackendTLSPolicyConfig{ CACertRefs: []v1.LocalObjectReference{ diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index e0d4bd2a61..f44e59b0bd 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -189,6 +189,7 @@ func findBackendTLSPolicyForService( btpNs = string(*btp.Source.Spec.TargetRef.Namespace) } if btp.Source.Spec.TargetRef.Name == ref.Name && btpNs == refNs { + // TODO: resolve conflicts between multiple backend TLS policies beTLSPolicy = btp break } diff --git a/internal/mode/static/state/graph/config_maps.go b/internal/mode/static/state/graph/config_maps.go index 7aad12a364..63705d0576 100644 --- a/internal/mode/static/state/graph/config_maps.go +++ b/internal/mode/static/state/graph/config_maps.go @@ -30,19 +30,19 @@ type caCertConfigMapEntry struct { // configMapResolver wraps the cluster ConfigMaps so that they can be resolved (includes validation). All resolved // ConfigMaps are saved to be used later. type configMapResolver struct { - clusterConfigMaps map[types.NamespacedName]*apiv1.ConfigMap - resolvedConfigMaps map[types.NamespacedName]*caCertConfigMapEntry + clusterConfigMaps map[types.NamespacedName]*apiv1.ConfigMap + resolvedCaCertConfigMaps map[types.NamespacedName]*caCertConfigMapEntry } func newConfigMapResolver(configMaps map[types.NamespacedName]*apiv1.ConfigMap) *configMapResolver { return &configMapResolver{ - clusterConfigMaps: configMaps, - resolvedConfigMaps: make(map[types.NamespacedName]*caCertConfigMapEntry), + clusterConfigMaps: configMaps, + resolvedCaCertConfigMaps: make(map[types.NamespacedName]*caCertConfigMapEntry), } } func (r *configMapResolver) resolve(nsname types.NamespacedName) error { - if s, resolved := r.resolvedConfigMaps[nsname]; resolved { + if s, resolved := r.resolvedCaCertConfigMaps[nsname]; resolved { return s.err } @@ -53,9 +53,7 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { if !exist { validationErr = errors.New("configMap does not exist") - } - - if exist { + } else { if cm.Data != nil { if _, exists := cm.Data[CAKey]; exists { validationErr = validateCA([]byte(cm.Data[CAKey])) @@ -73,7 +71,7 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { } } - r.resolvedConfigMaps[nsname] = &caCertConfigMapEntry{ + r.resolvedCaCertConfigMaps[nsname] = &caCertConfigMapEntry{ caCertConfigMap: CaCertConfigMap{ Source: cm, CACert: caCert, @@ -85,13 +83,13 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { } func (r *configMapResolver) getResolvedConfigMaps() map[types.NamespacedName]*CaCertConfigMap { - if len(r.resolvedConfigMaps) == 0 { + if len(r.resolvedCaCertConfigMaps) == 0 { return nil } resolved := make(map[types.NamespacedName]*CaCertConfigMap) - for nsname, entry := range r.resolvedConfigMaps { + for nsname, entry := range r.resolvedCaCertConfigMaps { // create iteration variable inside the loop to fix implicit memory aliasing caCertConfigMap := entry.caCertConfigMap resolved[nsname] = &caCertConfigMap diff --git a/site/content/includes/installation/install-gateway-api-resources.md b/site/content/includes/installation/install-gateway-api-resources.md index 3df1da91f5..55c669069d 100644 --- a/site/content/includes/installation/install-gateway-api-resources.md +++ b/site/content/includes/installation/install-gateway-api-resources.md @@ -11,8 +11,7 @@ kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/downloa ``` Alternatively, you can install the Gateway API resources from the experimental channel. We support a subset of the -additional features provided by the experimental channel. Please note that these APIs are not suitable for production -use. To install from the experimental channel, run the following: +additional features provided by the experimental channel. To install from the experimental channel, run the following: ```shell kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml diff --git a/site/content/installation/installing-ngf/helm.md b/site/content/installation/installing-ngf/helm.md index efd18a3d28..a7ffd02300 100644 --- a/site/content/installation/installing-ngf/helm.md +++ b/site/content/installation/installing-ngf/helm.md @@ -111,7 +111,8 @@ To disable the creation of a Service: #### Experimental features -We support a subset of the additional features provided by the Gateway API experimental channel. Please note that these APIs are not suitable for production use. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric: +We support a subset of the additional features provided by the Gateway API experimental channel. To enable the +experimental features of Gateway API which are supported by NGINX Gateway Fabric: ```shell helm install ngf oci://ghcr.io/nginxinc/charts/nginx-gateway-fabric --create-namespace -n nginx-gateway --set nginxGateway.experimentalFeatures.enable=true diff --git a/site/content/installation/installing-ngf/manifests.md b/site/content/installation/installing-ngf/manifests.md index 198bde5619..ae71d2e38c 100644 --- a/site/content/installation/installing-ngf/manifests.md +++ b/site/content/installation/installing-ngf/manifests.md @@ -75,7 +75,7 @@ Deploying NGINX Gateway Fabric with Kubernetes manifests takes only a few steps. #### Enable experimental features -We support a subset of the additional features provided by the Gateway API experimental channel. Please note that these APIs are not suitable for production use.To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric, edit the `deploy/manifests/nginx-gateway.yaml` to add the `experimental-features-enable` flag to the nginx-gateway deployment spec: +We support a subset of the additional features provided by the Gateway API experimental channel. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric, edit the `deploy/manifests/nginx-gateway.yaml` to add the `experimental-features-enable` flag to the nginx-gateway deployment spec: ```yaml <...> From e7263542b3d61ef01424541d25eb673edd698676 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Wed, 31 Jan 2024 16:12:21 +0000 Subject: [PATCH 04/13] Add more validation; handle conflicts --- internal/mode/static/manager.go | 3 +- internal/mode/static/nginx/config/servers.go | 25 +-- .../static/nginx/config/servers_template.go | 2 +- .../static/state/dataplane/configuration.go | 21 ++ .../state/dataplane/configuration_test.go | 3 +- internal/mode/static/state/dataplane/types.go | 1 + .../mode/static/state/graph/backend_refs.go | 58 ++++- .../static/state/graph/backend_refs_test.go | 200 ++++++++++++++++++ .../static/state/graph/backend_tls_policy.go | 10 +- .../state/graph/backend_tls_policy_test.go | 79 ++++++- .../mode/static/state/graph/config_maps.go | 4 +- .../overview/gateway-api-compatibility.md | 12 +- 12 files changed, 382 insertions(+), 36 deletions(-) diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go index dca0f4a172..719305d02a 100644 --- a/internal/mode/static/manager.go +++ b/internal/mode/static/manager.go @@ -390,7 +390,8 @@ func registerControllers( objectType: &gatewayv1alpha2.BackendTLSPolicy{}, }, { - objectType: &apiv1.ConfigMap{}, // TODO(ciarams87): Use only metadata predicate + // FIXME(ciarams87): If possible, use only metadata predicate + objectType: &apiv1.ConfigMap{}, }, } controllerRegCfgs = append(controllerRegCfgs, backendTLSObjs...) diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 9a8d7dba86..7c8401aa14 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -3,7 +3,6 @@ package config import ( "encoding/json" "fmt" - "os" "strings" gotemplate "text/template" @@ -275,8 +274,8 @@ func createProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVeri proxyVerify := createProxySSLVerify(b.VerifyTLS) if proxyVerify != nil { // If any backend has a backend TLS policy defined, then we use that for the proxy SSL verification. - // If multiple backends in the group have a backend TLS policy defined, then we use the first one we find. - // TODO(ciarams87): Fix this + // We require that all backends in a group have the same backend TLS policy. + // Verification that all backends in a group have the same backend TLS policy is done in the graph package. return proxyVerify } } @@ -291,7 +290,7 @@ func createProxySSLVerify(v *dataplane.VerifyTLS) *http.ProxySSLVerify { if v.CertBundleID != "" { trustedCert = generateCertBundleFileName(v.CertBundleID) } else { - trustedCert = getRootCAPath() + trustedCert = v.RootCAPath } return &http.ProxySSLVerify{ TrustedCertificate: trustedCert, @@ -299,24 +298,6 @@ func createProxySSLVerify(v *dataplane.VerifyTLS) *http.ProxySSLVerify { } } -// TODO(ciarams87): Move this logic earlier -func getRootCAPath() string { - certFiles := []string{ - "/etc/ssl/cert.pem", // Alpine Linux - "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. - "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 - "/etc/ssl/ca-bundle.pem", // OpenSUSE - "/etc/pki/tls/cacert.pem", // OpenELEC - "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 - } - for _, certFile := range certFiles { - if _, err := os.Stat(certFile); err == nil { - return certFile - } - } - return "" -} - func createReturnValForRedirectFilter(filter *dataplane.HTTPRequestRedirectFilter, listenerPort int32) *http.Return { if filter == nil { return nil diff --git a/internal/mode/static/nginx/config/servers_template.go b/internal/mode/static/nginx/config/servers_template.go index cf54cb7f96..dbf37575ae 100644 --- a/internal/mode/static/nginx/config/servers_template.go +++ b/internal/mode/static/nginx/config/servers_template.go @@ -54,7 +54,7 @@ server { proxy_pass {{ $l.ProxyPass }}; {{- if $l.ProxySSLVerify }} proxy_ssl_verify on; - proxy_ssl_name {{ $l.ProxySSLVerify.Hostname }}; + proxy_ssl_name {{ $l.ProxySSLVerify.Name }}; proxy_ssl_trusted_certificate {{ $l.ProxySSLVerify.TrustedCertificate }}; {{- end }} {{- end }} diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index 1a67907252..ba670bda9b 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -3,6 +3,7 @@ package dataplane import ( "context" "fmt" + "os" "sort" apiv1 "k8s.io/api/core/v1" @@ -177,11 +178,31 @@ func convertBackendTLS(btp *graph.BackendTLSPolicy) *VerifyTLS { verify := &VerifyTLS{} if btp.CaCertRef.Name != "" { verify.CertBundleID = generateCertBundleID(btp.CaCertRef) + } else { + verify.RootCAPath = getRootCAPath() } verify.Hostname = string(btp.Source.Spec.TLS.Hostname) return verify } +// getRootCAPath returns the path to the root CA certificate bundle. +func getRootCAPath() string { + certFiles := []string{ + "/etc/ssl/cert.pem", // Alpine Linux + "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. + "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 + "/etc/ssl/ca-bundle.pem", // OpenSUSE + "/etc/pki/tls/cacert.pem", // OpenELEC + "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 + } + for _, certFile := range certFiles { + if _, err := os.Stat(certFile); err == nil { + return certFile + } + } + return "" +} + func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) { rulesForProtocol := map[v1.ProtocolType]portPathRules{ v1.HTTPProtocolType: make(portPathRules), diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index fa77852672..d04ac674e1 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -2477,7 +2477,8 @@ func TestConvertBackendTLS(t *testing.T) { } expectedWithWellKnownCerts := &VerifyTLS{ - Hostname: "example.com", + Hostname: "example.com", + RootCAPath: getRootCAPath(), } tests := []struct { diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 53a657e404..da1b00afaa 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -247,4 +247,5 @@ type Backend struct { type VerifyTLS struct { CertBundleID CertBundleID Hostname string + RootCAPath string } diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index f44e59b0bd..8a5a1c1fb6 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -9,6 +9,7 @@ import ( gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -92,6 +93,13 @@ func addBackendRefsToRules( } } + if len(backendRefs) > 1 { + cond := validateBackendTLSPolicyMatchingAllBackends(backendRefs) + if cond != nil { + route.Conditions = append(route.Conditions, *cond) + } + } + route.Rules[idx].BackendRefs = backendRefs } } @@ -170,6 +178,40 @@ func createBackendRef( return backendRef, nil } +// validateBackendTLSPolicyMatchingAllBackends validates that all backends in a rule reference the same +// BackendTLSPolicy. We require that all backends in a group have the same backend TLS policy configuration. +// FIXME (ciarams87): This is a temporary solution until we can support multiple backend TLS policies per group. +func validateBackendTLSPolicyMatchingAllBackends(backendRefs []BackendRef) *conditions.Condition { + var mismatch bool + var referencePolicy *BackendTLSPolicy + + for _, backendRef := range backendRefs { + if backendRef.BackendTLSPolicy == nil { + if referencePolicy != nil { + // There was a reference before, so they do not all match + mismatch = true + } + continue + } + + if referencePolicy == nil { + // First reference, store the policy as reference + referencePolicy = backendRef.BackendTLSPolicy + } else { + // Check if the policies match + if backendRef.BackendTLSPolicy.Source.Name != referencePolicy.Source.Name || + backendRef.BackendTLSPolicy.Source.Namespace != referencePolicy.Source.Namespace { + mismatch = true + } + } + } + if mismatch { + msg := "Backend TLS policies do not match for all backends" + return helpers.GetPointer(staticConds.NewRouteBackendRefUnsupportedValue(msg)) + } + return nil +} + func findBackendTLSPolicyForService( backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy, ref gatewayv1.HTTPBackendRef, @@ -189,9 +231,19 @@ func findBackendTLSPolicyForService( btpNs = string(*btp.Source.Spec.TargetRef.Namespace) } if btp.Source.Spec.TargetRef.Name == ref.Name && btpNs == refNs { - // TODO: resolve conflicts between multiple backend TLS policies - beTLSPolicy = btp - break + if beTLSPolicy != nil { + if btp.Source.CreationTimestamp.Equal(&beTLSPolicy.Source.CreationTimestamp) { + // if the policies have the same creation timestamp, the one that comes first alphabetically wins + if btp.Source.Name < beTLSPolicy.Source.Name { + beTLSPolicy = btp + } + } else if btp.Source.CreationTimestamp.Before(&beTLSPolicy.Source.CreationTimestamp) { + // the oldest policy wins - see https://gateway-api.sigs.k8s.io/geps/gep-713/#conflict-resolution + beTLSPolicy = btp + } + } else { + beTLSPolicy = btp + } } } diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index 83555c899a..61d62465eb 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -838,3 +838,203 @@ func TestGetServicePort(t *testing.T) { g.Expect(err).Should(HaveOccurred()) g.Expect(port.Port).To(Equal(int32(0))) } + +func TestValidateBackendTLSPolicyMatchingAllBackends(t *testing.T) { + getBtp := func(name string) *BackendTLSPolicy { + return &BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "test", + }, + }, + } + } + + backendRefsNoPolicies := []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, + }, + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, + }, + } + + backendRefsWithMatchingPolicies := []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, + BackendTLSPolicy: getBtp("btp1"), + }, + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, + BackendTLSPolicy: getBtp("btp1"), + }, + } + backendRefsWithNotMatchingPolicies := []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, + BackendTLSPolicy: getBtp("btp1"), + }, + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, + BackendTLSPolicy: getBtp("btp2"), + }, + } + backendRefsOnePolicy := []BackendRef{ + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, + BackendTLSPolicy: getBtp("btp1"), + }, + { + SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, + }, + } + msg := "Backend TLS policies do not match for all backends" + tests := []struct { + expectedCondition *conditions.Condition + name string + backendRefs []BackendRef + }{ + { + name: "no policies", + backendRefs: backendRefsNoPolicies, + expectedCondition: nil, + }, + { + name: "matching policies", + backendRefs: backendRefsWithMatchingPolicies, + expectedCondition: nil, + }, + { + name: "not matching policies", + backendRefs: backendRefsWithNotMatchingPolicies, + expectedCondition: helpers.GetPointer(staticConds.NewRouteBackendRefUnsupportedValue(msg)), + }, + { + name: "only one policy", + backendRefs: backendRefsOnePolicy, + expectedCondition: helpers.GetPointer(staticConds.NewRouteBackendRefUnsupportedValue(msg)), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + cond := validateBackendTLSPolicyMatchingAllBackends(test.backendRefs) + + g.Expect(cond).To(Equal(test.expectedCondition)) + }) + } +} + +func TestFindBackendTLSPolicyForService(t *testing.T) { + oldCreationTimestamp := metav1.Now() + newCreationTimestamp := metav1.Now() + oldestBtp := BackendTLSPolicy{ + Valid: true, + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "oldest", + Namespace: "test", + CreationTimestamp: oldCreationTimestamp, + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "svc1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + }, + }, + } + newestBtp := BackendTLSPolicy{ + Valid: true, + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "newest", + Namespace: "test", + CreationTimestamp: newCreationTimestamp, + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "svc1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + }, + }, + } + + alphaFirstBtp := BackendTLSPolicy{ + Valid: true, + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "alphabeticallyfirst", + Namespace: "test", + CreationTimestamp: oldCreationTimestamp, + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "svc1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + }, + }, + } + + ref := gatewayv1.HTTPBackendRef{ + BackendRef: gatewayv1.BackendRef{ + BackendObjectReference: gatewayv1.BackendObjectReference{ + Kind: helpers.GetPointer[gatewayv1.Kind]("Service"), + Name: "svc1", + Namespace: helpers.GetPointer[gatewayv1.Namespace]("test"), + }, + }, + } + + tests := []struct { + name string + backendTLSPolicies map[types.NamespacedName]*BackendTLSPolicy + expectedBtpName string + }{ + { + name: "oldest wins", + backendTLSPolicies: map[types.NamespacedName]*BackendTLSPolicy{ + client.ObjectKeyFromObject(newestBtp.Source): &newestBtp, + client.ObjectKeyFromObject(oldestBtp.Source): &oldestBtp, + }, + expectedBtpName: "oldest", + }, + { + name: "alphabetically first wins", + backendTLSPolicies: map[types.NamespacedName]*BackendTLSPolicy{ + client.ObjectKeyFromObject(oldestBtp.Source): &oldestBtp, + client.ObjectKeyFromObject(alphaFirstBtp.Source): &alphaFirstBtp, + client.ObjectKeyFromObject(newestBtp.Source): &newestBtp, + }, + expectedBtpName: "alphabeticallyfirst", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + btp, err := findBackendTLSPolicyForService(test.backendTLSPolicies, ref, "test") + + g.Expect(btp.Source.Name).To(Equal(test.expectedBtpName)) + g.Expect(err).ToNot(HaveOccurred()) + }) + } +} diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go index da54b4c8e5..1a2caba7c3 100644 --- a/internal/mode/static/state/graph/backend_tls_policy.go +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -69,14 +69,18 @@ func validateBackendTLSPolicy( Namespace: helpers.GetPointer((v1.Namespace)(gateway.Source.Namespace)), Name: v1.ObjectName(gateway.Source.Name), } + var alreadyAncestor bool for _, ancestor := range backendTLSPolicy.Status.Ancestors { if string(ancestor.ControllerName) == ctlrName && ancestor.AncestorRef.Name == ancestorRef.Name && - ancestor.AncestorRef.Namespace == ancestorRef.Namespace { + *ancestor.AncestorRef.Namespace == *ancestorRef.Namespace { + alreadyAncestor = true break } } - valid = false - conds = append(conds, staticConds.NewBackendTLSPolicyIgnored("too many ancestors, cannot attach a new Gateway")) + if !alreadyAncestor { + valid = false + conds = append(conds, staticConds.NewBackendTLSPolicyIgnored("too many ancestors, cannot attach a new Gateway")) + } } if err := validateBackendTLSHostname(backendTLSPolicy); err != nil { valid = false diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go index 587d2f81f2..ccc0394f6e 100644 --- a/internal/mode/static/state/graph/backend_tls_policy_test.go +++ b/internal/mode/static/state/graph/backend_tls_policy_test.go @@ -56,7 +56,36 @@ func TestValidateBackendTLSPolicy(t *testing.T) { localObjectRefTooManyCerts := append(localObjectRefNormalCase, localObjectRefInvalidName...) - // TODO: add test for too many ancestors + getAncestorRef := func(ctlrName, parentName string) v1alpha2.PolicyAncestorStatus { + return v1alpha2.PolicyAncestorStatus{ + ControllerName: gatewayv1.GatewayController(ctlrName), + AncestorRef: gatewayv1.ParentReference{ + Name: gatewayv1.ObjectName(parentName), + Namespace: helpers.GetPointer(gatewayv1.Namespace("test")), + }, + } + } + + ancestors := []v1alpha2.PolicyAncestorStatus{ + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + getAncestorRef("not-us", "not-us"), + } + + ancestorsWithUs := append(ancestors, getAncestorRef("test", "gateway")) tests := []struct { tlsPolicy *v1alpha2.BackendTLSPolicy @@ -82,6 +111,27 @@ func TestValidateBackendTLSPolicy(t *testing.T) { isValid: true, caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, }, + { + name: "normal case with ca cert refs and 16 ancestors including us", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "foo.test.com", + }, + }, + Status: v1alpha2.PolicyStatus{ + Ancestors: ancestorsWithUs, + }, + }, + isValid: true, + caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + }, { name: "normal case with well known certs", tlsPolicy: &v1alpha2.BackendTLSPolicy{ @@ -218,6 +268,27 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, isValid: false, }, + { + name: "invalid case with too many ancestors", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "foo.test.com", + }, + }, + Status: v1alpha2.PolicyStatus{ + Ancestors: ancestors, + }, + }, + isValid: false, + caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + }, } configMaps := map[types.NamespacedName]*v1.ConfigMap{ @@ -247,7 +318,11 @@ func TestValidateBackendTLSPolicy(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) - valid, caCertName, conds := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver, "test", &Gateway{}) + gateway := &Gateway{ + Source: &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "test"}}, + } + + valid, caCertName, conds := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver, "test", gateway) g.Expect(valid).To(Equal(test.isValid)) g.Expect(caCertName).To(Equal(test.caCertName)) diff --git a/internal/mode/static/state/graph/config_maps.go b/internal/mode/static/state/graph/config_maps.go index 63705d0576..23bec08c63 100644 --- a/internal/mode/static/state/graph/config_maps.go +++ b/internal/mode/static/state/graph/config_maps.go @@ -52,7 +52,7 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { var caCert []byte if !exist { - validationErr = errors.New("configMap does not exist") + validationErr = errors.New("ConfigMap does not exist") } else { if cm.Data != nil { if _, exists := cm.Data[CAKey]; exists { @@ -67,7 +67,7 @@ func (r *configMapResolver) resolve(nsname types.NamespacedName) error { } } if len(caCert) == 0 { - validationErr = fmt.Errorf("configMap does not have the data or binaryData field %v", CAKey) + validationErr = fmt.Errorf("ConfigMap does not have the data or binaryData field %v", CAKey) } } diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index 03e6bcb7dd..07ac38eb98 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -254,7 +254,17 @@ Fields: - `group` - supported. - `kind` - supports `ConfigMap`. - `hostname` - supported. - - `wellKnownCerts` - supports `System`. + - `wellKnownCerts` - supports `System`. This will set the CA certificate to the system root CA path. +- `status` + - `ancestors` + - `ancestorRef` - supported. + - `controllerName`: supported. + - `conditions`: Supported (Condition/Status/Reason): + - `Attached/True/BackendTLSPolicyAttached` - Custom reason for when the BackendTLSPolicy is attached to at least one Service referenced by this Gateway. + - `Attached/False/BackendTLSPolicyIgnored` - Custom reason for when the BackendTLSPolicy config cannot be attached to the Gateway and will be ignored. + - `Valid/False/BackendTLSPolicyInvalid` - Custom reason for when the BackendTLSPolicy config is invalid. + +{{}}If multiple `backendRefs` are defined for a HTTPRoute rule, all the referenced Services *must* have matching BackendTLSPolicy configuration{{}} ### Custom Policies From 46d2bbf7baf6e6ebaf3366dd8d02301f0a4f240a Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Thu, 1 Feb 2024 14:29:28 +0000 Subject: [PATCH 05/13] Add status updater unit tests --- conformance/scripts/install-gateway.sh | 5 - conformance/scripts/uninstall-gateway.sh | 5 - docs/developer/quickstart.md | 6 + internal/framework/status/backend_tls_test.go | 51 +++++++++ internal/framework/status/setters_test.go | 69 ++++++++++++ internal/framework/status/updater_test.go | 77 ++++++++++++- internal/mode/static/build_statuses.go | 2 +- internal/mode/static/build_statuses_test.go | 104 ++++++++++++++++++ .../mode/static/state/graph/backend_refs.go | 2 + .../static/state/graph/backend_tls_policy.go | 4 +- .../graph/{config_maps.go => configmaps.go} | 0 ...config_maps_test.go => configmaps_test.go} | 0 12 files changed, 308 insertions(+), 17 deletions(-) create mode 100644 internal/framework/status/backend_tls_test.go rename internal/mode/static/state/graph/{config_maps.go => configmaps.go} (100%) rename internal/mode/static/state/graph/{config_maps_test.go => configmaps_test.go} (100%) diff --git a/conformance/scripts/install-gateway.sh b/conformance/scripts/install-gateway.sh index 902d91b91b..641567d987 100755 --- a/conformance/scripts/install-gateway.sh +++ b/conformance/scripts/install-gateway.sh @@ -10,11 +10,6 @@ if [ -z $2 ]; then exit 1 fi -if [ -z $3 ]; then - echo "enable experimental argument not set; exiting" - exit 1 -fi - if [ $1 == "main" ]; then temp_dir=$(mktemp -d) cd ${temp_dir} diff --git a/conformance/scripts/uninstall-gateway.sh b/conformance/scripts/uninstall-gateway.sh index 35d4fc2635..f6f26c2c00 100755 --- a/conformance/scripts/uninstall-gateway.sh +++ b/conformance/scripts/uninstall-gateway.sh @@ -10,11 +10,6 @@ if [ -z $2 ]; then exit 1 fi -if [ -z $3 ]; then - echo "enable experimental argument not set; exiting" - exit 1 -fi - if [ $1 == "main" ]; then temp_dir=$(mktemp -d) cd ${temp_dir} diff --git a/docs/developer/quickstart.md b/docs/developer/quickstart.md index b339484ea8..c0e4920b64 100644 --- a/docs/developer/quickstart.md +++ b/docs/developer/quickstart.md @@ -128,6 +128,12 @@ This will build the docker images `nginx-gateway-fabric:` and `nginx- kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml ``` + Alternatively, install Gateway API CRDs from the experimental channel: + + ```shell + kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml + ``` + 4. Install NGF using your custom image and expose NGF with a NodePort Service: - To install with Helm (where your release name is `my-release`): diff --git a/internal/framework/status/backend_tls_test.go b/internal/framework/status/backend_tls_test.go new file mode 100644 index 0000000000..8a9c967ff4 --- /dev/null +++ b/internal/framework/status/backend_tls_test.go @@ -0,0 +1,51 @@ +package status + +import ( + "testing" + + . "github.com/onsi/gomega" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" + + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" + "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" +) + +func TestPrepareBackendTLSPolicyStatus(t *testing.T) { + oldStatus := v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + AncestorRef: v1.ParentReference{ + Namespace: helpers.GetPointer((v1.Namespace)("ns1")), + Name: v1alpha2.ObjectName("other-gw"), + }, + ControllerName: v1alpha2.GatewayController("otherCtlr"), + Conditions: []metav1.Condition{{Type: "otherType", Status: "otherStatus"}}, + }, + }, + } + + newStatus := BackendTLSPolicyStatus{ + AncestorStatuses: []AncestorStatus{ + { + GatewayNsName: types.NamespacedName{ + Namespace: "ns1", + Name: "gw1", + }, + Conditions: []conditions.Condition{{Type: "type1", Status: "status1"}}, + }, + }, + ObservedGeneration: 1, + } + + transistionTime := metav1.Now() + ctlrName := "nginx-gateway" + + policyStatus := prepareBackendTLSPolicyStatus(oldStatus, newStatus, ctlrName, transistionTime) + + g := NewWithT(t) + + g.Expect(policyStatus.Ancestors).To(HaveLen(2)) +} diff --git a/internal/framework/status/setters_test.go b/internal/framework/status/setters_test.go index c00fb2b447..199b317415 100644 --- a/internal/framework/status/setters_test.go +++ b/internal/framework/status/setters_test.go @@ -7,6 +7,7 @@ import ( . "github.com/onsi/gomega" v1 "k8s.io/apimachinery/pkg/apis/meta/v1" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + gatewayv1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" @@ -848,3 +849,71 @@ func TestEqualPointers(t *testing.T) { }) } } + +func TestBtpStatusEqual(t *testing.T) { + getPolicyStatus := func(ancestorName, ancestorNs, ctlrName string) gatewayv1alpha2.PolicyStatus { + return gatewayv1alpha2.PolicyStatus{ + Ancestors: []gatewayv1alpha2.PolicyAncestorStatus{ + { + AncestorRef: gatewayv1.ParentReference{ + Namespace: helpers.GetPointer[gatewayv1.Namespace]((gatewayv1.Namespace)(ancestorNs)), + Name: gatewayv1alpha2.ObjectName(ancestorName), + }, + ControllerName: gatewayv1alpha2.GatewayController(ctlrName), + Conditions: []v1.Condition{{Type: "otherType", Status: "otherStatus"}}, + }, + }, + } + } + tests := []struct { + name string + controllerName string + previous gatewayv1alpha2.PolicyStatus + current gatewayv1alpha2.PolicyStatus + expEqual bool + }{ + { + name: "status equal", + previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + current: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + controllerName: "ctlr1", + expEqual: true, + }, + { + name: "status not equal, different ancestor name", + previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + current: getPolicyStatus("ancestor2", "ns1", "ctlr1"), + controllerName: "ctlr1", + expEqual: false, + }, + { + name: "status not equal, different ancestor namespace", + previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + current: getPolicyStatus("ancestor1", "ns2", "ctlr1"), + controllerName: "ctlr1", + expEqual: false, + }, + { + name: "status not equal, different controller name on current", + previous: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + current: getPolicyStatus("ancestor1", "ns1", "ctlr2"), + controllerName: "ctlr1", + expEqual: false, + }, + { + name: "status not equal, different controller name on previous", + previous: getPolicyStatus("ancestor1", "ns1", "ctlr2"), + current: getPolicyStatus("ancestor1", "ns1", "ctlr1"), + controllerName: "ctlr1", + expEqual: false, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + equal := btpStatusEqual(test.controllerName, test.previous, test.current) + g.Expect(equal).To(Equal(test.expEqual)) + }) + } +} diff --git a/internal/framework/status/updater_test.go b/internal/framework/status/updater_test.go index d743097c07..faed9195a4 100644 --- a/internal/framework/status/updater_test.go +++ b/internal/framework/status/updater_test.go @@ -14,6 +14,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/log/zap" v1 "sigs.k8s.io/gateway-api/apis/v1" + v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" @@ -42,6 +43,7 @@ var _ = Describe("Updater", func() { scheme := runtime.NewScheme() Expect(v1.AddToScheme(scheme)).Should(Succeed()) + Expect(v1alpha2.AddToScheme(scheme)).Should(Succeed()) Expect(ngfAPI.AddToScheme(scheme)).Should(Succeed()) client = fake.NewClientBuilder(). @@ -51,6 +53,7 @@ var _ = Describe("Updater", func() { &v1.Gateway{}, &v1.HTTPRoute{}, &ngfAPI.NginxGateway{}, + &v1alpha2.BackendTLSPolicy{}, ). Build() @@ -63,8 +66,9 @@ var _ = Describe("Updater", func() { Describe("Process status updates", Ordered, func() { type generations struct { - gatewayClass int64 - gateways int64 + gatewayClass int64 + gateways int64 + backendTLSPolicies int64 } var ( @@ -73,6 +77,7 @@ var _ = Describe("Updater", func() { gw, ignoredGw *v1.Gateway hr *v1.HTTPRoute ng *ngfAPI.NginxGateway + btls *v1alpha2.BackendTLSPolicy addr = v1.GatewayStatusAddress{ Type: helpers.GetPointer(v1.IPAddressType), Value: "1.2.3.4", @@ -118,6 +123,17 @@ var _ = Describe("Updater", func() { }, }, }, + BackendTLSPolicyStatuses: status.BackendTLSPolicyStatuses{ + {Namespace: "test", Name: "backend-tls-policy"}: { + ObservedGeneration: gens.backendTLSPolicies, + AncestorStatuses: []status.AncestorStatus{ + { + GatewayNsName: types.NamespacedName{Namespace: "test", Name: "gateway"}, + Conditions: status.CreateTestConditions("Test"), + }, + }, + }, + }, } } @@ -233,6 +249,31 @@ var _ = Describe("Updater", func() { } } + createExpectedBtlsWithGeneration = func(gen int64) *v1alpha2.BackendTLSPolicy { + return &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "backend-tls-policy", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "BackendTLSPolicy", + APIVersion: "gateway.networking.k8s.io/v1alpha2", + }, + Status: v1alpha2.PolicyStatus{ + Ancestors: []v1alpha2.PolicyAncestorStatus{ + { + AncestorRef: v1.ParentReference{ + Namespace: (*v1.Namespace)(helpers.GetPointer("test")), + Name: "gateway", + }, + ControllerName: v1alpha2.GatewayController(gatewayCtrlName), + Conditions: status.CreateExpectedAPIConditions("Test", gen, fakeClockTime), + }, + }, + }, + } + } + createExpectedNGWithGeneration = func(gen int64) *ngfAPI.NginxGateway { return &ngfAPI.NginxGateway{ ObjectMeta: metav1.ObjectMeta{ @@ -299,6 +340,16 @@ var _ = Describe("Updater", func() { APIVersion: "gateway.networking.k8s.io/v1", }, } + btls = &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: "backend-tls-policy", + }, + TypeMeta: metav1.TypeMeta{ + Kind: "BackendTLSPolicy", + APIVersion: "gateway.networking.k8s.io/v1alpha2", + }, + } ng = &ngfAPI.NginxGateway{ ObjectMeta: metav1.ObjectMeta{ Namespace: "nginx-gateway", @@ -317,12 +368,14 @@ var _ = Describe("Updater", func() { Expect(client.Create(context.Background(), ignoredGw)).Should(Succeed()) Expect(client.Create(context.Background(), hr)).Should(Succeed()) Expect(client.Create(context.Background(), ng)).Should(Succeed()) + Expect(client.Create(context.Background(), btls)).Should(Succeed()) }) It("should update gateway API statuses", func() { updater.Update(context.Background(), createGwAPIStatuses(generations{ - gatewayClass: 1, - gateways: 1, + gatewayClass: 1, + gateways: 1, + backendTLSPolicies: 1, })) }) @@ -378,6 +431,22 @@ var _ = Describe("Updater", func() { Expect(helpers.Diff(expectedHR, latestHR)).To(BeEmpty()) }) + It("should have the updated status of BackendTLSPolicy in the API server", func() { + latestBtls := &v1alpha2.BackendTLSPolicy{} + expectedBtls := createExpectedBtlsWithGeneration(1) + + err := client.Get( + context.Background(), + types.NamespacedName{Namespace: "test", Name: "backend-tls-policy"}, + latestBtls, + ) + Expect(err).ToNot(HaveOccurred()) + + expectedBtls.ResourceVersion = latestBtls.ResourceVersion + + Expect(helpers.Diff(expectedBtls, latestBtls)).To(BeEmpty()) + }) + It("should update nginx gateway status", func() { updater.Update(context.Background(), createNGStatus(1)) }) diff --git a/internal/mode/static/build_statuses.go b/internal/mode/static/build_statuses.go index a252b84b28..3de40454cc 100644 --- a/internal/mode/static/build_statuses.go +++ b/internal/mode/static/build_statuses.go @@ -196,10 +196,10 @@ func buildGatewayStatus( func buildBackendTLSPolicyStatuses(backendTLSPolicies map[types.NamespacedName]*graph.BackendTLSPolicy, ) status.BackendTLSPolicyStatuses { statuses := make(status.BackendTLSPolicyStatuses, len(backendTLSPolicies)) - ignoreStatus := false for nsname, backendTLSPolicy := range backendTLSPolicies { if backendTLSPolicy.IsReferenced { + ignoreStatus := false if !backendTLSPolicy.Valid { for i := range backendTLSPolicy.Conditions { if backendTLSPolicy.Conditions[i].Reason == string(staticConds.BackendTLSPolicyReasonIgnored) { diff --git a/internal/mode/static/build_statuses_test.go b/internal/mode/static/build_statuses_test.go index 46defb8fad..e757875fac 100644 --- a/internal/mode/static/build_statuses_test.go +++ b/internal/mode/static/build_statuses_test.go @@ -9,6 +9,7 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" v1 "sigs.k8s.io/gateway-api/apis/v1" + v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" @@ -613,3 +614,106 @@ func TestBuildGatewayStatuses(t *testing.T) { }) } } + +func TestBuildBackendTLSPolicyStatuses(t *testing.T) { + getBackendTLSPolicy := func( + name string, + valid bool, + isReferenced bool, + conditions []conditions.Condition, + ) *graph.BackendTLSPolicy { + return &graph.BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + Generation: 1, + }, + }, + Valid: valid, + IsReferenced: isReferenced, + Conditions: conditions, + Gateway: types.NamespacedName{Name: "gateway", Namespace: "test"}, + } + } + + attachedConds := []conditions.Condition{staticConds.NewBackendTLSPolicyAttached()} + invalidConds := []conditions.Condition{staticConds.NewBackendTLSPolicyInvalid("invalid backendTLSPolicy")} + ignoredConds := []conditions.Condition{staticConds.NewBackendTLSPolicyIgnored("ignored backendTLSPolicy")} + + tests := []struct { + backendTLSPolicies map[types.NamespacedName]*graph.BackendTLSPolicy + expected status.BackendTLSPolicyStatuses + name string + }{ + { + name: "nil backendTLSPolicies", + expected: status.BackendTLSPolicyStatuses{}, + }, + { + name: "valid backendTLSPolicy", + backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ + {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, true, attachedConds), + }, + expected: status.BackendTLSPolicyStatuses{ + {Namespace: "test", Name: "valid-bt"}: { + AncestorStatuses: []status.AncestorStatus{ + { + Conditions: attachedConds, + GatewayNsName: types.NamespacedName{Name: "gateway", Namespace: "test"}, + }, + }, + }, + }, + }, + { + name: "invalid backendTLSPolicy", + backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ + {Namespace: "test", Name: "invalid-bt"}: getBackendTLSPolicy("invalid-bt", false, true, invalidConds), + }, + expected: status.BackendTLSPolicyStatuses{ + {Namespace: "test", Name: "invalid-bt"}: { + AncestorStatuses: []status.AncestorStatus{ + { + Conditions: invalidConds, + GatewayNsName: types.NamespacedName{Name: "gateway", Namespace: "test"}, + }, + }, + }, + }, + }, + { + name: "ignored or not referenced backendTLSPolicies", + backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ + {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, ignoredConds), + {Namespace: "test", Name: "not-referenced"}: getBackendTLSPolicy("not-referenced", true, false, nil), + }, + expected: status.BackendTLSPolicyStatuses{}, + }, + { + name: "mix valid and ignored backendTLSPolicies", + backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ + {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, ignoredConds), + {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, true, attachedConds), + }, + expected: status.BackendTLSPolicyStatuses{ + {Namespace: "test", Name: "valid-bt"}: { + AncestorStatuses: []status.AncestorStatus{ + { + Conditions: attachedConds, + GatewayNsName: types.NamespacedName{Name: "gateway", Namespace: "test"}, + }, + }, + }, + }, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + result := buildBackendTLSPolicyStatuses(test.backendTLSPolicies) + g.Expect(helpers.Diff(test.expected, result)).To(BeEmpty()) + }) + } +} diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 8a5a1c1fb6..5c97f7a96b 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -190,6 +190,7 @@ func validateBackendTLSPolicyMatchingAllBackends(backendRefs []BackendRef) *cond if referencePolicy != nil { // There was a reference before, so they do not all match mismatch = true + break } continue } @@ -202,6 +203,7 @@ func validateBackendTLSPolicyMatchingAllBackends(backendRefs []BackendRef) *cond if backendRef.BackendTLSPolicy.Source.Name != referencePolicy.Source.Name || backendRef.BackendTLSPolicy.Source.Namespace != referencePolicy.Source.Namespace { mismatch = true + break } } } diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go index 1a2caba7c3..2677281647 100644 --- a/internal/mode/static/state/graph/backend_tls_policy.go +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -16,7 +16,7 @@ import ( type BackendTLSPolicy struct { // Source is the source resource. Source *v1alpha2.BackendTLSPolicy - // CaCartRef is the name of the ConfigMap that contains the CA certificate. + // CaCertRef is the name of the ConfigMap that contains the CA certificate. CaCertRef types.NamespacedName // Gateway is the name of the Gateway that is being checked for this BackendTLSPolicy. Gateway types.NamespacedName @@ -133,7 +133,7 @@ func validateBackendTLSCACertRef(btp *v1alpha2.BackendTLSPolicy, configMapResolv } if btp.Spec.TLS.CACertRefs[0].Group != "" && btp.Spec.TLS.CACertRefs[0].Group != "core" { path := field.NewPath("tls.cacertrefs[0].group") - valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Kind, []string{"", "core"}) + valErr := field.NotSupported(path, btp.Spec.TLS.CACertRefs[0].Group, []string{"", "core"}) return valErr } nsName := types.NamespacedName{Namespace: btp.Namespace, Name: string(btp.Spec.TLS.CACertRefs[0].Name)} diff --git a/internal/mode/static/state/graph/config_maps.go b/internal/mode/static/state/graph/configmaps.go similarity index 100% rename from internal/mode/static/state/graph/config_maps.go rename to internal/mode/static/state/graph/configmaps.go diff --git a/internal/mode/static/state/graph/config_maps_test.go b/internal/mode/static/state/graph/configmaps_test.go similarity index 100% rename from internal/mode/static/state/graph/config_maps_test.go rename to internal/mode/static/state/graph/configmaps_test.go From 498c43a8cce8c1e5378de354bbe9defdc47e257f Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Thu, 1 Feb 2024 17:14:39 +0000 Subject: [PATCH 06/13] Move example to docs --- examples/backend-tls/README.md | 89 ------- .../backend-tls/backend-certs-configmap.yaml | 27 -- examples/backend-tls/gateway.yaml | 10 - examples/backend-tls/policy.yaml | 16 -- examples/backend-tls/secure-app-routes.yaml | 18 -- examples/backend-tls/secure-app.yaml | 76 ------ .../backend-tls-termination.md | 247 ++++++++++++++++++ 7 files changed, 247 insertions(+), 236 deletions(-) delete mode 100644 examples/backend-tls/README.md delete mode 100644 examples/backend-tls/backend-certs-configmap.yaml delete mode 100644 examples/backend-tls/gateway.yaml delete mode 100644 examples/backend-tls/policy.yaml delete mode 100644 examples/backend-tls/secure-app-routes.yaml delete mode 100644 examples/backend-tls/secure-app.yaml create mode 100644 site/content/how-to/traffic-management/backend-tls-termination.md diff --git a/examples/backend-tls/README.md b/examples/backend-tls/README.md deleted file mode 100644 index 704ffae7f7..0000000000 --- a/examples/backend-tls/README.md +++ /dev/null @@ -1,89 +0,0 @@ -# Backend TLS Policy Example - -In this example, we will create a Backend TLS Policy, attach it to our application service, and then configure routing -rules. The Backend TLS Policy will be picked up by NGF and the connection between NGF and the upstream server will use -HTTPS. - -## Running the Example - -## 1. Deploy NGINX Gateway Fabric - -1. Follow the [installation instructions](https://docs.nginx.com/nginx-gateway-fabric/installation/) to deploy NGINX Gateway Fabric. - Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the - `- --experimental-features-enable` flag. - -2. Save the public IP address of NGINX Gateway Fabric into a shell variable: - - ```text - GW_IP=XXX.YYY.ZZZ.III - ``` - -3. Save the HTTPS port of NGINX Gateway Fabric: - - ```text - GW_HTTPS_PORT= - ``` - -## 2. Deploy the secure-app Application - -1. Create the secure-app Deployment and Service: - - ```shell - kubectl apply -f secure-app.yaml - ``` - -1. Check that the Pods are running in the `default` namespace: - - ```shell - kubectl -n default get pods - ``` - - ```text - NAME READY STATUS RESTARTS AGE - secure-app-575785644-b6nwh 1/1 Running 0 5s - ``` - -## 3. Deploy the Backend TLS Policy - -1. Create the ConfigMap that holds the `ca.crt` entry for verifying our self-signed certificates: - - ```shell - kubectl apply -f backend-certs-configmap.yaml - ``` - -2. Create the Backend TLS Policy which targets our `secure-app` Service and refers to our ConfigMap created in the - previous step: - - ```shell - kubectl apply -f policy.yaml - ``` - -## 3. Configure HTTP Termination and Routing - -1. Create the Gateway resource: - - ```shell - kubectl apply -f gateway.yaml - ``` - - This [Gateway](./gateway.yaml) configures: - - `http` listener for HTTP traffic - - `https` listener for HTTPS traffic. It terminates TLS connections using the `app-secret` we created in step 1. - -2. Create the HTTPRoute resources: - - ```shell - kubectl apply -f secure-app-routes.yaml - ``` - -## 4. Test the Application - -To access the application, we will use `curl` to send requests to the `secure-app` Service over HTTP. - -```shell -curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ -``` - -```text -hello from pod secure-app-575785644-749tq -``` diff --git a/examples/backend-tls/backend-certs-configmap.yaml b/examples/backend-tls/backend-certs-configmap.yaml deleted file mode 100644 index e8e52235bc..0000000000 --- a/examples/backend-tls/backend-certs-configmap.yaml +++ /dev/null @@ -1,27 +0,0 @@ -kind: ConfigMap -apiVersion: v1 -metadata: - name: backend-cert -data: - ca.crt: | - -----BEGIN CERTIFICATE----- - MIIDbTCCAlWgAwIBAgIUPA3fFnkLl63GZ7noUjb5NoLhSYkwDQYJKoZIhvcNAQEL - BQAwRjEfMB0GA1UEAwwWc2VjdXJlLWFwcC5leGFtcGxlLmNvbTELMAkGA1UEBhMC - VVMxFjAUBgNVBAcMDVNhbiBGcmFuc2lzY28wHhcNMjQwMTE4MTgwMTAxWhcNMjUw - MTA4MTgwMTAxWjBGMR8wHQYDVQQDDBZzZWN1cmUtYXBwLmV4YW1wbGUuY29tMQsw - CQYDVQQGEwJVUzEWMBQGA1UEBwwNU2FuIEZyYW5zaXNjbzCCASIwDQYJKoZIhvcN - AQEBBQADggEPADCCAQoCggEBAJGgn81BrqzmI4aQmGrg7RgkO5oYwlThQ9X/xVHB - YVFptjRPAZz9g92g5birI/NZ43C6nEbZrJrSCqN3wgvV84jJmBAgpAvW+LhF4caa - nhAnecJCcTbwrd542vCDoDRsNV5ffbpESgC4FxPGkRVbSa0KHQz8qCLqS2+uaB7X - t76iw6y4pQ3klobVp1XtUpzZMGMBqZFnsAdl+PWMmSTvqjixkSlfcUY6Crnk9W6d - Sns5cpzKdUs+2ZkBe6VkBgSs8xbaz8Y2YC1GhRqGlxYLT3WBaIlSCKPuRrGjwE3r - AsW6gSL919H1O1a+MjQuLuQ4lnCbCpNzM9OV1JISMWfwifMCAwEAAaNTMFEwHQYD - VR0OBBYEFOEzjs7FrQr1bW3mKkUgI+5Fo9XaMB8GA1UdIwQYMBaAFOEzjs7FrQr1 - bW3mKkUgI+5Fo9XaMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEB - AG/eX4pctINIrHvRyHOusdac5iXSJbQRZgWvP1F2p95qoIDESciAU1Sh1oJv+As5 - IlJOZPJNuZFpDLjc8kzSoEbc1Q5+QyTBlyNNsagWYYwK0CEJ6KJt80vytffmdOIg - z8/a+2Ax829vcn1w1SUi5V6ea/l8K74f2SL/zSSHgtEiz8V0TlvT7J6wurgmnk4t - yQRmsXlDGefuijMNCVf7jWwLx2BODfKoEA1pJkthnNvdizlikmz+9elxhV9bRf3Y - NnubytWPfO1oeHjVGvxVjCouIYine+VlskvwHmMi/dYod6yd7aFYu4CU3g/hjwKo - LY2WNv5j3JhDnEYK9Zj3z7A= - -----END CERTIFICATE----- diff --git a/examples/backend-tls/gateway.yaml b/examples/backend-tls/gateway.yaml deleted file mode 100644 index 9d402bd5a1..0000000000 --- a/examples/backend-tls/gateway.yaml +++ /dev/null @@ -1,10 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: Gateway -metadata: - name: gateway -spec: - gatewayClassName: nginx - listeners: - - name: http - port: 80 - protocol: HTTP diff --git a/examples/backend-tls/policy.yaml b/examples/backend-tls/policy.yaml deleted file mode 100644 index 8d4d75e1d5..0000000000 --- a/examples/backend-tls/policy.yaml +++ /dev/null @@ -1,16 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1alpha2 -kind: BackendTLSPolicy -metadata: - name: backend-tls -spec: - targetRef: - group: '' - kind: Service - name: secure-app - namespace: default - tls: - caCertRefs: - - name: backend-cert - group: '' - kind: ConfigMap - hostname: secure-app.example.com diff --git a/examples/backend-tls/secure-app-routes.yaml b/examples/backend-tls/secure-app-routes.yaml deleted file mode 100644 index 1fb788f5f6..0000000000 --- a/examples/backend-tls/secure-app-routes.yaml +++ /dev/null @@ -1,18 +0,0 @@ -apiVersion: gateway.networking.k8s.io/v1 -kind: HTTPRoute -metadata: - name: secure-app -spec: - parentRefs: - - name: gateway - sectionName: http - hostnames: - - "secure-app.example.com" - rules: - - matches: - - path: - type: PathPrefix - value: / - backendRefs: - - name: secure-app - port: 8443 diff --git a/examples/backend-tls/secure-app.yaml b/examples/backend-tls/secure-app.yaml deleted file mode 100644 index 46ed8d5e8e..0000000000 --- a/examples/backend-tls/secure-app.yaml +++ /dev/null @@ -1,76 +0,0 @@ -apiVersion: apps/v1 -kind: Deployment -metadata: - name: secure-app -spec: - replicas: 1 - selector: - matchLabels: - app: secure-app - template: - metadata: - labels: - app: secure-app - spec: - containers: - - name: secure-app - image: nginxdemos/nginx-unprivileged:plain-text - ports: - - containerPort: 8443 - volumeMounts: - - name: secret - mountPath: /etc/nginx/ssl - readOnly: true - - name: config-volume - mountPath: /etc/nginx/conf.d - volumes: - - name: secret - secret: - secretName: app-tls-secret - - name: config-volume - configMap: - name: secure-config ---- -apiVersion: v1 -kind: Service -metadata: - name: secure-app -spec: - ports: - - port: 8443 - targetPort: 8443 - protocol: TCP - name: https - selector: - app: secure-app ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: secure-config -data: - app.conf: |- - server { - listen 8443 ssl; - listen [::]:8443 ssl; - - server_name secure-app.example.com; - - ssl_certificate /etc/nginx/ssl/tls.crt; - ssl_certificate_key /etc/nginx/ssl/tls.key; - - default_type text/plain; - - location / { - return 200 "hello from pod $hostname\n"; - } - } ---- -apiVersion: v1 -kind: Secret -metadata: - name: app-tls-secret -type: Opaque -data: - tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQwRENDQXJpZ0F3SUJBZ0lVVDQwYTFYd3doUHVBdDJNMkdZZUovYXluZlFBd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1JqRWZNQjBHQTFVRUF3d1djMlZqZFhKbExXRndjQzVsZUdGdGNHeGxMbU52YlRFTE1Ba0dBMVVFQmhNQwpWVk14RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1YzJselkyOHdIaGNOTWpRd01URTRNVGd3TVRBeFdoY05NalV3Ck1URTNNVGd3TVRBeFdqQi9NUXN3Q1FZRFZRUUdFd0pWVXpFVE1CRUdBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVcKTUJRR0ExVUVCd3dOVTJGdUlFWnlZVzV6YVhOamJ6RU9NQXdHQTFVRUNnd0ZUa2RKVGxneEVqQVFCZ05WQkFzTQpDVTVIU1U1WUlFUmxkakVmTUIwR0ExVUVBd3dXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdmJUQ0NBU0l3CkRRWUpLb1pJaHZjTkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMeUx0eURNbTZ4M0ZEUFJsOGZ0azNweCtrRWQKYTVpTGZOQ3lDbUVjYktBQVBDNEhZckl5b1B5QXpSTlJCMWErekE0UTlrbzJZRG5vR0dkeFJaMEdydldKZUV2Mgo3MWlHNGxhbHRVTS9WOWNvSktQY0UyTEI0R3R6cFA3ckdIWXNvRDlOUXFpV3YwZ0lOdE42MjdrWGg4UW41V1hYCk92Y2FkS2h0bjJER3RvU0VzT3dpNzR5NEt3SmFkWnlwLzJaM0hPakRTNjVIVmxydmUxUXpBMVRzTEp6S3cva3gKbHBSR0lWK0lhUjZXbXZsaVFVdDJxWFg0L3hGeVVEM2Vic05TeXpHUk5mQ0NOTWxlWlV3MTR3ZUdhOEVnc2tDcQprOGdYSmpFZXQxMlR4OGxkY3BpVWlxYVpkOStYZjJmUS8yL2Y5c1IzM3Q4K0VVUWpoZ2ZIbHlsLzV1RUNBd0VBCkFhTjlNSHN3SHdZRFZSMGpCQmd3Rm9BVTRUT096c1d0Q3ZWdGJlWXFSU0FqN2tXajFkb3dDUVlEVlIwVEJBSXcKQURBTEJnTlZIUThFQkFNQ0JQQXdJUVlEVlIwUkJCb3dHSUlXYzJWamRYSmxMV0Z3Y0M1bGVHRnRjR3hsTG1OdgpiVEFkQmdOVkhRNEVGZ1FVZmtWREFFWmIwcjRTZ2swck10a0FvQ2c2RjRnd0RRWUpLb1pJaHZjTkFRRUxCUUFECmdnRUJBQWFiQit6RzVSODl6WitBT2RsRy9wWE9nYjF6VkJsQ0dMSkhyYTl1cTMvcXRPR1VacDlnd2dZSWJ4VnkKUkVLbWVRa05pV0haSDNCSlNTZ3czbE9abGNxcW5xbUJ2OFAxTUxDZ3JqbDJSN1d2NVhkb2RlQkJxc0lvZkNxVgp3ZG51THJUU3RTbmd2MGhDcldBNlBmTnlQeXMzSGJva1k3RExNREhuNmhBQWcwMUNDT0pWWGpNZjFqLzNIMFNCClBQSWxtek5aRUpEd0JMR2hyb1V3aUY3NkNUV1Fudi8yc1pvWHMwUlFiRTY3TmNraXc2Z0svaWRwVTVzMmlkOEQKVExjVjNxenVFaE1ZeUlua0ZWNEJLZlFkTWxDQnE1QWdyU1Jqb2FoaCszbFRwYVpUalJGUGFVd3VZYXVsQXRzNgpra1ROaGltWWQ3Ym1aVk5MK2I0MzhmN1RMaGc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0= - tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2Z0lCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktnd2dnU2tBZ0VBQW9JQkFRQzhpN2Nnekp1c2R4UXoKMFpmSDdaTjZjZnBCSFd1WWkzelFzZ3BoSEd5Z0FEd3VCMkt5TXFEOGdNMFRVUWRXdnN3T0VQWktObUE1NkJobgpjVVdkQnE3MWlYaEw5dTlZaHVKV3BiVkRQMWZYS0NTajNCTml3ZUJyYzZUKzZ4aDJMS0EvVFVLb2xyOUlDRGJUCmV0dTVGNGZFSitWbDF6cjNHblNvYlo5Z3hyYUVoTERzSXUrTXVDc0NXbldjcWY5bWR4em93MHV1UjFaYTczdFUKTXdOVTdDeWN5c1A1TVphVVJpRmZpR2tlbHByNVlrRkxkcWwxK1A4UmNsQTkzbTdEVXNzeGtUWHdnalRKWG1WTQpOZU1IaG12QklMSkFxcFBJRnlZeEhyZGRrOGZKWFhLWWxJcW1tWGZmbDM5bjBQOXYzL2JFZDk3ZlBoRkVJNFlICng1Y3BmK2JoQWdNQkFBRUNnZ0VBUXJucGJleXJmVTVKTW91Ty9UenBrQkJ0UWdVZzhvUVBBS2E1d0tONEYrbnQKWWxiUHlZUGNjSEErNDRLdUo3ZHZiTno0NU11NG8xV3Q2Vkh2a29KdWdjd01iRW53YTdLVXdKaDFmVjZaL2pXaApQZkpoVS9hTUwwcm1qaWJ5YWNRaVZEVEtEZk1Ici96a05sVEpGUWlzVGpIV1lBUGJSTjh5Z1BjR3pBK1hRVzg5CmxsOFdoeWwrZndjRVAzNTM4TUdKYUpHVmV4Q2d5cVRyKzZwQ29yRUpFL1pSNytiMTNyejRsbmxpZXVYa2pCVkcKSnYvUVI0RVhTSDhuRit3K2FvWTFQaGd2QnFJTnFQZjJMT1V0MzNiN3FDTkFmSVBRdFZFejJsN3NqbmJlcElTTwpvTkhNUFY0N21XTzY2dzhFMzJVMjNjVEtUbytJcWovM0d1eGhweXlYaHdLQmdRRDVNRDhmY3ZyM0xVZHkvZ3I0Ci82MVBqUXNSaWRYdjN0Mk1MVEk4UkduNzJWcGxvVVExNDZHV2xGTGVVVDY1L1ZVMjNsZFUrUWt2eFNMK3U1bW4KRUJIdXUyVmtBUWVYcUJXWkpPTmFSZG9Ia1YzK2Fyc0U4Qld0SWVtZHJ0MWN6bHFjc1VzMWdGdG1COGI3RHB3UwpHKzRoZDlzZG0weDBNS2hoOFVGOUQrZytUd0tCZ1FEQnN4dUpoZ1hnck14S0dVMHJ5MW9WWTJtMGpDUEpJcTkzCmNZRUZGY3lYZ0U4OWlDVmlob3dVVE8yMXpTZ3o0SVVKNUxoc2M5N3VIVER1VXdwcVI5NFBsdjlyaXJvakowM1UKT3FyWHgwbWdNN2xibVM1L1RwS0czZG1QblZ0WEZMektISFgyWDVnUW56emYvdXVKK3NtbDVLQW5WN0VZc1oxcgpkVXJvRm8zcnp3S0JnUUNZdjM5aUdzeEdLaVpMRWZqTjY0UmthRFBwdTFFOTZhSnE0K1dRVmV1VnF3V2ptTGhFClJGWHdCTm5MVjRnWTRIYVUzTFF4N1RvNVl5RnhmclBRV2FSMGI4RFdEVitIRWt5ekJJNnM3bmFZL3YzY0Q3YTIKYnlrS2FPaFlkVEZTUzFmMkJ5UHdGczl2K3NKNWNOb3dxNWhNUWJrNks5RXd4QWJqaXN5M0NjSTJOd0tCZ1FDbwo2c2pZNVVlNjV2WkFxRS9rSVRJdDlNUDU3enhGNnptWnNDSVRqUzhkNzRjcTRjKzRYQjFNbHNtMkFYTk55ajQ2Cm9uc3lHTm9RVE9TZThVdmo0MGlEeitwdW5rdzAyOUhEZ21YNlJwQ3VaRzBBdEZVWU1DMFg3K0FLbmU5SndZdmgKdFhBcHFyT3h5eXdMS3dPOUVEZEp0RmIxK0VNNGhhd0NTZ2RJM21KbGdRS0JnRzIxeEJNRXRzMFBVN3lDYTZ0YwpadDc1NUV4aEdkR3F5MmtHYmtmdzBEaHBQQVVUZmdncVF3NVBYdGVIS1ZBSDlKaG5kVnBBZFFxNmZ1MER1MDNKCkl0cGpxNWluZXVoR0x0alpMR1Nhd0dwY0FUU3h4Z3dCM0l0Z29LKzBCRFhteWxId0lEcUc5Z2crRU5KK0VhL0MKeTFOMmV0ZG1sQ01hNjM4cVJlNFlTWk55Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0= diff --git a/site/content/how-to/traffic-management/backend-tls-termination.md b/site/content/how-to/traffic-management/backend-tls-termination.md new file mode 100644 index 0000000000..92679c7f07 --- /dev/null +++ b/site/content/how-to/traffic-management/backend-tls-termination.md @@ -0,0 +1,247 @@ +--- +title: "Backend TLS Termination" +description: "Learn how to encrypt HTTP traffic between NGINX Gateway Fabric and your backend pods." +weight: 600 +toc: true +docs: "DOCS-000" +--- + +In this guide, we will show how to specify the TLS configuration of the connection from the Gateway to a backend pod/s via the Service API object using a [BackendTLSPolicy](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/). + +## Prerequisites + +- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the `- --experimental-features-enable` flag.. +- [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables: + + ```text + GW_IP=XXX.YYY.ZZZ.III + GW_PORT= + ``` + +{{< note >}}In a production environment, you should have a DNS record for the external IP address that is exposed, and it should refer to the hostname that the gateway will forward for.{{< /note >}} + +## Set up + +Create the **secure-app** application in Kubernetes by copying and pasting the following block into your terminal: + +```yaml +kubectl apply -f - < 8443/TCP 9s +``` + +## Create the Backend TLS configuration + +Create the ConfigMap that holds the `ca.crt` entry for verifying our self-signed certificates: + +```yaml +kubectl apply -f - <}}If you have a DNS record allocated for `secure-app.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} + +```shell +curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ --insecure +``` + +```text +hello from pod secure-app-868cfd5b5-v7gwk +``` + +## Further Reading + +To learn more about configuring backend TLS termination using the Gateway API, see the following resources: + +- [Backend TLS Policy](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/) +- [Backend TLS Policy GEP](https://gateway-api.sigs.k8s.io/geps/gep-1897/) From 105c34923907b9efdeca37100da429eeb74b790c Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Fri, 2 Feb 2024 12:11:02 +0000 Subject: [PATCH 07/13] Improve how-to guide --- .../backend-tls-termination.md | 164 +++++++++++++----- 1 file changed, 116 insertions(+), 48 deletions(-) diff --git a/site/content/how-to/traffic-management/backend-tls-termination.md b/site/content/how-to/traffic-management/backend-tls-termination.md index 92679c7f07..a35f025b90 100644 --- a/site/content/how-to/traffic-management/backend-tls-termination.md +++ b/site/content/how-to/traffic-management/backend-tls-termination.md @@ -6,11 +6,11 @@ toc: true docs: "DOCS-000" --- -In this guide, we will show how to specify the TLS configuration of the connection from the Gateway to a backend pod/s via the Service API object using a [BackendTLSPolicy](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/). +In this guide, we will show how to specify the TLS configuration of the connection from the Gateway to a backend pod/s via the Service API object using a [BackendTLSPolicy](https://gateway-api.sigs.k8s.io/api-types/backendtlspolicy/). This covers the use-case where the service or backend owner is doing their own TLS and NGINX Gateway Fabric needs to know how to connect to this backend pod that has its own certificate over HTTPS. ## Prerequisites -- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the `- --experimental-features-enable` flag.. +- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the `- --experimental-features-enable` flag. - [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables: ```text @@ -106,7 +106,7 @@ data: EOF ``` -This will create the **secure-app** service and a deployment, as well as a Secret containing the certificate and key that will be used by the backend application to decrypt the HTTP traffic. Note that the application is configured to accept HTTPS traffic only. Run the following command to verify the resources were created: +This will create the **secure-app** service and a deployment, as well as a Secret containing the certificate and key that will be used by the backend application to decrypt the HTTPS traffic. Note that the application is configured to accept HTTPS traffic only. Run the following command to verify the resources were created: ```shell kubectl get pods,svc @@ -122,9 +122,76 @@ NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/secure-app ClusterIP 10.96.213.57 8443/TCP 9s ``` +## Configure Routing rules + +First, we will create the Gateway resource with an HTTP listener: + +```yaml +kubectl apply -f - <}}If you have a DNS record allocated for `secure-app.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} + +```shell +curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ --insecure +``` + +```text + +400 The plain HTTP request was sent to HTTPS port + +

400 Bad Request

+
The plain HTTP request was sent to HTTPS port
+
nginx/1.25.3
+ + +``` + +We can see we a status 400 Bad Request message from NGINX. + ## Create the Backend TLS configuration -Create the ConfigMap that holds the `ca.crt` entry for verifying our self-signed certificates: +To configure the backend TLS terminationm, first we will create the ConfigMap that holds the `ca.crt` entry for verifying our self-signed certificates: ```yaml kubectl apply -f - < +Annotations: +API Version: gateway.networking.k8s.io/v1alpha2 +Kind: BackendTLSPolicy +Metadata: + Creation Timestamp: 2024-02-01T12:02:38Z + Generation: 1 + Resource Version: 19380 + UID: b3983a6e-92f1-4a98-b2af-64b317d74528 +Spec: + Target Ref: + Group: + Kind: Service + Name: secure-app + Namespace: default + Tls: + Ca Cert Refs: + Group: + Kind: ConfigMap + Name: backend-cert + Hostname: secure-app.example.com +Status: + Ancestors: + Ancestor Ref: + Group: gateway.networking.k8s.io + Kind: Gateway + Name: gateway + Namespace: default + Conditions: + Last Transition Time: 2024-02-01T12:02:38Z + Message: BackendTLSPolicy is attached to the Gateway + Reason: BackendTLSPolicyAttached + Status: True + Type: Attached + Controller Name: gateway.nginx.org/nginx-gateway-controller +Events: ``` -## Send Traffic +## Send Traffic with backend TLS configuration -Using the external IP address and port for NGINX Gateway Fabric, we can send traffic to our secure-app application. - -{{< note >}}If you have a DNS record allocated for `secure-app.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} +Now let's try sending traffic again: ```shell curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ --insecure From 549cb1d1f9e4042a486f0d2a58ef238594caaaaa Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Wed, 7 Feb 2024 20:49:54 +0000 Subject: [PATCH 08/13] Review feedback --- cmd/gateway/commands.go | 10 +- deploy/helm-chart/templates/deployment.yaml | 4 +- deploy/helm-chart/values.yaml | 2 +- docs/developer/quickstart.md | 2 +- internal/framework/status/setters_test.go | 13 + internal/framework/status/statuses.go | 1 + internal/mode/static/build_statuses.go | 11 +- internal/mode/static/build_statuses_test.go | 17 +- internal/mode/static/manager.go | 4 + .../mode/static/nginx/config/generator.go | 10 +- internal/mode/static/nginx/config/servers.go | 2 +- .../mode/static/nginx/config/servers_test.go | 23 +- .../static/state/conditions/conditions.go | 31 +- .../static/state/dataplane/configuration.go | 35 +- .../state/dataplane/configuration_test.go | 2 +- .../mode/static/state/graph/backend_refs.go | 29 +- .../static/state/graph/backend_refs_test.go | 318 +++++++++++++----- .../static/state/graph/backend_tls_policy.go | 83 +++-- .../state/graph/backend_tls_policy_test.go | 12 +- .../mode/static/state/graph/graph_test.go | 4 +- ...ination.md => securing-backend-traffic.md} | 22 +- .../installation/installing-ngf/manifests.md | 4 +- .../overview/gateway-api-compatibility.md | 11 +- site/content/reference/cli-help.md | 38 +-- 24 files changed, 429 insertions(+), 259 deletions(-) rename site/content/how-to/traffic-management/{backend-tls-termination.md => securing-backend-traffic.md} (95%) diff --git a/cmd/gateway/commands.go b/cmd/gateway/commands.go index 3ba3055af3..7265f5c4a7 100644 --- a/cmd/gateway/commands.go +++ b/cmd/gateway/commands.go @@ -56,7 +56,7 @@ func createStaticModeCommand() *cobra.Command { leaderElectionDisableFlag = "leader-election-disable" leaderElectionLockNameFlag = "leader-election-lock-name" plusFlag = "nginx-plus" - experimentalEnableFlag = "experimental-features-enable" + gwAPIExperimentalFlag = "gateway-api-experimental-features" ) // flag values @@ -97,7 +97,7 @@ func createStaticModeCommand() *cobra.Command { plus bool - enableExperimental bool + gwExperimentalFeatures bool ) cmd := &cobra.Command{ @@ -175,7 +175,7 @@ func createStaticModeCommand() *cobra.Command { Plus: plus, TelemetryReportPeriod: period, Version: version, - ExperimentalFeatures: enableExperimental, + ExperimentalFeatures: gwExperimentalFeatures, } if err := static.StartManager(conf); err != nil { @@ -290,8 +290,8 @@ func createStaticModeCommand() *cobra.Command { ) cmd.Flags().BoolVar( - &enableExperimental, - experimentalEnableFlag, + &gwExperimentalFeatures, + gwAPIExperimentalFlag, false, "Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. "+ "Requires the Gateway APIs installed from the experimental channel.", diff --git a/deploy/helm-chart/templates/deployment.yaml b/deploy/helm-chart/templates/deployment.yaml index 8061c6012e..79b0e242cb 100644 --- a/deploy/helm-chart/templates/deployment.yaml +++ b/deploy/helm-chart/templates/deployment.yaml @@ -52,8 +52,8 @@ spec: {{- else }} - --leader-election-disable {{- end }} - {{- if .Values.nginxGateway.experimentalFeatures.enable }} - - --experimental-features-enable + {{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }} + - --gateway-api-experimental-features {{- end }} env: - name: POD_IP diff --git a/deploy/helm-chart/values.yaml b/deploy/helm-chart/values.yaml index 57923d5cdb..8154343043 100644 --- a/deploy/helm-chart/values.yaml +++ b/deploy/helm-chart/values.yaml @@ -51,7 +51,7 @@ nginxGateway: ## extraVolumeMounts are the additional volume mounts for the nginx-gateway container. extraVolumeMounts: [] - experimentalFeatures: + gwAPIExperimentalFeatures: ## Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. Requires the Gateway ## APIs installed from the experimental channel. enable: false diff --git a/docs/developer/quickstart.md b/docs/developer/quickstart.md index c0e4920b64..6675dcefa9 100644 --- a/docs/developer/quickstart.md +++ b/docs/developer/quickstart.md @@ -128,7 +128,7 @@ This will build the docker images `nginx-gateway-fabric:` and `nginx- kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/standard-install.yaml ``` - Alternatively, install Gateway API CRDs from the experimental channel: + If you're implementing experimental Gateway API features, install Gateway API CRDs from the experimental channel: ```shell kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.0.0/experimental-install.yaml diff --git a/internal/framework/status/setters_test.go b/internal/framework/status/setters_test.go index 199b317415..b86f14a133 100644 --- a/internal/framework/status/setters_test.go +++ b/internal/framework/status/setters_test.go @@ -865,6 +865,12 @@ func TestBtpStatusEqual(t *testing.T) { }, } } + prevMultiple := getPolicyStatus("ancestor1", "ns1", "ctlr1") + prevMultiple.Ancestors = append(prevMultiple.Ancestors, getPolicyStatus("ancestor2", "ns2", "ctlr2").Ancestors...) + + currMultiple := getPolicyStatus("ancestor1", "ns1", "ctlr1") + currMultiple.Ancestors = append(currMultiple.Ancestors, getPolicyStatus("ancestor3", "ns3", "ctlr2").Ancestors...) + tests := []struct { name string controllerName string @@ -907,6 +913,13 @@ func TestBtpStatusEqual(t *testing.T) { controllerName: "ctlr1", expEqual: false, }, + { + name: "status not equal, different controller ancestor changed", + previous: prevMultiple, + current: currMultiple, + controllerName: "ctlr1", + expEqual: false, + }, } for _, test := range tests { diff --git a/internal/framework/status/statuses.go b/internal/framework/status/statuses.go index 4b2727418b..1de369768b 100644 --- a/internal/framework/status/statuses.go +++ b/internal/framework/status/statuses.go @@ -116,6 +116,7 @@ type GatewayClassStatus struct { ObservedGeneration int64 } +// AncestorStatus holds status-related information related to how the BackendTLSPolicy binds to a specific ancestorRef. type AncestorStatus struct { // GatewayNsName is the Namespaced name of the Gateway, which the ancestorRef references. GatewayNsName types.NamespacedName diff --git a/internal/mode/static/build_statuses.go b/internal/mode/static/build_statuses.go index 3de40454cc..6055d9fdd1 100644 --- a/internal/mode/static/build_statuses.go +++ b/internal/mode/static/build_statuses.go @@ -199,16 +199,7 @@ func buildBackendTLSPolicyStatuses(backendTLSPolicies map[types.NamespacedName]* for nsname, backendTLSPolicy := range backendTLSPolicies { if backendTLSPolicy.IsReferenced { - ignoreStatus := false - if !backendTLSPolicy.Valid { - for i := range backendTLSPolicy.Conditions { - if backendTLSPolicy.Conditions[i].Reason == string(staticConds.BackendTLSPolicyReasonIgnored) { - // We should not report the status of an ignored BackendTLSPolicy. - ignoreStatus = true - } - } - } - if !ignoreStatus { + if !backendTLSPolicy.Ignored { statuses[nsname] = status.BackendTLSPolicyStatus{ AncestorStatuses: []status.AncestorStatus{ { diff --git a/internal/mode/static/build_statuses_test.go b/internal/mode/static/build_statuses_test.go index e757875fac..65f27a1c9b 100644 --- a/internal/mode/static/build_statuses_test.go +++ b/internal/mode/static/build_statuses_test.go @@ -619,6 +619,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { getBackendTLSPolicy := func( name string, valid bool, + ignored bool, isReferenced bool, conditions []conditions.Condition, ) *graph.BackendTLSPolicy { @@ -631,15 +632,15 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { }, }, Valid: valid, + Ignored: ignored, IsReferenced: isReferenced, Conditions: conditions, Gateway: types.NamespacedName{Name: "gateway", Namespace: "test"}, } } - attachedConds := []conditions.Condition{staticConds.NewBackendTLSPolicyAttached()} + attachedConds := []conditions.Condition{staticConds.NewBackendTLSPolicyAccepted()} invalidConds := []conditions.Condition{staticConds.NewBackendTLSPolicyInvalid("invalid backendTLSPolicy")} - ignoredConds := []conditions.Condition{staticConds.NewBackendTLSPolicyIgnored("ignored backendTLSPolicy")} tests := []struct { backendTLSPolicies map[types.NamespacedName]*graph.BackendTLSPolicy @@ -653,7 +654,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { { name: "valid backendTLSPolicy", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, true, attachedConds), + {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, false, true, attachedConds), }, expected: status.BackendTLSPolicyStatuses{ {Namespace: "test", Name: "valid-bt"}: { @@ -669,7 +670,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { { name: "invalid backendTLSPolicy", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "invalid-bt"}: getBackendTLSPolicy("invalid-bt", false, true, invalidConds), + {Namespace: "test", Name: "invalid-bt"}: getBackendTLSPolicy("invalid-bt", false, false, true, invalidConds), }, expected: status.BackendTLSPolicyStatuses{ {Namespace: "test", Name: "invalid-bt"}: { @@ -685,16 +686,16 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { { name: "ignored or not referenced backendTLSPolicies", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, ignoredConds), - {Namespace: "test", Name: "not-referenced"}: getBackendTLSPolicy("not-referenced", true, false, nil), + {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, true, nil), + {Namespace: "test", Name: "not-referenced"}: getBackendTLSPolicy("not-referenced", true, false, false, nil), }, expected: status.BackendTLSPolicyStatuses{}, }, { name: "mix valid and ignored backendTLSPolicies", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, ignoredConds), - {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, true, attachedConds), + {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, true, nil), + {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, false, true, attachedConds), }, expected: status.BackendTLSPolicyStatuses{ {Namespace: "test", Name: "valid-bt"}: { diff --git a/internal/mode/static/manager.go b/internal/mode/static/manager.go index 719305d02a..6d63d61626 100644 --- a/internal/mode/static/manager.go +++ b/internal/mode/static/manager.go @@ -388,9 +388,13 @@ func registerControllers( backendTLSObjs := []ctlrCfg{ { objectType: &gatewayv1alpha2.BackendTLSPolicy{}, + options: []controller.Option{ + controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}), + }, }, { // FIXME(ciarams87): If possible, use only metadata predicate + // https://github.com/nginxinc/nginx-gateway-fabric/issues/1545 objectType: &apiv1.ConfigMap{}, }, } diff --git a/internal/mode/static/nginx/config/generator.go b/internal/mode/static/nginx/config/generator.go index b20c51caed..4917a80cac 100644 --- a/internal/mode/static/nginx/config/generator.go +++ b/internal/mode/static/nginx/config/generator.go @@ -1,7 +1,6 @@ package config import ( - "encoding/base64" "path/filepath" "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/nginx/file" @@ -96,15 +95,10 @@ func generatePEMFileName(id dataplane.SSLKeyPairID) string { } func generateCertBundle(id dataplane.CertBundleID, cert []byte) file.File { - data := make([]byte, base64.StdEncoding.DecodedLen(len(cert))) - _, err := base64.StdEncoding.Decode(data, cert) - if err != nil { - data = cert - } return file.File{ - Content: data, + Content: cert, Path: generateCertBundleFileName(id), - Type: file.TypeSecret, + Type: file.TypeRegular, } } diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index 7c8401aa14..aa6f5ab076 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -283,7 +283,7 @@ func createProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVeri } func createProxySSLVerify(v *dataplane.VerifyTLS) *http.ProxySSLVerify { - if v == nil || v.Hostname == "" { + if v == nil { return nil } var trustedCert string diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index d74e0b2383..0e83bac5a1 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -1869,10 +1869,7 @@ func TestConvertBackendTLSFromGroup(t *testing.T) { UpstreamName: "my-upstream", Valid: true, Weight: 1, - VerifyTLS: &dataplane.VerifyTLS{ - CertBundleID: "", - Hostname: "", - }, + VerifyTLS: nil, }, }, expected: nil, @@ -1900,6 +1897,24 @@ func TestConvertBackendTLSFromGroup(t *testing.T) { Name: "my-hostname", }, }, + { + msg: "tls enabled, system certs enabled", + grp: []dataplane.Backend{ + { + UpstreamName: "my-upstream", + Valid: true, + Weight: 1, + VerifyTLS: &dataplane.VerifyTLS{ + Hostname: "my-hostname", + RootCAPath: "/etc/ssl/certs/ca-certificates.crt", + }, + }, + }, + expected: &http.ProxySSLVerify{ + TrustedCertificate: "/etc/ssl/certs/ca-certificates.crt", + Name: "my-hostname", + }, + }, } for _, tc := range tests { diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go index c1205b375f..454f51aa52 100644 --- a/internal/mode/static/state/conditions/conditions.go +++ b/internal/mode/static/state/conditions/conditions.go @@ -5,6 +5,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" v1 "sigs.k8s.io/gateway-api/apis/v1" + "sigs.k8s.io/gateway-api/apis/v1alpha2" ngfAPI "github.com/nginxinc/nginx-gateway-fabric/apis/v1alpha1" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" @@ -73,9 +74,6 @@ const ( // BackendTLSPolicyReasonAttached is the condition reason for the BackendTLSPolicy Attached condition. BackendTLSPolicyReasonAttached BackendTLSPolicyConditionReason = "BackendTLSPolicyAttached" - // BackendTLSPolicyReasonIgnored is the condition reason for the BackendTLSPolicy being ignored by this Gateway. - BackendTLSPolicyReasonIgnored BackendTLSPolicyConditionReason = "BackendTLSPolicyIgnored" - // BackendTLSPolicyConditionValid is the condition type indicating whether the BackendTLS policy is valid. BackendTLSPolicyConditionValid BackendTLSPolicyConditionType = "Valid" @@ -570,34 +568,23 @@ func NewNginxGatewayInvalid(msg string) conditions.Condition { } } -// NewBackendTLSPolicyAttached returns a Condition that indicates that the BackendTLSPolicy config is valid and attached -// to the Gateway. -func NewBackendTLSPolicyAttached() conditions.Condition { +// NewBackendTLSPolicyAccepted returns a Condition that indicates that the BackendTLSPolicy config is valid and accepted +// by the Gateway. +func NewBackendTLSPolicyAccepted() conditions.Condition { return conditions.Condition{ - Type: string(BackendTLSPolicyConditionAttached), + Type: string(v1alpha2.PolicyConditionAccepted), Status: metav1.ConditionTrue, - Reason: string(BackendTLSPolicyReasonAttached), - Message: "BackendTLSPolicy is attached to the Gateway", - } -} - -// NewBackendTLSPolicyIgnored returns a Condition that indicates that the BackendTLSPolicy config cannot be attached to -// the Gateway and will be ignored. -func NewBackendTLSPolicyIgnored(msg string) conditions.Condition { - return conditions.Condition{ - Type: string(BackendTLSPolicyConditionAttached), - Status: metav1.ConditionFalse, - Reason: string(BackendTLSPolicyReasonIgnored), - Message: msg, + Reason: string(v1alpha2.PolicyReasonAccepted), + Message: "BackendTLSPolicy is accepted by the Gateway", } } // NewBackendTLSPolicyInvalid returns a Condition that indicates that the BackendTLSPolicy config is invalid. func NewBackendTLSPolicyInvalid(msg string) conditions.Condition { return conditions.Condition{ - Type: string(BackendTLSPolicyConditionValid), + Type: string(v1alpha2.PolicyConditionAccepted), Status: metav1.ConditionFalse, - Reason: string(BackendTLSPolicyReasonInvalid), + Reason: string(v1alpha2.PolicyReasonInvalid), Message: msg, } } diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index ba670bda9b..25ed7e217a 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -2,8 +2,8 @@ package dataplane import ( "context" + "encoding/base64" "fmt" - "os" "sort" apiv1 "k8s.io/api/core/v1" @@ -15,7 +15,10 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/resolver" ) -const wildcardHostname = "~^" +const ( + wildcardHostname = "~^" + alpineSSLRootCAPath = "/etc/ssl/cert.pem" +) // BuildConfiguration builds the Configuration from the Graph. func BuildConfiguration( @@ -102,7 +105,13 @@ func buildCertBundles( id := generateCertBundleID(cmName) if _, exists := refByBG[id]; exists { if cm.CACert != nil || len(cm.CACert) > 0 { - bundles[id] = CertBundle(cm.CACert) + // the cert could be base64 encoded or plaintext + data := make([]byte, base64.StdEncoding.DecodedLen(len(cm.CACert))) + _, err := base64.StdEncoding.Decode(data, cm.CACert) + if err != nil { + data = cm.CACert + } + bundles[id] = CertBundle(data) } } } @@ -179,30 +188,12 @@ func convertBackendTLS(btp *graph.BackendTLSPolicy) *VerifyTLS { if btp.CaCertRef.Name != "" { verify.CertBundleID = generateCertBundleID(btp.CaCertRef) } else { - verify.RootCAPath = getRootCAPath() + verify.RootCAPath = alpineSSLRootCAPath } verify.Hostname = string(btp.Source.Spec.TLS.Hostname) return verify } -// getRootCAPath returns the path to the root CA certificate bundle. -func getRootCAPath() string { - certFiles := []string{ - "/etc/ssl/cert.pem", // Alpine Linux - "/etc/ssl/certs/ca-certificates.crt", // Debian/Ubuntu/Gentoo etc. - "/etc/pki/tls/certs/ca-bundle.crt", // Fedora/RHEL 6 - "/etc/ssl/ca-bundle.pem", // OpenSUSE - "/etc/pki/tls/cacert.pem", // OpenELEC - "/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem", // CentOS/RHEL 7 - } - for _, certFile := range certFiles { - if _, err := os.Stat(certFile); err == nil { - return certFile - } - } - return "" -} - func buildServers(listeners []*graph.Listener) (http, ssl []VirtualServer) { rulesForProtocol := map[v1.ProtocolType]portPathRules{ v1.HTTPProtocolType: make(portPathRules), diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index d04ac674e1..4a44b5c5b7 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -2478,7 +2478,7 @@ func TestConvertBackendTLS(t *testing.T) { expectedWithWellKnownCerts := &VerifyTLS{ Hostname: "example.com", - RootCAPath: getRootCAPath(), + RootCAPath: alpineSSLRootCAPath, } tests := []struct { diff --git a/internal/mode/static/state/graph/backend_refs.go b/internal/mode/static/state/graph/backend_refs.go index 5c97f7a96b..8d66d9b3a2 100644 --- a/internal/mode/static/state/graph/backend_refs.go +++ b/internal/mode/static/state/graph/backend_refs.go @@ -2,14 +2,17 @@ package graph import ( "fmt" + "slices" v1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" "k8s.io/apimachinery/pkg/util/validation/field" gatewayv1 "sigs.k8s.io/gateway-api/apis/v1" + v1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/conditions" "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/sort" staticConds "github.com/nginxinc/nginx-gateway-fabric/internal/mode/static/state/conditions" ) @@ -97,6 +100,10 @@ func addBackendRefsToRules( cond := validateBackendTLSPolicyMatchingAllBackends(backendRefs) if cond != nil { route.Conditions = append(route.Conditions, *cond) + // mark all backendRefs as invalid + for i := range backendRefs { + backendRefs[i].Valid = false + } } } @@ -180,11 +187,20 @@ func createBackendRef( // validateBackendTLSPolicyMatchingAllBackends validates that all backends in a rule reference the same // BackendTLSPolicy. We require that all backends in a group have the same backend TLS policy configuration. +// The backend TLS policy configuration is considered matching if: 1. CACertRefs reference the same ConfigMap, or +// 2. WellKnownCACerts are the same, and 3. Hostname is the same. // FIXME (ciarams87): This is a temporary solution until we can support multiple backend TLS policies per group. +// https://github.com/nginxinc/nginx-gateway-fabric/issues/1546 func validateBackendTLSPolicyMatchingAllBackends(backendRefs []BackendRef) *conditions.Condition { var mismatch bool var referencePolicy *BackendTLSPolicy + checkPoliciesEqual := func(p1, p2 *v1alpha2.BackendTLSPolicy) bool { + return !slices.Equal(p1.Spec.TLS.CACertRefs, p2.Spec.TLS.CACertRefs) || + p1.Spec.TLS.WellKnownCACerts != p2.Spec.TLS.WellKnownCACerts || + p1.Spec.TLS.Hostname != p2.Spec.TLS.Hostname + } + for _, backendRef := range backendRefs { if backendRef.BackendTLSPolicy == nil { if referencePolicy != nil { @@ -200,8 +216,7 @@ func validateBackendTLSPolicyMatchingAllBackends(backendRefs []BackendRef) *cond referencePolicy = backendRef.BackendTLSPolicy } else { // Check if the policies match - if backendRef.BackendTLSPolicy.Source.Name != referencePolicy.Source.Name || - backendRef.BackendTLSPolicy.Source.Namespace != referencePolicy.Source.Namespace { + if checkPoliciesEqual(backendRef.BackendTLSPolicy.Source, referencePolicy.Source) { mismatch = true break } @@ -234,13 +249,7 @@ func findBackendTLSPolicyForService( } if btp.Source.Spec.TargetRef.Name == ref.Name && btpNs == refNs { if beTLSPolicy != nil { - if btp.Source.CreationTimestamp.Equal(&beTLSPolicy.Source.CreationTimestamp) { - // if the policies have the same creation timestamp, the one that comes first alphabetically wins - if btp.Source.Name < beTLSPolicy.Source.Name { - beTLSPolicy = btp - } - } else if btp.Source.CreationTimestamp.Before(&beTLSPolicy.Source.CreationTimestamp) { - // the oldest policy wins - see https://gateway-api.sigs.k8s.io/geps/gep-713/#conflict-resolution + if sort.LessObjectMeta(&btp.Source.ObjectMeta, &beTLSPolicy.Source.ObjectMeta) { beTLSPolicy = btp } } else { @@ -254,7 +263,7 @@ func findBackendTLSPolicyForService( if !beTLSPolicy.Valid { err = fmt.Errorf("The backend TLS policy is invalid: %s", beTLSPolicy.Conditions[0].Message) } else { - beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewBackendTLSPolicyAttached()) + beTLSPolicy.Conditions = append(beTLSPolicy.Conditions, staticConds.NewBackendTLSPolicyAccepted()) } } diff --git a/internal/mode/static/state/graph/backend_refs_test.go b/internal/mode/static/state/graph/backend_refs_test.go index 61d62465eb..32d963ed5c 100644 --- a/internal/mode/static/state/graph/backend_refs_test.go +++ b/internal/mode/static/state/graph/backend_refs_test.go @@ -394,39 +394,142 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { hrWithOneBackend := createRoute("hr1", "Service", 1, "svc1") hrWithTwoBackends := createRoute("hr2", "Service", 2, "svc1") + hrWithTwoDiffBackends := createRoute("hr2", "Service", 2, "svc1") hrWithInvalidRule := createRoute("hr3", "NotService", 1, "svc1") hrWithZeroBackendRefs := createRoute("hr4", "Service", 1, "svc1") hrWithZeroBackendRefs.Spec.Rules[0].BackendRefs = nil + hrWithTwoDiffBackends.Spec.Rules[0].BackendRefs[1].Name = "svc2" - svc1 := &v1.Service{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: "test", - Name: "svc1", - }, - Spec: v1.ServiceSpec{ - Ports: []v1.ServicePort{ - { - Port: 80, - }, - { - Port: 81, + getSvc := func(name string) *v1.Service { + return &v1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Port: 80, + }, + { + Port: 81, + }, }, }, - }, + } } + svc1 := getSvc("svc1") svc1NsName := types.NamespacedName{ Namespace: "test", Name: "svc1", } + svc2 := getSvc("svc2") + svc2NsName := types.NamespacedName{ + Namespace: "test", + Name: "svc2", + } + services := map[types.NamespacedName]*v1.Service{ {Namespace: "test", Name: "svc1"}: svc1, + {Namespace: "test", Name: "svc2"}: svc2, + } + emptyPolicies := map[types.NamespacedName]*BackendTLSPolicy{} + + getPolicy := func(name, svcName, cmName string) *BackendTLSPolicy { + return &BackendTLSPolicy{ + Valid: true, + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: gatewayv1.ObjectName(svcName), + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []gatewayv1.LocalObjectReference{ + { + Group: "", + Kind: "ConfigMap", + Name: gatewayv1.ObjectName(cmName), + }, + }, + }, + }, + }, + } + } + + policiesMatching := map[types.NamespacedName]*BackendTLSPolicy{ + {Namespace: "test", Name: "btp1"}: getPolicy("btp1", "svc1", "test"), + {Namespace: "test", Name: "btp2"}: getPolicy("btp2", "svc2", "test"), + } + policiesNotMatching := map[types.NamespacedName]*BackendTLSPolicy{ + {Namespace: "test", Name: "btp1"}: getPolicy("btp1", "svc1", "test1"), + {Namespace: "test", Name: "btp2"}: getPolicy("btp2", "svc2", "test2"), } - policies := map[types.NamespacedName]*BackendTLSPolicy{} + + getBtp := func(name string, svcName string, cmName string) *BackendTLSPolicy { + return &BackendTLSPolicy{ + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test"}, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: gatewayv1.ObjectName(svcName), + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []gatewayv1.LocalObjectReference{ + { + Group: "", + Kind: "ConfigMap", + Name: gatewayv1.ObjectName(cmName), + }, + }, + }, + }, + }, + Conditions: []conditions.Condition{ + { + Type: "Accepted", + Status: "True", + Reason: "Accepted", + Message: "BackendTLSPolicy is accepted by the Gateway", + }, + }, + Valid: true, + IsReferenced: true, + } + } + + btp1 := getBtp("btp1", "svc1", "test1") + btp2 := getBtp("btp2", "svc2", "test2") + btp3 := getBtp("btp1", "svc1", "test") + btp3.Conditions = append(btp3.Conditions, conditions.Condition{ + Type: "Accepted", + Status: "True", + Reason: "Accepted", + Message: "BackendTLSPolicy is accepted by the Gateway", + }, + ) tests := []struct { - name string route *Route + policies map[types.NamespacedName]*BackendTLSPolicy + name string expectedBackendRefs []BackendRef expectedConditions []conditions.Condition }{ @@ -446,6 +549,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, }, expectedConditions: nil, + policies: emptyPolicies, name: "normal case with one rule with one backend", }, { @@ -470,8 +574,36 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, }, expectedConditions: nil, + policies: emptyPolicies, name: "normal case with one rule with two backends", }, + { + route: &Route{ + Source: hrWithTwoBackends, + ParentRefs: sectionNameRefs, + Valid: true, + Rules: createRules(hrWithTwoBackends, allValid, allValid), + }, + expectedBackendRefs: []BackendRef{ + { + SvcNsName: svc1NsName, + ServicePort: svc1.Spec.Ports[0], + Valid: true, + Weight: 1, + BackendTLSPolicy: btp3, + }, + { + SvcNsName: svc1NsName, + ServicePort: svc1.Spec.Ports[1], + Valid: true, + Weight: 5, + BackendTLSPolicy: btp3, + }, + }, + expectedConditions: nil, + policies: policiesMatching, + name: "normal case with one rule with two backends and matching policies", + }, { route: &Route{ Source: hrWithOneBackend, @@ -480,6 +612,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, expectedBackendRefs: nil, expectedConditions: nil, + policies: emptyPolicies, name: "invalid route", }, { @@ -491,6 +624,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, expectedBackendRefs: nil, expectedConditions: nil, + policies: emptyPolicies, name: "invalid matches", }, { @@ -502,6 +636,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { }, expectedBackendRefs: nil, expectedConditions: nil, + policies: emptyPolicies, name: "invalid filters", }, { @@ -521,7 +656,39 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { `spec.rules[0].backendRefs[0].kind: Unsupported value: "NotService": supported values: "Service"`, ), }, - name: "invalid backendRef", + policies: emptyPolicies, + name: "invalid backendRef", + }, + { + route: &Route{ + Source: hrWithTwoDiffBackends, + ParentRefs: sectionNameRefs, + Valid: true, + Rules: createRules(hrWithTwoDiffBackends, allValid, allValid), + }, + expectedBackendRefs: []BackendRef{ + { + SvcNsName: svc1NsName, + ServicePort: svc1.Spec.Ports[0], + Valid: false, + Weight: 1, + BackendTLSPolicy: btp1, + }, + { + SvcNsName: svc2NsName, + ServicePort: svc2.Spec.Ports[1], + Valid: false, + Weight: 5, + BackendTLSPolicy: btp2, + }, + }, + expectedConditions: []conditions.Condition{ + staticConds.NewRouteBackendRefUnsupportedValue( + `Backend TLS policies do not match for all backends`, + ), + }, + policies: policiesNotMatching, + name: "invalid backendRef - backend TLS policies do not match for all backends", }, { route: &Route{ @@ -540,7 +707,7 @@ func TestAddBackendRefsToRulesTest(t *testing.T) { t.Run(test.name, func(t *testing.T) { g := NewWithT(t) resolver := newReferenceGrantResolver(nil) - addBackendRefsToRules(test.route, resolver, services, policies) + addBackendRefsToRules(test.route, resolver, services, test.policies) var actual []BackendRef if test.route.Rules != nil { @@ -623,12 +790,7 @@ func TestCreateBackend(t *testing.T) { }, Valid: false, Conditions: []conditions.Condition{ - { - // Type: conditions.Invalid, - // Status: conditions.ConditionFalse, - // Reason: staticConds.BackendTLSPolicyInvalidWellKnownCACerts, - Message: "unsupported value", - }, + staticConds.NewBackendTLSPolicyInvalid("unsupported value"), }, } @@ -840,13 +1002,25 @@ func TestGetServicePort(t *testing.T) { } func TestValidateBackendTLSPolicyMatchingAllBackends(t *testing.T) { - getBtp := func(name string) *BackendTLSPolicy { + getBtp := func(name, caCertName string) *BackendTLSPolicy { return &BackendTLSPolicy{ Source: &v1alpha2.BackendTLSPolicy{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: "test", }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TLS: v1alpha2.BackendTLSPolicyConfig{ + Hostname: "foo.example.com", + CACertRefs: []gatewayv1.LocalObjectReference{ + { + Group: "", + Kind: "ConfigMap", + Name: gatewayv1.ObjectName(caCertName), + }, + }, + }, + }, }, } } @@ -863,27 +1037,27 @@ func TestValidateBackendTLSPolicyMatchingAllBackends(t *testing.T) { backendRefsWithMatchingPolicies := []BackendRef{ { SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, - BackendTLSPolicy: getBtp("btp1"), + BackendTLSPolicy: getBtp("btp1", "ca1"), }, { SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, - BackendTLSPolicy: getBtp("btp1"), + BackendTLSPolicy: getBtp("btp2", "ca1"), }, } backendRefsWithNotMatchingPolicies := []BackendRef{ { SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, - BackendTLSPolicy: getBtp("btp1"), + BackendTLSPolicy: getBtp("btp1", "ca1"), }, { SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, - BackendTLSPolicy: getBtp("btp2"), + BackendTLSPolicy: getBtp("btp2", "ca2"), }, } backendRefsOnePolicy := []BackendRef{ { SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc1"}, - BackendTLSPolicy: getBtp("btp1"), + BackendTLSPolicy: getBtp("btp1", "ca1"), }, { SvcNsName: types.NamespacedName{Namespace: "test", Name: "svc2"}, @@ -931,67 +1105,31 @@ func TestValidateBackendTLSPolicyMatchingAllBackends(t *testing.T) { func TestFindBackendTLSPolicyForService(t *testing.T) { oldCreationTimestamp := metav1.Now() newCreationTimestamp := metav1.Now() - oldestBtp := BackendTLSPolicy{ - Valid: true, - Source: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "oldest", - Namespace: "test", - CreationTimestamp: oldCreationTimestamp, - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "svc1", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), - }, - }, - }, - }, - } - newestBtp := BackendTLSPolicy{ - Valid: true, - Source: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "newest", - Namespace: "test", - CreationTimestamp: newCreationTimestamp, - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "svc1", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), - }, + getBtp := func(name string, timestamp metav1.Time) *BackendTLSPolicy { + return &BackendTLSPolicy{ + Valid: true, + Source: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: name, + Namespace: "test", + CreationTimestamp: timestamp, }, - }, - }, - } - - alphaFirstBtp := BackendTLSPolicy{ - Valid: true, - Source: &v1alpha2.BackendTLSPolicy{ - ObjectMeta: metav1.ObjectMeta{ - Name: "alphabeticallyfirst", - Namespace: "test", - CreationTimestamp: oldCreationTimestamp, - }, - Spec: v1alpha2.BackendTLSPolicySpec{ - TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ - PolicyTargetReference: v1alpha2.PolicyTargetReference{ - Group: "", - Kind: "Service", - Name: "svc1", - Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Group: "", + Kind: "Service", + Name: "svc1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, }, }, }, - }, + } } + oldestBtp := getBtp("oldest", oldCreationTimestamp) + newestBtp := getBtp("newest", newCreationTimestamp) + alphaFirstBtp := getBtp("alphabeticallyfirst", oldCreationTimestamp) ref := gatewayv1.HTTPBackendRef{ BackendRef: gatewayv1.BackendRef{ @@ -1011,17 +1149,17 @@ func TestFindBackendTLSPolicyForService(t *testing.T) { { name: "oldest wins", backendTLSPolicies: map[types.NamespacedName]*BackendTLSPolicy{ - client.ObjectKeyFromObject(newestBtp.Source): &newestBtp, - client.ObjectKeyFromObject(oldestBtp.Source): &oldestBtp, + client.ObjectKeyFromObject(newestBtp.Source): newestBtp, + client.ObjectKeyFromObject(oldestBtp.Source): oldestBtp, }, expectedBtpName: "oldest", }, { name: "alphabetically first wins", backendTLSPolicies: map[types.NamespacedName]*BackendTLSPolicy{ - client.ObjectKeyFromObject(oldestBtp.Source): &oldestBtp, - client.ObjectKeyFromObject(alphaFirstBtp.Source): &alphaFirstBtp, - client.ObjectKeyFromObject(newestBtp.Source): &newestBtp, + client.ObjectKeyFromObject(oldestBtp.Source): oldestBtp, + client.ObjectKeyFromObject(alphaFirstBtp.Source): alphaFirstBtp, + client.ObjectKeyFromObject(newestBtp.Source): newestBtp, }, expectedBtpName: "alphabeticallyfirst", }, diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go index 2677281647..57e844914f 100644 --- a/internal/mode/static/state/graph/backend_tls_policy.go +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -1,6 +1,7 @@ package graph import ( + "errors" "fmt" "k8s.io/apimachinery/pkg/types" @@ -26,6 +27,8 @@ type BackendTLSPolicy struct { Valid bool // IsReferenced shows whether the BackendTLSPolicy is referenced by a BackendRef. IsReferenced bool + // Ignored shows whether the BackendTLSPolicy is ignored. + Ignored bool } func processBackendTLSPolicies( @@ -39,17 +42,24 @@ func processBackendTLSPolicies( } processedBackendTLSPolicies := make(map[types.NamespacedName]*BackendTLSPolicy, len(backendTLSPolicies)) for nsname, backendTLSPolicy := range backendTLSPolicies { + valid, ignored, caCertRef, conds := validateBackendTLSPolicy( + backendTLSPolicy, + configMapResolver, + ctlrName, + gateway, + ) + processedBackendTLSPolicies[nsname] = &BackendTLSPolicy{ - Source: backendTLSPolicy, - } - valid, caCertRef, conds := validateBackendTLSPolicy(backendTLSPolicy, configMapResolver, ctlrName, gateway) - processedBackendTLSPolicies[nsname].Valid = valid - processedBackendTLSPolicies[nsname].Conditions = conds - processedBackendTLSPolicies[nsname].Gateway = types.NamespacedName{ - Namespace: gateway.Source.Namespace, - Name: gateway.Source.Name, + Source: backendTLSPolicy, + Valid: valid, + Conditions: conds, + Gateway: types.NamespacedName{ + Namespace: gateway.Source.Namespace, + Name: gateway.Source.Name, + }, + CaCertRef: caCertRef, + Ignored: ignored, } - processedBackendTLSPolicies[nsname].CaCertRef = caCertRef } return processedBackendTLSPolicies } @@ -59,28 +69,14 @@ func validateBackendTLSPolicy( configMapResolver *configMapResolver, ctlrName string, gateway *Gateway, -) (bool, types.NamespacedName, []conditions.Condition) { - conds := make([]conditions.Condition, 0) +) (bool, bool, types.NamespacedName, []conditions.Condition) { + var conds []conditions.Condition valid := true - caCertRef := types.NamespacedName{} - if len(backendTLSPolicy.Status.Ancestors) >= 16 { - // check if we already are an ancestor on this policy. If we are, we are safe to continue. - ancestorRef := v1.ParentReference{ - Namespace: helpers.GetPointer((v1.Namespace)(gateway.Source.Namespace)), - Name: v1.ObjectName(gateway.Source.Name), - } - var alreadyAncestor bool - for _, ancestor := range backendTLSPolicy.Status.Ancestors { - if string(ancestor.ControllerName) == ctlrName && ancestor.AncestorRef.Name == ancestorRef.Name && - *ancestor.AncestorRef.Namespace == *ancestorRef.Namespace { - alreadyAncestor = true - break - } - } - if !alreadyAncestor { - valid = false - conds = append(conds, staticConds.NewBackendTLSPolicyIgnored("too many ancestors, cannot attach a new Gateway")) - } + ignored := false + var caCertRef types.NamespacedName + if err := validateAncestorMaxCount(backendTLSPolicy, ctlrName, gateway); err != nil { + valid = false + ignored = true } if err := validateBackendTLSHostname(backendTLSPolicy); err != nil { valid = false @@ -106,7 +102,30 @@ func validateBackendTLSPolicy( valid = false conds = append(conds, staticConds.NewBackendTLSPolicyInvalid("CACertRefs and WellKnownCACerts are both nil")) } - return valid, caCertRef, conds + return valid, ignored, caCertRef, conds +} + +func validateAncestorMaxCount(backendTLSPolicy *v1alpha2.BackendTLSPolicy, ctlrName string, gateway *Gateway) error { + var err error + if len(backendTLSPolicy.Status.Ancestors) >= 16 { + // check if we already are an ancestor on this policy. If we are, we are safe to continue. + ancestorRef := v1.ParentReference{ + Namespace: helpers.GetPointer((v1.Namespace)(gateway.Source.Namespace)), + Name: v1.ObjectName(gateway.Source.Name), + } + var alreadyAncestor bool + for _, ancestor := range backendTLSPolicy.Status.Ancestors { + if string(ancestor.ControllerName) == ctlrName && ancestor.AncestorRef.Name == ancestorRef.Name && + ancestor.AncestorRef.Namespace != nil && *ancestor.AncestorRef.Namespace == *ancestorRef.Namespace { + alreadyAncestor = true + break + } + } + if !alreadyAncestor { + err = errors.New("too many ancestors, cannot attach a new Gateway") + } + } + return err } func validateBackendTLSHostname(btp *v1alpha2.BackendTLSPolicy) error { @@ -147,7 +166,7 @@ func validateBackendTLSCACertRef(btp *v1alpha2.BackendTLSPolicy, configMapResolv func validateBackendTLSWellKnownCACerts(btp *v1alpha2.BackendTLSPolicy) error { if *btp.Spec.TLS.WellKnownCACerts != v1alpha2.WellKnownCACertSystem { path := field.NewPath("tls.wellknowncacerts") - return field.Invalid(path, btp.Spec.TLS.WellKnownCACerts, "unsupported value") + return field.NotSupported(path, btp.Spec.TLS.WellKnownCACerts, []string{string(v1alpha2.WellKnownCACertSystem)}) } return nil } diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go index ccc0394f6e..f1d4958e22 100644 --- a/internal/mode/static/state/graph/backend_tls_policy_test.go +++ b/internal/mode/static/state/graph/backend_tls_policy_test.go @@ -92,6 +92,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { caCertName types.NamespacedName name string isValid bool + ignored bool }{ { name: "normal case with ca cert refs", @@ -288,6 +289,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, isValid: false, caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + ignored: true, }, } @@ -322,11 +324,17 @@ func TestValidateBackendTLSPolicy(t *testing.T) { Source: &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "test"}}, } - valid, caCertName, conds := validateBackendTLSPolicy(test.tlsPolicy, configMapResolver, "test", gateway) + valid, ignored, caCertName, conds := validateBackendTLSPolicy( + test.tlsPolicy, + configMapResolver, + "test", + gateway, + ) g.Expect(valid).To(Equal(test.isValid)) g.Expect(caCertName).To(Equal(test.caCertName)) - if !test.isValid { + g.Expect(ignored).To(Equal(test.ignored)) + if !test.isValid && !test.ignored { g.Expect(conds).To(HaveLen(1)) } else { g.Expect(conds).To(HaveLen(0)) diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go index 35ed73fdf3..b784f9a8e2 100644 --- a/internal/mode/static/state/graph/graph_test.go +++ b/internal/mode/static/state/graph/graph_test.go @@ -104,8 +104,8 @@ func TestBuildGraph(t *testing.T) { } btpAttachedConds := []conditions.Condition{ - staticConds.NewBackendTLSPolicyAttached(), - staticConds.NewBackendTLSPolicyAttached(), + staticConds.NewBackendTLSPolicyAccepted(), + staticConds.NewBackendTLSPolicyAccepted(), } btp := BackendTLSPolicy{ diff --git a/site/content/how-to/traffic-management/backend-tls-termination.md b/site/content/how-to/traffic-management/securing-backend-traffic.md similarity index 95% rename from site/content/how-to/traffic-management/backend-tls-termination.md rename to site/content/how-to/traffic-management/securing-backend-traffic.md index a35f025b90..64b2db51c9 100644 --- a/site/content/how-to/traffic-management/backend-tls-termination.md +++ b/site/content/how-to/traffic-management/securing-backend-traffic.md @@ -1,5 +1,5 @@ --- -title: "Backend TLS Termination" +title: "Securing Traffic to Backends" description: "Learn how to encrypt HTTP traffic between NGINX Gateway Fabric and your backend pods." weight: 600 toc: true @@ -10,7 +10,7 @@ In this guide, we will show how to specify the TLS configuration of the connecti ## Prerequisites -- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the `- --experimental-features-enable` flag. +- [Install]({{< relref "installation/" >}}) NGINX Gateway Fabric. Please note that the Gateway APIs from the experimental channel are required, and NGF must be deployed with the `--gateway-api-experimental-features` flag. - [Expose NGINX Gateway Fabric]({{< relref "installation/expose-nginx-gateway-fabric.md" >}}) and save the public IP address and port of NGINX Gateway Fabric into shell variables: ```text @@ -90,7 +90,7 @@ data: default_type text/plain; location / { - return 200 "hello from pod $hostname\n"; + return 200 "hello from pod secure-app\n"; } } --- @@ -173,7 +173,7 @@ Using the external IP address and port for NGINX Gateway Fabric, we can send tra {{< note >}}If you have a DNS record allocated for `secure-app.example.com`, you can send the request directly to that hostname, without needing to resolve.{{< /note >}} ```shell -curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ --insecure +curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ ``` ```text @@ -248,10 +248,10 @@ spec: EOF ``` -To confirm the Polict was created and attached successfully, we can run a describe on the BackendTLSPolicy object: +To confirm the Policy was created and attached successfully, we can run a describe on the BackendTLSPolicy object: ```shell -k describe backendtlspolicies.gateway.networking.k8s.io +kubectl describe backendtlspolicies.gateway.networking.k8s.io ``` ```text @@ -287,10 +287,10 @@ Status: Namespace: default Conditions: Last Transition Time: 2024-02-01T12:02:38Z - Message: BackendTLSPolicy is attached to the Gateway - Reason: BackendTLSPolicyAttached + Message: BackendTLSPolicy is accepted by the Gateway + Reason: Accepted Status: True - Type: Attached + Type: Accepted Controller Name: gateway.nginx.org/nginx-gateway-controller Events: ``` @@ -300,11 +300,11 @@ Events: Now let's try sending traffic again: ```shell -curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ --insecure +curl --resolve secure-app.example.com:$GW_PORT:$GW_IP http://secure-app.example.com:$GW_PORT/ ``` ```text -hello from pod secure-app-868cfd5b5-v7gwk +hello from pod secure-app ``` ## Further Reading diff --git a/site/content/installation/installing-ngf/manifests.md b/site/content/installation/installing-ngf/manifests.md index ae71d2e38c..e83a18c122 100644 --- a/site/content/installation/installing-ngf/manifests.md +++ b/site/content/installation/installing-ngf/manifests.md @@ -75,7 +75,7 @@ Deploying NGINX Gateway Fabric with Kubernetes manifests takes only a few steps. #### Enable experimental features -We support a subset of the additional features provided by the Gateway API experimental channel. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric, edit the `deploy/manifests/nginx-gateway.yaml` to add the `experimental-features-enable` flag to the nginx-gateway deployment spec: +We support a subset of the additional features provided by the Gateway API experimental channel. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric, edit the `deploy/manifests/nginx-gateway.yaml` to add the `gateway-api-experimental-features` flag to the nginx-gateway deployment spec: ```yaml <...> @@ -91,7 +91,7 @@ spec: containers: - args: <...> - - --experimental-features-enable + - --gateway-api-experimental-features ``` {{}}Requires the Gateway APIs installed from the experimental channel.{{}} diff --git a/site/content/overview/gateway-api-compatibility.md b/site/content/overview/gateway-api-compatibility.md index 07ac38eb98..5a8a4e7b6f 100644 --- a/site/content/overview/gateway-api-compatibility.md +++ b/site/content/overview/gateway-api-compatibility.md @@ -254,17 +254,16 @@ Fields: - `group` - supported. - `kind` - supports `ConfigMap`. - `hostname` - supported. - - `wellKnownCerts` - supports `System`. This will set the CA certificate to the system root CA path. + - `wellKnownCerts` - supports `System`. This will set the CA certificate to the Alpine system root CA path `/etc/ssl/cert.pem`. NB: This option will only work if the NGINX image used is Alpine based. The NGF NGINX images are Alpine based by default. - `status` - `ancestors` - `ancestorRef` - supported. - `controllerName`: supported. - - `conditions`: Supported (Condition/Status/Reason): - - `Attached/True/BackendTLSPolicyAttached` - Custom reason for when the BackendTLSPolicy is attached to at least one Service referenced by this Gateway. - - `Attached/False/BackendTLSPolicyIgnored` - Custom reason for when the BackendTLSPolicy config cannot be attached to the Gateway and will be ignored. - - `Valid/False/BackendTLSPolicyInvalid` - Custom reason for when the BackendTLSPolicy config is invalid. + - `conditions`: Partially supported. Supported (Condition/Status/Reason): + - `Accepted/True/PolicyReasonAccepted` + - `Accepted/False/PolicyReasonInvalid` -{{}}If multiple `backendRefs` are defined for a HTTPRoute rule, all the referenced Services *must* have matching BackendTLSPolicy configuration{{}} +{{}}If multiple `backendRefs` are defined for a HTTPRoute rule, all the referenced Services *must* have matching BackendTLSPolicy configuration. BackendTLSPolicy configuration is considered to be matching if 1. CACertRefs reference the same ConfigMap, or 2. WellKnownCACerts are the same, and 3. Hostname is the same.{{}} ### Custom Policies diff --git a/site/content/reference/cli-help.md b/site/content/reference/cli-help.md index aa72ee363a..1c7c661e55 100644 --- a/site/content/reference/cli-help.md +++ b/site/content/reference/cli-help.md @@ -19,23 +19,23 @@ _Usage_: ### Flags {{< bootstrap-table "table table-bordered table-striped table-responsive" >}} -| Name | Type | Description | -|------------------------------|----------|-------------| -| _gateway-ctlr-name_ | _string_ | The name of the Gateway controller. The controller name must be in the form: `DOMAIN/PATH`. The controller's domain is `gateway.nginx.org`. | -| _gatewayclass_ | _string_ | The name of the GatewayClass resource. Every NGINX Gateway Fabric must have a unique corresponding GatewayClass resource. | -| _gateway_ | _string_ | The namespaced name of the Gateway resource to use. Must be of the form: `NAMESPACE/NAME`. If not specified, the control plane will process all Gateways for the configured GatewayClass. Among them, it will choose the oldest resource by creation timestamp. If the timestamps are equal, it will choose the resource that appears first in alphabetical order by {namespace}/{name}. | -| _nginx-plus_ | _bool_ | Enable support for NGINX Plus. | -| _experimental-features-enable_ | _bool_ | Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. Requires the Gateway APIs installed from the experimental channel. | -| _config_ | _string_ | The name of the NginxGateway resource to be used for this controller's dynamic configuration. Lives in the same namespace as the controller. | -| _service_ | _string_ | The name of the service that fronts this NGINX Gateway Fabric pod. Lives in the same namespace as the controller. | -| _metrics-disable_ | _bool_ | Disable exposing metrics in the Prometheus format (Default: `false`). | -| _metrics-listen-port_ | _int_ | Sets the port where the Prometheus metrics are exposed. An integer between 1024 - 65535 (Default: `9113`) | -| _metrics-secure-serving_ | _bool_ | Configures if the metrics endpoint should be secured using https. Note that this endpoint will be secured with a self-signed certificate (Default `false`). | -| _update-gatewayclass-status_ | _bool_ | Update the status of the GatewayClass resource (Default: `true`). | -| _health-disable_ | _bool_ | Disable running the health probe server (Default: `false`). | -| _health-port_ | _int_ | Set the port where the health probe server is exposed. An integer between 1024 - 65535 (Default: `8081`). | -| _leader-election-disable_ | _bool_ | Disable leader election, which is used to avoid multiple replicas of the NGINX Gateway Fabric reporting the status of the Gateway API resources. If disabled, all replicas of NGINX Gateway Fabric will update the statuses of the Gateway API resources (Default: `false`). | -| _leader-election-lock-name_ | _string_ | The name of the leader election lock. A lease object with this name will be created in the same namespace as the controller (Default: `"nginx-gateway-leader-election-lock"`). | +| Name | Type | Description | +| ----------------------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| _gateway-ctlr-name_ | _string_ | The name of the Gateway controller. The controller name must be in the form: `DOMAIN/PATH`. The controller's domain is `gateway.nginx.org`. | +| _gatewayclass_ | _string_ | The name of the GatewayClass resource. Every NGINX Gateway Fabric must have a unique corresponding GatewayClass resource. | +| _gateway_ | _string_ | The namespaced name of the Gateway resource to use. Must be of the form: `NAMESPACE/NAME`. If not specified, the control plane will process all Gateways for the configured GatewayClass. Among them, it will choose the oldest resource by creation timestamp. If the timestamps are equal, it will choose the resource that appears first in alphabetical order by {namespace}/{name}. | +| _nginx-plus_ | _bool_ | Enable support for NGINX Plus. | +| _gateway-api-experimental-features_ | _bool_ | Enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric. Requires the Gateway APIs installed from the experimental channel. | +| _config_ | _string_ | The name of the NginxGateway resource to be used for this controller's dynamic configuration. Lives in the same namespace as the controller. | +| _service_ | _string_ | The name of the service that fronts this NGINX Gateway Fabric pod. Lives in the same namespace as the controller. | +| _metrics-disable_ | _bool_ | Disable exposing metrics in the Prometheus format (Default: `false`). | +| _metrics-listen-port_ | _int_ | Sets the port where the Prometheus metrics are exposed. An integer between 1024 - 65535 (Default: `9113`) | +| _metrics-secure-serving_ | _bool_ | Configures if the metrics endpoint should be secured using https. Note that this endpoint will be secured with a self-signed certificate (Default `false`). | +| _update-gatewayclass-status_ | _bool_ | Update the status of the GatewayClass resource (Default: `true`). | +| _health-disable_ | _bool_ | Disable running the health probe server (Default: `false`). | +| _health-port_ | _int_ | Set the port where the health probe server is exposed. An integer between 1024 - 65535 (Default: `8081`). | +| _leader-election-disable_ | _bool_ | Disable leader election, which is used to avoid multiple replicas of the NGINX Gateway Fabric reporting the status of the Gateway API resources. If disabled, all replicas of NGINX Gateway Fabric will update the statuses of the Gateway API resources (Default: `false`). | +| _leader-election-lock-name_ | _string_ | The name of the leader election lock. A lease object with this name will be created in the same namespace as the controller (Default: `"nginx-gateway-leader-election-lock"`). | {{% /bootstrap-table %}} ## Sleep @@ -49,7 +49,7 @@ _Usage_: ``` {{< bootstrap-table "table table-bordered table-striped table-responsive" >}} -| Name | Type | Description | -|----------|-----------------|-------------------------------------------------------------------------------------------------------| +| Name | Type | Description | +| -------- | --------------- | ----------------------------------------------------------------------------------------------------------------------------- | | duration | `time.Duration` | Set the duration of sleep. Must be parsable by [`time.ParseDuration`](https://pkg.go.dev/time#ParseDuration). (default `30s`) | {{% /bootstrap-table %}} From df219076547fc07d3a8fdf0419688842236b130b Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Fri, 9 Feb 2024 11:34:25 +0000 Subject: [PATCH 09/13] Add change processor test cases --- .../static/state/change_processor_test.go | 120 ++++++++++++++++-- 1 file changed, 110 insertions(+), 10 deletions(-) diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go index 70f41d8a0b..df91241d24 100644 --- a/internal/mode/static/state/change_processor_test.go +++ b/internal/mode/static/state/change_processor_test.go @@ -1002,6 +1002,7 @@ var _ = Describe("ChangeProcessor", func() { hr1svc, sharedSvc, bazSvc1, bazSvc2, bazSvc3, invalidSvc, notRefSvc *apiv1.Service hr1slice1, hr1slice2, noRefSlice, missingSvcNameSlice *discoveryV1.EndpointSlice gw *v1.Gateway + btls *v1alpha2.BackendTLSPolicy ) createSvc := func(name string) *apiv1.Service { @@ -1023,6 +1024,24 @@ var _ = Describe("ChangeProcessor", func() { } } + createBackendTLSPolicy := func(name string, svcName string) *v1alpha2.BackendTLSPolicy { + return &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Namespace: "test", + Name: name, + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Kind: v1.Kind("Service"), + Name: v1.ObjectName(svcName), + Namespace: helpers.GetPointer(v1.Namespace("test")), + }, + }, + }, + } + } + BeforeAll(func() { testNamespace := v1.Namespace("test") kindService := v1.Kind("Service") @@ -1068,6 +1087,9 @@ var _ = Describe("ChangeProcessor", func() { noRefSlice = createEndpointSlice("no-ref", "no-ref") missingSvcNameSlice = createEndpointSlice("missing-svc-name", "") + // backendTLSPolicy + btls = createBackendTLSPolicy("btls", "foo-svc") + gw = createGateway("gw") processor.CaptureUpsertChange(gc) processor.CaptureUpsertChange(gw) @@ -1100,6 +1122,11 @@ var _ = Describe("ChangeProcessor", func() { testUpsertTriggersChange(hr1svc, state.ClusterStateChange) }) }) + When("a backendTLSPolicy is added for referenced service", func() { + It("should trigger a change", func() { + testUpsertTriggersChange(btls, state.ClusterStateChange) + }) + }) When("an hr1 endpoint slice is added", func() { It("should trigger a change", func() { testUpsertTriggersChange(hr1slice1, state.EndpointsOnlyChange) @@ -1503,17 +1530,20 @@ var _ = Describe("ChangeProcessor", func() { // Note: in these tests, we deliberately don't fully inspect the returned configuration and statuses // -- this is done in 'Normal cases of processing changes' + //nolint:lll var ( - processor *state.ChangeProcessorImpl - gcNsName, gwNsName, hrNsName, hr2NsName, rgNsName, svcNsName, sliceNsName, secretNsName types.NamespacedName - gc, gcUpdated *v1.GatewayClass - gw1, gw1Updated, gw2 *v1.Gateway - hr1, hr1Updated, hr2 *v1.HTTPRoute - rg1, rg1Updated, rg2 *v1beta1.ReferenceGrant - svc, barSvc, unrelatedSvc *apiv1.Service - slice, barSlice, unrelatedSlice *discoveryV1.EndpointSlice - ns, unrelatedNS, testNs, barNs *apiv1.Namespace - secret, secretUpdated, unrelatedSecret, barSecret, barSecretUpdated *apiv1.Secret + processor *state.ChangeProcessorImpl + gcNsName, gwNsName, hrNsName, hr2NsName, rgNsName, svcNsName, sliceNsName, secretNsName, cmNsName, btlsNsName types.NamespacedName + gc, gcUpdated *v1.GatewayClass + gw1, gw1Updated, gw2 *v1.Gateway + hr1, hr1Updated, hr2 *v1.HTTPRoute + rg1, rg1Updated, rg2 *v1beta1.ReferenceGrant + svc, barSvc, unrelatedSvc *apiv1.Service + slice, barSlice, unrelatedSlice *discoveryV1.EndpointSlice + ns, unrelatedNS, testNs, barNs *apiv1.Namespace + secret, secretUpdated, unrelatedSecret, barSecret, barSecretUpdated *apiv1.Secret + cm, cmUpdated, unrelatedCM *apiv1.ConfigMap + btls, btlsUpdated *v1alpha2.BackendTLSPolicy ) BeforeEach(OncePerOrdered, func() { @@ -1756,6 +1786,56 @@ var _ = Describe("ChangeProcessor", func() { rg2 = rg1.DeepCopy() rg2.Name = "rg-2" + + cmNsName = types.NamespacedName{Namespace: "test", Name: "cm-1"} + cm = &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: cmNsName.Name, + Namespace: cmNsName.Namespace, + }, + Data: map[string]string{ + "ca.crt": "value", + }, + } + cmUpdated = cm.DeepCopy() + cmUpdated.Data["ca.crt"] = "updated-value" + + unrelatedCM = &apiv1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "unrelated-cm", + Namespace: "unrelated-ns", + }, + Data: map[string]string{ + "ca.crt": "value", + }, + } + + btlsNsName = types.NamespacedName{Namespace: "test", Name: "btls-1"} + btls = &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: btlsNsName.Name, + Namespace: btlsNsName.Namespace, + Generation: 1, + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: v1.ObjectName(svc.Name), + Namespace: helpers.GetPointer(v1.Namespace(svc.Namespace)), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []v1.LocalObjectReference{ + { + Name: v1.ObjectName(cm.Name), + }, + }, + }, + }, + } + btlsUpdated = btls.DeepCopy() + btlsUpdated.Generation++ }) // Changing change - a change that makes processor.Process() report changed // Non-changing change - a change that doesn't do that @@ -1771,6 +1851,8 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(testNs) processor.CaptureUpsertChange(hr1) processor.CaptureUpsertChange(rg1) + processor.CaptureUpsertChange(btls) + processor.CaptureUpsertChange(cm) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -1782,12 +1864,16 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gw1Updated) processor.CaptureUpsertChange(hr1Updated) processor.CaptureUpsertChange(rg1Updated) + processor.CaptureUpsertChange(btlsUpdated) + processor.CaptureUpsertChange(cmUpdated) // there are non-changing changes processor.CaptureUpsertChange(gcUpdated) processor.CaptureUpsertChange(gw1Updated) processor.CaptureUpsertChange(hr1Updated) processor.CaptureUpsertChange(rg1Updated) + processor.CaptureUpsertChange(btlsUpdated) + processor.CaptureUpsertChange(cmUpdated) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -1809,6 +1895,8 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureDeleteChange(&v1.Gateway{}, gwNsName) processor.CaptureDeleteChange(&v1.HTTPRoute{}, hrNsName) processor.CaptureDeleteChange(&v1beta1.ReferenceGrant{}, rgNsName) + processor.CaptureDeleteChange(&v1alpha2.BackendTLSPolicy{}, btlsNsName) + processor.CaptureDeleteChange(&apiv1.ConfigMap{}, cmNsName) // these are non-changing changes processor.CaptureUpsertChange(gw2) @@ -1847,6 +1935,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(hr1) processor.CaptureUpsertChange(secret) processor.CaptureUpsertChange(barSecret) + processor.CaptureUpsertChange(cm) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) }) @@ -1856,6 +1945,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(slice) processor.CaptureUpsertChange(ns) processor.CaptureUpsertChange(secretUpdated) + processor.CaptureUpsertChange(cmUpdated) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) }) @@ -1864,6 +1954,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(unrelatedSlice) processor.CaptureUpsertChange(unrelatedNS) processor.CaptureUpsertChange(unrelatedSecret) + processor.CaptureUpsertChange(unrelatedCM) changed, _ := processor.Process() Expect(changed).To(Equal(state.NoChange)) @@ -1875,12 +1966,14 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(barSlice) processor.CaptureUpsertChange(barNs) processor.CaptureUpsertChange(barSecretUpdated) + processor.CaptureUpsertChange(cmUpdated) // there are non-changing changes processor.CaptureUpsertChange(unrelatedSvc) processor.CaptureUpsertChange(unrelatedSlice) processor.CaptureUpsertChange(unrelatedNS) processor.CaptureUpsertChange(unrelatedSecret) + processor.CaptureUpsertChange(unrelatedCM) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -1893,12 +1986,14 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureDeleteChange(&discoveryV1.EndpointSlice{}, sliceNsName) processor.CaptureDeleteChange(&apiv1.Namespace{}, types.NamespacedName{Name: "ns"}) processor.CaptureDeleteChange(&apiv1.Secret{}, secretNsName) + processor.CaptureDeleteChange(&apiv1.ConfigMap{}, cmNsName) // these are non-changing changes processor.CaptureUpsertChange(unrelatedSvc) processor.CaptureUpsertChange(unrelatedSlice) processor.CaptureUpsertChange(unrelatedNS) processor.CaptureUpsertChange(unrelatedSecret) + processor.CaptureUpsertChange(unrelatedCM) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -1913,12 +2008,14 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(testNs) processor.CaptureUpsertChange(hr1) processor.CaptureUpsertChange(rg1) + processor.CaptureUpsertChange(btls) // related Kubernetes API resources processor.CaptureUpsertChange(svc) processor.CaptureUpsertChange(slice) processor.CaptureUpsertChange(ns) processor.CaptureUpsertChange(secret) + processor.CaptureUpsertChange(cm) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) @@ -1929,6 +2026,7 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(unrelatedSlice) processor.CaptureUpsertChange(unrelatedNS) processor.CaptureUpsertChange(unrelatedSecret) + processor.CaptureUpsertChange(unrelatedCM) changed, _ := processor.Process() Expect(changed).To(Equal(state.NoChange)) @@ -1940,12 +2038,14 @@ var _ = Describe("ChangeProcessor", func() { processor.CaptureUpsertChange(gw1Updated) processor.CaptureUpsertChange(hr1Updated) processor.CaptureUpsertChange(rg1Updated) + processor.CaptureUpsertChange(btlsUpdated) // these are non-changing changes processor.CaptureUpsertChange(unrelatedSvc) processor.CaptureUpsertChange(unrelatedSlice) processor.CaptureUpsertChange(unrelatedNS) processor.CaptureUpsertChange(unrelatedSecret) + processor.CaptureUpsertChange(unrelatedCM) changed, _ := processor.Process() Expect(changed).To(Equal(state.ClusterStateChange)) From 96269ae0f7ddd845304ecc348b92168c1bc63562 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Tue, 13 Feb 2024 15:03:46 +0000 Subject: [PATCH 10/13] Fix linter --- internal/mode/static/nginx/config/servers_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index 0e83bac5a1..fbd5b75006 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -1918,7 +1918,7 @@ func TestConvertBackendTLSFromGroup(t *testing.T) { } for _, tc := range tests { - t.Run(tc.msg, func(t *testing.T) { + t.Run(tc.msg, func(_ *testing.T) { result := createProxyTLSFromBackends(tc.grp) g.Expect(result).To(Equal(tc.expected)) }) From 42af02a62f0f53f4247a6a3d2066433a78f60c27 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Wed, 14 Feb 2024 12:03:21 +0000 Subject: [PATCH 11/13] Tidy code, fix panic, update for plus manifest --- .yamllint.yaml | 1 - deploy/manifests/nginx-plus-gateway.yaml | 3 + internal/mode/static/build_statuses_test.go | 62 ++++++++---- internal/mode/static/nginx/config/servers.go | 16 ++-- .../mode/static/nginx/config/servers_test.go | 2 +- .../static/state/change_processor_test.go | 1 - .../static/state/conditions/conditions.go | 13 --- .../static/state/graph/backend_tls_policy.go | 26 ++--- .../state/graph/backend_tls_policy_test.go | 95 ++++++++++++++----- .../mode/static/state/graph/graph_test.go | 14 +-- 10 files changed, 152 insertions(+), 81 deletions(-) diff --git a/.yamllint.yaml b/.yamllint.yaml index 9782deb0f9..3a77819f42 100644 --- a/.yamllint.yaml +++ b/.yamllint.yaml @@ -32,7 +32,6 @@ rules: ignore: | deploy/manifests/nginx-gateway.yaml deploy/manifests/crds - examples/backend-tls/secure-app.yaml key-duplicates: enable key-ordering: disable line-length: diff --git a/deploy/manifests/nginx-plus-gateway.yaml b/deploy/manifests/nginx-plus-gateway.yaml index 23759bdf02..3c979e6a25 100644 --- a/deploy/manifests/nginx-plus-gateway.yaml +++ b/deploy/manifests/nginx-plus-gateway.yaml @@ -32,6 +32,7 @@ rules: - namespaces - services - secrets + - configmaps verbs: - list - watch @@ -76,6 +77,7 @@ rules: - gateways - httproutes - referencegrants + - backendtlspolicies verbs: - list - watch @@ -85,6 +87,7 @@ rules: - httproutes/status - gateways/status - gatewayclasses/status + - backendtlspolicies/status verbs: - update - apiGroups: diff --git a/internal/mode/static/build_statuses_test.go b/internal/mode/static/build_statuses_test.go index 65f27a1c9b..146b34409b 100644 --- a/internal/mode/static/build_statuses_test.go +++ b/internal/mode/static/build_statuses_test.go @@ -616,25 +616,27 @@ func TestBuildGatewayStatuses(t *testing.T) { } func TestBuildBackendTLSPolicyStatuses(t *testing.T) { - getBackendTLSPolicy := func( - name string, - valid bool, - ignored bool, - isReferenced bool, - conditions []conditions.Condition, - ) *graph.BackendTLSPolicy { + type policyCfg struct { + Name string + Conditions []conditions.Condition + Valid bool + Ignored bool + IsReferenced bool + } + + getBackendTLSPolicy := func(policyCfg policyCfg) *graph.BackendTLSPolicy { return &graph.BackendTLSPolicy{ Source: &v1alpha2.BackendTLSPolicy{ ObjectMeta: metav1.ObjectMeta{ Namespace: "test", - Name: name, + Name: policyCfg.Name, Generation: 1, }, }, - Valid: valid, - Ignored: ignored, - IsReferenced: isReferenced, - Conditions: conditions, + Valid: policyCfg.Valid, + Ignored: policyCfg.Ignored, + IsReferenced: policyCfg.IsReferenced, + Conditions: policyCfg.Conditions, Gateway: types.NamespacedName{Name: "gateway", Namespace: "test"}, } } @@ -642,6 +644,30 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { attachedConds := []conditions.Condition{staticConds.NewBackendTLSPolicyAccepted()} invalidConds := []conditions.Condition{staticConds.NewBackendTLSPolicyInvalid("invalid backendTLSPolicy")} + validPolicyCfg := policyCfg{ + Name: "valid-bt", + Valid: true, + IsReferenced: true, + Conditions: attachedConds, + } + + invalidPolicyCfg := policyCfg{ + Name: "invalid-bt", + IsReferenced: true, + Conditions: invalidConds, + } + + ignoredPolicyCfg := policyCfg{ + Name: "ignored-bt", + Ignored: true, + IsReferenced: true, + } + + notReferencedPolicyCfg := policyCfg{ + Name: "not-referenced", + Valid: true, + } + tests := []struct { backendTLSPolicies map[types.NamespacedName]*graph.BackendTLSPolicy expected status.BackendTLSPolicyStatuses @@ -654,7 +680,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { { name: "valid backendTLSPolicy", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, false, true, attachedConds), + {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy(validPolicyCfg), }, expected: status.BackendTLSPolicyStatuses{ {Namespace: "test", Name: "valid-bt"}: { @@ -670,7 +696,7 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { { name: "invalid backendTLSPolicy", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "invalid-bt"}: getBackendTLSPolicy("invalid-bt", false, false, true, invalidConds), + {Namespace: "test", Name: "invalid-bt"}: getBackendTLSPolicy(invalidPolicyCfg), }, expected: status.BackendTLSPolicyStatuses{ {Namespace: "test", Name: "invalid-bt"}: { @@ -686,16 +712,16 @@ func TestBuildBackendTLSPolicyStatuses(t *testing.T) { { name: "ignored or not referenced backendTLSPolicies", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, true, nil), - {Namespace: "test", Name: "not-referenced"}: getBackendTLSPolicy("not-referenced", true, false, false, nil), + {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy(ignoredPolicyCfg), + {Namespace: "test", Name: "not-referenced"}: getBackendTLSPolicy(notReferencedPolicyCfg), }, expected: status.BackendTLSPolicyStatuses{}, }, { name: "mix valid and ignored backendTLSPolicies", backendTLSPolicies: map[types.NamespacedName]*graph.BackendTLSPolicy{ - {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy("ignored-bt", false, true, true, nil), - {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy("valid-bt", true, false, true, attachedConds), + {Namespace: "test", Name: "ignored-bt"}: getBackendTLSPolicy(ignoredPolicyCfg), + {Namespace: "test", Name: "valid-bt"}: getBackendTLSPolicy(validPolicyCfg), }, expected: status.BackendTLSPolicyStatuses{ {Namespace: "test", Name: "valid-bt"}: { diff --git a/internal/mode/static/nginx/config/servers.go b/internal/mode/static/nginx/config/servers.go index aa6f5ab076..a80de123f7 100644 --- a/internal/mode/static/nginx/config/servers.go +++ b/internal/mode/static/nginx/config/servers.go @@ -258,7 +258,7 @@ func updateLocationsForFilters( proxyPass := createProxyPass( matchRule.BackendGroup, matchRule.Filters.RequestURLRewrite, - buildLocations[i].ProxySSLVerify != nil, + generateProtocolString(buildLocations[i].ProxySSLVerify), ) buildLocations[i].ProxyPass = proxyPass } @@ -266,6 +266,13 @@ func updateLocationsForFilters( return buildLocations } +func generateProtocolString(ssl *http.ProxySSLVerify) string { + if ssl != nil { + return "https" + } + return "http" +} + func createProxyTLSFromBackends(backends []dataplane.Backend) *http.ProxySSLVerify { if len(backends) == 0 { return nil @@ -467,18 +474,13 @@ func isPathOnlyMatch(match dataplane.Match) bool { func createProxyPass( backendGroup dataplane.BackendGroup, filter *dataplane.HTTPURLRewriteFilter, - enableTLS bool, + protocol string, ) string { var requestURI string if filter == nil || filter.Path == nil { requestURI = "$request_uri" } - protocol := "http" - if enableTLS { - protocol = "https" - } - backendName := backendGroupName(backendGroup) if backendGroupNeedsSplit(backendGroup) { return protocol + "://$" + convertStringToSafeVariableName(backendName) + requestURI diff --git a/internal/mode/static/nginx/config/servers_test.go b/internal/mode/static/nginx/config/servers_test.go index fbd5b75006..218e59d9d3 100644 --- a/internal/mode/static/nginx/config/servers_test.go +++ b/internal/mode/static/nginx/config/servers_test.go @@ -1714,7 +1714,7 @@ func TestCreateProxyPass(t *testing.T) { } for _, tc := range tests { - result := createProxyPass(tc.grp, tc.rewrite, false) + result := createProxyPass(tc.grp, tc.rewrite, generateProtocolString(nil)) g.Expect(result).To(Equal(tc.expected)) } } diff --git a/internal/mode/static/state/change_processor_test.go b/internal/mode/static/state/change_processor_test.go index df91241d24..790f42e49e 100644 --- a/internal/mode/static/state/change_processor_test.go +++ b/internal/mode/static/state/change_processor_test.go @@ -1835,7 +1835,6 @@ var _ = Describe("ChangeProcessor", func() { }, } btlsUpdated = btls.DeepCopy() - btlsUpdated.Generation++ }) // Changing change - a change that makes processor.Process() report changed // Non-changing change - a change that doesn't do that diff --git a/internal/mode/static/state/conditions/conditions.go b/internal/mode/static/state/conditions/conditions.go index 454f51aa52..894fb3596c 100644 --- a/internal/mode/static/state/conditions/conditions.go +++ b/internal/mode/static/state/conditions/conditions.go @@ -66,19 +66,6 @@ const ( RouteMessageFailedNginxReload = GatewayMessageFailedNginxReload + ". NGINX may still be configured " + "for this HTTPRoute. However, future updates to this resource will not be configured until the Gateway " + "is programmed again" - - // BackendTLSPolicyConditionAttached is the condition type indicating the BackendTLS policy is valid and attached to - // the Gateway. - BackendTLSPolicyConditionAttached BackendTLSPolicyConditionType = "Attached" - - // BackendTLSPolicyReasonAttached is the condition reason for the BackendTLSPolicy Attached condition. - BackendTLSPolicyReasonAttached BackendTLSPolicyConditionReason = "BackendTLSPolicyAttached" - - // BackendTLSPolicyConditionValid is the condition type indicating whether the BackendTLS policy is valid. - BackendTLSPolicyConditionValid BackendTLSPolicyConditionType = "Valid" - - // BackendTLSPolicyReasonInvalid is the condition reason for the BackendTLSPolicy Valid condition being False. - BackendTLSPolicyReasonInvalid BackendTLSPolicyConditionReason = "BackendTLSPolicyInvalid" ) // NewTODO returns a Condition that can be used as a placeholder for a condition that is not yet implemented. diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go index 57e844914f..4f5923d024 100644 --- a/internal/mode/static/state/graph/backend_tls_policy.go +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -37,18 +37,26 @@ func processBackendTLSPolicies( ctlrName string, gateway *Gateway, ) map[types.NamespacedName]*BackendTLSPolicy { - if len(backendTLSPolicies) == 0 { + if len(backendTLSPolicies) == 0 || gateway == nil { return nil } + processedBackendTLSPolicies := make(map[types.NamespacedName]*BackendTLSPolicy, len(backendTLSPolicies)) for nsname, backendTLSPolicy := range backendTLSPolicies { - valid, ignored, caCertRef, conds := validateBackendTLSPolicy( + var caCertRef types.NamespacedName + valid, ignored, conds := validateBackendTLSPolicy( backendTLSPolicy, configMapResolver, ctlrName, gateway, ) + if valid && !ignored && backendTLSPolicy.Spec.TLS.CACertRefs != nil { + caCertRef = types.NamespacedName{ + Namespace: backendTLSPolicy.Namespace, Name: string(backendTLSPolicy.Spec.TLS.CACertRefs[0].Name), + } + } + processedBackendTLSPolicies[nsname] = &BackendTLSPolicy{ Source: backendTLSPolicy, Valid: valid, @@ -69,11 +77,9 @@ func validateBackendTLSPolicy( configMapResolver *configMapResolver, ctlrName string, gateway *Gateway, -) (bool, bool, types.NamespacedName, []conditions.Condition) { - var conds []conditions.Condition - valid := true - ignored := false - var caCertRef types.NamespacedName +) (valid, ignored bool, conds []conditions.Condition) { + valid = true + ignored = false if err := validateAncestorMaxCount(backendTLSPolicy, ctlrName, gateway); err != nil { valid = false ignored = true @@ -87,10 +93,6 @@ func validateBackendTLSPolicy( valid = false conds = append(conds, staticConds.NewBackendTLSPolicyInvalid( fmt.Sprintf("invalid CACertRef: %s", err.Error()))) - } else { - caCertRef = types.NamespacedName{ - Namespace: backendTLSPolicy.Namespace, Name: string(backendTLSPolicy.Spec.TLS.CACertRefs[0].Name), - } } } else if backendTLSPolicy.Spec.TLS.WellKnownCACerts != nil { if err := validateBackendTLSWellKnownCACerts(backendTLSPolicy); err != nil { @@ -102,7 +104,7 @@ func validateBackendTLSPolicy( valid = false conds = append(conds, staticConds.NewBackendTLSPolicyInvalid("CACertRefs and WellKnownCACerts are both nil")) } - return valid, ignored, caCertRef, conds + return valid, ignored, conds } func validateAncestorMaxCount(backendTLSPolicy *v1alpha2.BackendTLSPolicy, ctlrName string, gateway *Gateway) error { diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go index f1d4958e22..0e9bfa006f 100644 --- a/internal/mode/static/state/graph/backend_tls_policy_test.go +++ b/internal/mode/static/state/graph/backend_tls_policy_test.go @@ -13,6 +13,70 @@ import ( "github.com/nginxinc/nginx-gateway-fabric/internal/framework/helpers" ) +func TestProcessBackendTLSPoliciesEmpty(t *testing.T) { + backendTLSPolicies := map[types.NamespacedName]*v1alpha2.BackendTLSPolicy{ + {Namespace: "test", Name: "tls-policy"}: { + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: v1alpha2.PolicyTargetReferenceWithSectionName{ + PolicyTargetReference: v1alpha2.PolicyTargetReference{ + Kind: "Service", + Name: "service1", + Namespace: (*gatewayv1.Namespace)(helpers.GetPointer("test")), + }, + }, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: []gatewayv1.LocalObjectReference{ + { + Kind: "ConfigMap", + Name: "configmap", + Group: "", + }, + }, + Hostname: "foo.test.com", + }, + }, + }, + } + + gateway := &Gateway{ + Source: &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "test"}}, + } + + tests := []struct { + expected map[types.NamespacedName]*BackendTLSPolicy + gateway *Gateway + backendTLSPolicies map[types.NamespacedName]*v1alpha2.BackendTLSPolicy + name string + }{ + { + name: "no policies", + expected: nil, + gateway: gateway, + backendTLSPolicies: nil, + }, + { + name: "nil gateway", + expected: nil, + backendTLSPolicies: backendTLSPolicies, + gateway: nil, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + g := NewWithT(t) + + processed := processBackendTLSPolicies(test.backendTLSPolicies, nil, "test", test.gateway) + + g.Expect(processed).To(Equal(test.expected)) + }) + } +} + func TestValidateBackendTLSPolicy(t *testing.T) { targetRefNormalCase := &v1alpha2.PolicyTargetReferenceWithSectionName{ PolicyTargetReference: v1alpha2.PolicyTargetReference{ @@ -88,11 +152,11 @@ func TestValidateBackendTLSPolicy(t *testing.T) { ancestorsWithUs := append(ancestors, getAncestorRef("test", "gateway")) tests := []struct { - tlsPolicy *v1alpha2.BackendTLSPolicy - caCertName types.NamespacedName - name string - isValid bool - ignored bool + tlsPolicy *v1alpha2.BackendTLSPolicy + gateway *Gateway + name string + isValid bool + ignored bool }{ { name: "normal case with ca cert refs", @@ -109,8 +173,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: true, - caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + isValid: true, }, { name: "normal case with ca cert refs and 16 ancestors including us", @@ -130,8 +193,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { Ancestors: ancestorsWithUs, }, }, - isValid: true, - caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, + isValid: true, }, { name: "normal case with well known certs", @@ -165,8 +227,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, - caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, }, { name: "invalid ca cert ref name", @@ -183,7 +243,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, }, { name: "invalid ca cert ref kind", @@ -200,7 +259,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, }, { name: "invalid ca cert ref group", @@ -217,7 +275,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, }, { name: "invalid case with well known certs", @@ -234,7 +291,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, }, { name: "invalid case neither TLS config option chosen", @@ -250,7 +306,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, }, { name: "invalid case with too many ca cert refs", @@ -267,7 +322,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, - isValid: false, }, { name: "invalid case with too many ancestors", @@ -287,9 +341,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { Ancestors: ancestors, }, }, - isValid: false, - caCertName: types.NamespacedName{Namespace: "test", Name: "configmap"}, - ignored: true, + ignored: true, }, } @@ -324,7 +376,7 @@ func TestValidateBackendTLSPolicy(t *testing.T) { Source: &gatewayv1.Gateway{ObjectMeta: metav1.ObjectMeta{Name: "gateway", Namespace: "test"}}, } - valid, ignored, caCertName, conds := validateBackendTLSPolicy( + valid, ignored, conds := validateBackendTLSPolicy( test.tlsPolicy, configMapResolver, "test", @@ -332,7 +384,6 @@ func TestValidateBackendTLSPolicy(t *testing.T) { ) g.Expect(valid).To(Equal(test.isValid)) - g.Expect(caCertName).To(Equal(test.caCertName)) g.Expect(ignored).To(Equal(test.ignored)) if !test.isValid && !test.ignored { g.Expect(conds).To(HaveLen(1)) diff --git a/internal/mode/static/state/graph/graph_test.go b/internal/mode/static/state/graph/graph_test.go index b784f9a8e2..8a278d673a 100644 --- a/internal/mode/static/state/graph/graph_test.go +++ b/internal/mode/static/state/graph/graph_test.go @@ -103,7 +103,7 @@ func TestBuildGraph(t *testing.T) { }, } - btpAttachedConds := []conditions.Condition{ + btpAcceptedConds := []conditions.Condition{ staticConds.NewBackendTLSPolicyAccepted(), staticConds.NewBackendTLSPolicyAccepted(), } @@ -138,7 +138,7 @@ func TestBuildGraph(t *testing.T) { Valid: true, IsReferenced: true, Gateway: types.NamespacedName{Namespace: "test", Name: "gateway-1"}, - Conditions: btpAttachedConds, + Conditions: btpAcceptedConds, CaCertRef: types.NamespacedName{Namespace: "service", Name: "configmap"}, } @@ -696,25 +696,27 @@ func TestIsReferenced(t *testing.T) { expected: false, }, - // Edge cases + // ConfigMap cases { - name: "ConfigMap in graph's ReferencedConfigMaps passes", + name: "ConfigMap in graph's ReferencedConfigMaps is referenced", resource: baseConfigMap, graph: graph, expected: true, }, { - name: "ConfigMap not in ReferencedConfigMaps with same Namespace and different Name fails", + name: "ConfigMap not in ReferencedConfigMaps with same Namespace and different Name is not referenced", resource: sameNamespaceDifferentNameConfigMap, graph: graph, expected: false, }, { - name: "ConfigMap not in ReferencedConfigMaps with different Namespace and same Name fails", + name: "ConfigMap not in ReferencedConfigMaps with different Namespace and same Name is not referenced", resource: differentNamespaceSameNameConfigMap, graph: graph, expected: false, }, + + // Edge cases { name: "Resource is not supported by IsReferenced", resource: &gatewayv1.HTTPRoute{}, From 949c08e5f538837a4a81c8194549bca2b4e71c92 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Wed, 14 Feb 2024 16:43:27 +0000 Subject: [PATCH 12/13] Add additional test case --- internal/mode/static/state/change_processor.go | 3 +++ .../static/state/graph/backend_tls_policy.go | 6 +++++- .../state/graph/backend_tls_policy_test.go | 17 +++++++++++++++++ 3 files changed, 25 insertions(+), 1 deletion(-) diff --git a/internal/mode/static/state/change_processor.go b/internal/mode/static/state/change_processor.go index 07a8c26416..210238f0fe 100644 --- a/internal/mode/static/state/change_processor.go +++ b/internal/mode/static/state/change_processor.go @@ -231,6 +231,9 @@ func NewChangeProcessorImpl(cfg ChangeProcessorConfig) *ChangeProcessorImpl { // belong to the NGINX Gateway Fabric or an HTTPRoute that doesn't belong to any of the Gateways of the // NGINX Gateway Fabric. Find a way to ignore changes that don't affect the configuration and/or statuses of // the resources. +// Tracking issues: https://github.com/nginxinc/nginx-gateway-fabric/issues/1123, +// https://github.com/nginxinc/nginx-gateway-fabric/issues/1124, +// https://github.com/nginxinc/nginx-gateway-fabric/issues/1577 // FIXME(pleshakov) // Remove CaptureUpsertChange() and CaptureDeleteChange() from ChangeProcessor and pass all changes directly to diff --git a/internal/mode/static/state/graph/backend_tls_policy.go b/internal/mode/static/state/graph/backend_tls_policy.go index 4f5923d024..ee6ed9a366 100644 --- a/internal/mode/static/state/graph/backend_tls_policy.go +++ b/internal/mode/static/state/graph/backend_tls_policy.go @@ -88,7 +88,11 @@ func validateBackendTLSPolicy( valid = false conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(fmt.Sprintf("invalid hostname: %s", err.Error()))) } - if backendTLSPolicy.Spec.TLS.CACertRefs != nil && len(backendTLSPolicy.Spec.TLS.CACertRefs) > 0 { + if backendTLSPolicy.Spec.TLS.CACertRefs != nil && backendTLSPolicy.Spec.TLS.WellKnownCACerts != nil { + valid = false + msg := "CACertRefs and WellKnownCACerts are mutually exclusive" + conds = append(conds, staticConds.NewBackendTLSPolicyInvalid(msg)) + } else if backendTLSPolicy.Spec.TLS.CACertRefs != nil && len(backendTLSPolicy.Spec.TLS.CACertRefs) > 0 { if err := validateBackendTLSCACertRef(backendTLSPolicy, configMapResolver); err != nil { valid = false conds = append(conds, staticConds.NewBackendTLSPolicyInvalid( diff --git a/internal/mode/static/state/graph/backend_tls_policy_test.go b/internal/mode/static/state/graph/backend_tls_policy_test.go index 0e9bfa006f..0cedce2222 100644 --- a/internal/mode/static/state/graph/backend_tls_policy_test.go +++ b/internal/mode/static/state/graph/backend_tls_policy_test.go @@ -323,6 +323,23 @@ func TestValidateBackendTLSPolicy(t *testing.T) { }, }, }, + { + name: "invalid case with too both ca cert refs and wellknowncerts", + tlsPolicy: &v1alpha2.BackendTLSPolicy{ + ObjectMeta: metav1.ObjectMeta{ + Name: "tls-policy", + Namespace: "test", + }, + Spec: v1alpha2.BackendTLSPolicySpec{ + TargetRef: *targetRefNormalCase, + TLS: v1alpha2.BackendTLSPolicyConfig{ + CACertRefs: localObjectRefNormalCase, + Hostname: "foo.test.com", + WellKnownCACerts: (helpers.GetPointer(v1alpha2.WellKnownCACertSystem)), + }, + }, + }, + }, { name: "invalid case with too many ancestors", tlsPolicy: &v1alpha2.BackendTLSPolicy{ From 244391d66950ea0e703d104f707a8b7197e63796 Mon Sep 17 00:00:00 2001 From: Ciara Stacke Date: Wed, 14 Feb 2024 17:41:37 +0000 Subject: [PATCH 13/13] Add conditional to RBAC and generate new manifests --- Makefile | 9 +- deploy/helm-chart/templates/rbac.yaml | 6 + .../manifests/nginx-gateway-experimental.yaml | 291 +++++++++++++++++ deploy/manifests/nginx-gateway.yaml | 3 - .../nginx-plus-gateway-experimental.yaml | 292 ++++++++++++++++++ deploy/manifests/nginx-plus-gateway.yaml | 3 - docs/developer/quickstart.md | 9 + .../installation/installing-ngf/manifests.md | 33 +- 8 files changed, 617 insertions(+), 29 deletions(-) create mode 100644 deploy/manifests/nginx-gateway-experimental.yaml create mode 100644 deploy/manifests/nginx-plus-gateway-experimental.yaml diff --git a/Makefile b/Makefile index 1f717fe4eb..cac2bf24e0 100644 --- a/Makefile +++ b/Makefile @@ -81,18 +81,17 @@ generate-crds: ## Generate CRDs and Go types using kubebuilder go run sigs.k8s.io/controller-tools/cmd/controller-gen object paths=./apis/... .PHONY: generate-manifests -generate-manifests: generate-manifests-plus ## Generate manifests using Helm. +generate-manifests: ## Generate manifests using Helm. cp $(CHART_DIR)/crds/* $(MANIFEST_DIR)/crds/ helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) $(HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE) -n nginx-gateway | cat $(strip $(MANIFEST_DIR))/namespace.yaml - > $(strip $(MANIFEST_DIR))/nginx-gateway.yaml + helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) $(HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE) --set nginx.plus=true --set nginx.image.repository=$(NGINX_PLUS_PREFIX) -n nginx-gateway | cat $(strip $(MANIFEST_DIR))/namespace.yaml - > $(strip $(MANIFEST_DIR))/nginx-plus-gateway.yaml + helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) $(HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE) --set nginxGateway.gwAPIExperimentalFeatures.enable=true -n nginx-gateway | cat $(strip $(MANIFEST_DIR))/namespace.yaml - > $(strip $(MANIFEST_DIR))/nginx-gateway-experimental.yaml + helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) $(HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE) --set nginxGateway.gwAPIExperimentalFeatures.enable=true --set nginx.plus=true --set nginx.image.repository=$(NGINX_PLUS_PREFIX) -n nginx-gateway | cat $(strip $(MANIFEST_DIR))/namespace.yaml - > $(strip $(MANIFEST_DIR))/nginx-plus-gateway-experimental.yaml helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) --set metrics.enable=false -n nginx-gateway -s templates/deployment.yaml > conformance/provisioner/static-deployment.yaml helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) -n nginx-gateway -s templates/service.yaml > $(strip $(MANIFEST_DIR))/service/loadbalancer.yaml helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) --set service.annotations.'service\.beta\.kubernetes\.io\/aws-load-balancer-type'="nlb" -n nginx-gateway -s templates/service.yaml > $(strip $(MANIFEST_DIR))/service/loadbalancer-aws-nlb.yaml helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) --set service.type=NodePort --set service.externalTrafficPolicy="" -n nginx-gateway -s templates/service.yaml > $(strip $(MANIFEST_DIR))/service/nodeport.yaml -.PHONY: generate-manifests-plus -generate-manifests-plus: ## Generate manifests using Helm for NGINX Plus. - helm template nginx-gateway $(CHART_DIR) $(HELM_TEMPLATE_COMMON_ARGS) $(HELM_TEMPLATE_EXTRA_ARGS_FOR_ALL_MANIFESTS_FILE) --set nginx.plus=true --set nginx.image.repository=$(NGINX_PLUS_PREFIX) -n nginx-gateway | cat $(strip $(MANIFEST_DIR))/namespace.yaml - > $(strip $(MANIFEST_DIR))/nginx-plus-gateway.yaml - .PHONY: crds-release-file crds-release-file: ## Generate combined crds file for releases scripts/combine-crds.sh diff --git a/deploy/helm-chart/templates/rbac.yaml b/deploy/helm-chart/templates/rbac.yaml index 4e6c798b1e..16939ef6c9 100644 --- a/deploy/helm-chart/templates/rbac.yaml +++ b/deploy/helm-chart/templates/rbac.yaml @@ -32,7 +32,9 @@ rules: - namespaces - services - secrets +{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }} - configmaps +{{- end }} verbs: - list - watch @@ -77,7 +79,9 @@ rules: - gateways - httproutes - referencegrants +{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }} - backendtlspolicies +{{- end }} verbs: - list - watch @@ -87,7 +91,9 @@ rules: - httproutes/status - gateways/status - gatewayclasses/status +{{- if .Values.nginxGateway.gwAPIExperimentalFeatures.enable }} - backendtlspolicies/status +{{- end }} verbs: - update - apiGroups: diff --git a/deploy/manifests/nginx-gateway-experimental.yaml b/deploy/manifests/nginx-gateway-experimental.yaml new file mode 100644 index 0000000000..0625b4b5e2 --- /dev/null +++ b/deploy/manifests/nginx-gateway-experimental.yaml @@ -0,0 +1,291 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: nginx-gateway +--- +# Source: nginx-gateway-fabric/templates/rbac.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nginx-gateway + namespace: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" + annotations: + {} +--- +# Source: nginx-gateway-fabric/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +rules: +- apiGroups: + - "" + resources: + - namespaces + - services + - secrets + - configmaps + verbs: + - list + - watch +# FIXME(bjee19): make nodes, pods, replicasets permission dependent on telemetry being enabled. +# https://github.com/nginxinc/nginx-gateway-fabric/issues/1317. +- apiGroups: + - "" + resources: + - pods + verbs: + - get +- apiGroups: + - "" + resources: + - nodes + verbs: + - list +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + - gateways + - httproutes + - referencegrants + - backendtlspolicies + verbs: + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/status + - gateways/status + - gatewayclasses/status + - backendtlspolicies/status + verbs: + - update +- apiGroups: + - gateway.nginx.org + resources: + - nginxgateways + verbs: + - get + - list + - watch +- apiGroups: + - gateway.nginx.org + resources: + - nginxgateways/status + verbs: + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - list + - watch +--- +# Source: nginx-gateway-fabric/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: nginx-gateway +subjects: +- kind: ServiceAccount + name: nginx-gateway + namespace: nginx-gateway +--- +# Source: nginx-gateway-fabric/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-gateway + namespace: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + template: + metadata: + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9113" + spec: + containers: + - args: + - static-mode + - --gateway-ctlr-name=gateway.nginx.org/nginx-gateway-controller + - --gatewayclass=nginx + - --config=nginx-gateway-config + - --service=nginx-gateway + - --metrics-port=9113 + - --health-port=8081 + - --leader-election-lock-name=nginx-gateway-leader-election + - --gateway-api-experimental-features + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + image: ghcr.io/nginxinc/nginx-gateway-fabric:edge + imagePullPolicy: Always + name: nginx-gateway + ports: + - name: metrics + containerPort: 9113 + - name: health + containerPort: 8081 + readinessProbe: + httpGet: + path: /readyz + port: health + initialDelaySeconds: 3 + periodSeconds: 1 + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - KILL + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 102 + runAsGroup: 1001 + volumeMounts: + - name: nginx-conf + mountPath: /etc/nginx/conf.d + - name: nginx-secrets + mountPath: /etc/nginx/secrets + - name: nginx-run + mountPath: /var/run/nginx + - image: ghcr.io/nginxinc/nginx-gateway-fabric/nginx:edge + imagePullPolicy: Always + name: nginx + ports: + - containerPort: 80 + name: http + - containerPort: 443 + name: https + securityContext: + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 101 + runAsGroup: 1001 + volumeMounts: + - name: nginx-conf + mountPath: /etc/nginx/conf.d + - name: nginx-secrets + mountPath: /etc/nginx/secrets + - name: nginx-run + mountPath: /var/run/nginx + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-lib + mountPath: /var/lib/nginx + terminationGracePeriodSeconds: 30 + serviceAccountName: nginx-gateway + shareProcessNamespace: true + securityContext: + fsGroup: 1001 + runAsNonRoot: true + volumes: + - name: nginx-conf + emptyDir: {} + - name: nginx-secrets + emptyDir: {} + - name: nginx-run + emptyDir: {} + - name: nginx-cache + emptyDir: {} + - name: nginx-lib + emptyDir: {} +--- +# Source: nginx-gateway-fabric/templates/gatewayclass.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: nginx + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +spec: + controllerName: gateway.nginx.org/nginx-gateway-controller +--- +# Source: nginx-gateway-fabric/templates/nginxgateway.yaml +apiVersion: gateway.nginx.org/v1alpha1 +kind: NginxGateway +metadata: + name: nginx-gateway-config + namespace: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +spec: + logging: + level: info diff --git a/deploy/manifests/nginx-gateway.yaml b/deploy/manifests/nginx-gateway.yaml index 5d39190807..578e4950b3 100644 --- a/deploy/manifests/nginx-gateway.yaml +++ b/deploy/manifests/nginx-gateway.yaml @@ -32,7 +32,6 @@ rules: - namespaces - services - secrets - - configmaps verbs: - list - watch @@ -77,7 +76,6 @@ rules: - gateways - httproutes - referencegrants - - backendtlspolicies verbs: - list - watch @@ -87,7 +85,6 @@ rules: - httproutes/status - gateways/status - gatewayclasses/status - - backendtlspolicies/status verbs: - update - apiGroups: diff --git a/deploy/manifests/nginx-plus-gateway-experimental.yaml b/deploy/manifests/nginx-plus-gateway-experimental.yaml new file mode 100644 index 0000000000..49d099b894 --- /dev/null +++ b/deploy/manifests/nginx-plus-gateway-experimental.yaml @@ -0,0 +1,292 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: nginx-gateway +--- +# Source: nginx-gateway-fabric/templates/rbac.yaml +apiVersion: v1 +kind: ServiceAccount +metadata: + name: nginx-gateway + namespace: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" + annotations: + {} +--- +# Source: nginx-gateway-fabric/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +rules: +- apiGroups: + - "" + resources: + - namespaces + - services + - secrets + - configmaps + verbs: + - list + - watch +# FIXME(bjee19): make nodes, pods, replicasets permission dependent on telemetry being enabled. +# https://github.com/nginxinc/nginx-gateway-fabric/issues/1317. +- apiGroups: + - "" + resources: + - pods + verbs: + - get +- apiGroups: + - "" + resources: + - nodes + verbs: + - list +- apiGroups: + - "" + resources: + - events + verbs: + - create + - patch +- apiGroups: + - apps + resources: + - replicasets + verbs: + - get +- apiGroups: + - discovery.k8s.io + resources: + - endpointslices + verbs: + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - gatewayclasses + - gateways + - httproutes + - referencegrants + - backendtlspolicies + verbs: + - list + - watch +- apiGroups: + - gateway.networking.k8s.io + resources: + - httproutes/status + - gateways/status + - gatewayclasses/status + - backendtlspolicies/status + verbs: + - update +- apiGroups: + - gateway.nginx.org + resources: + - nginxgateways + verbs: + - get + - list + - watch +- apiGroups: + - gateway.nginx.org + resources: + - nginxgateways/status + verbs: + - update +- apiGroups: + - coordination.k8s.io + resources: + - leases + verbs: + - create + - get + - update +- apiGroups: + - apiextensions.k8s.io + resources: + - customresourcedefinitions + verbs: + - list + - watch +--- +# Source: nginx-gateway-fabric/templates/rbac.yaml +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRoleBinding +metadata: + name: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: nginx-gateway +subjects: +- kind: ServiceAccount + name: nginx-gateway + namespace: nginx-gateway +--- +# Source: nginx-gateway-fabric/templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-gateway + namespace: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + template: + metadata: + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + annotations: + prometheus.io/scrape: "true" + prometheus.io/port: "9113" + spec: + containers: + - args: + - static-mode + - --gateway-ctlr-name=gateway.nginx.org/nginx-gateway-controller + - --gatewayclass=nginx + - --config=nginx-gateway-config + - --service=nginx-gateway + - --nginx-plus + - --metrics-port=9113 + - --health-port=8081 + - --leader-election-lock-name=nginx-gateway-leader-election + - --gateway-api-experimental-features + env: + - name: POD_IP + valueFrom: + fieldRef: + fieldPath: status.podIP + - name: POD_NAMESPACE + valueFrom: + fieldRef: + fieldPath: metadata.namespace + - name: POD_NAME + valueFrom: + fieldRef: + fieldPath: metadata.name + image: ghcr.io/nginxinc/nginx-gateway-fabric:edge + imagePullPolicy: Always + name: nginx-gateway + ports: + - name: metrics + containerPort: 9113 + - name: health + containerPort: 8081 + readinessProbe: + httpGet: + path: /readyz + port: health + initialDelaySeconds: 3 + periodSeconds: 1 + securityContext: + allowPrivilegeEscalation: false + capabilities: + add: + - KILL + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 102 + runAsGroup: 1001 + volumeMounts: + - name: nginx-conf + mountPath: /etc/nginx/conf.d + - name: nginx-secrets + mountPath: /etc/nginx/secrets + - name: nginx-run + mountPath: /var/run/nginx + - image: nginx-gateway-fabric/nginx-plus:edge + imagePullPolicy: Always + name: nginx + ports: + - containerPort: 80 + name: http + - containerPort: 443 + name: https + securityContext: + capabilities: + add: + - NET_BIND_SERVICE + drop: + - ALL + readOnlyRootFilesystem: true + runAsUser: 101 + runAsGroup: 1001 + volumeMounts: + - name: nginx-conf + mountPath: /etc/nginx/conf.d + - name: nginx-secrets + mountPath: /etc/nginx/secrets + - name: nginx-run + mountPath: /var/run/nginx + - name: nginx-cache + mountPath: /var/cache/nginx + - name: nginx-lib + mountPath: /var/lib/nginx + terminationGracePeriodSeconds: 30 + serviceAccountName: nginx-gateway + shareProcessNamespace: true + securityContext: + fsGroup: 1001 + runAsNonRoot: true + volumes: + - name: nginx-conf + emptyDir: {} + - name: nginx-secrets + emptyDir: {} + - name: nginx-run + emptyDir: {} + - name: nginx-cache + emptyDir: {} + - name: nginx-lib + emptyDir: {} +--- +# Source: nginx-gateway-fabric/templates/gatewayclass.yaml +apiVersion: gateway.networking.k8s.io/v1 +kind: GatewayClass +metadata: + name: nginx + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +spec: + controllerName: gateway.nginx.org/nginx-gateway-controller +--- +# Source: nginx-gateway-fabric/templates/nginxgateway.yaml +apiVersion: gateway.nginx.org/v1alpha1 +kind: NginxGateway +metadata: + name: nginx-gateway-config + namespace: nginx-gateway + labels: + app.kubernetes.io/name: nginx-gateway + app.kubernetes.io/instance: nginx-gateway + app.kubernetes.io/version: "edge" +spec: + logging: + level: info diff --git a/deploy/manifests/nginx-plus-gateway.yaml b/deploy/manifests/nginx-plus-gateway.yaml index 3c979e6a25..23759bdf02 100644 --- a/deploy/manifests/nginx-plus-gateway.yaml +++ b/deploy/manifests/nginx-plus-gateway.yaml @@ -32,7 +32,6 @@ rules: - namespaces - services - secrets - - configmaps verbs: - list - watch @@ -77,7 +76,6 @@ rules: - gateways - httproutes - referencegrants - - backendtlspolicies verbs: - list - watch @@ -87,7 +85,6 @@ rules: - httproutes/status - gateways/status - gatewayclasses/status - - backendtlspolicies/status verbs: - update - apiGroups: diff --git a/docs/developer/quickstart.md b/docs/developer/quickstart.md index 6675dcefa9..41de3a4b6d 100644 --- a/docs/developer/quickstart.md +++ b/docs/developer/quickstart.md @@ -168,6 +168,15 @@ This will build the docker images `nginx-gateway-fabric:` and `nginx- kubectl apply -f deploy/manifests/service/nodeport.yaml ``` + - To install with experimental manifests: + + ```shell + make generate-manifests HELM_TEMPLATE_COMMON_ARGS="--set nginxGateway.image.repository=nginx-gateway-fabric --set nginxGateway.image.tag=$(whoami) --set nginxGateway.image.pullPolicy=Never --set nginx.image.repository=nginx-gateway-fabric/nginx --set nginx.image.tag=$(whoami) --set nginx.image.pullPolicy=Never" + kubectl apply -f deploy/manifests/crds + kubectl apply -f deploy/manifests/nginx-gateway-experimental.yaml + kubectl apply -f deploy/manifests/service/nodeport.yaml + ``` + ### Run Examples To make sure NGF is running properly, try out the [examples](/examples). diff --git a/site/content/installation/installing-ngf/manifests.md b/site/content/installation/installing-ngf/manifests.md index e83a18c122..94ca6ab1aa 100644 --- a/site/content/installation/installing-ngf/manifests.md +++ b/site/content/installation/installing-ngf/manifests.md @@ -75,24 +75,21 @@ Deploying NGINX Gateway Fabric with Kubernetes manifests takes only a few steps. #### Enable experimental features -We support a subset of the additional features provided by the Gateway API experimental channel. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric, edit the `deploy/manifests/nginx-gateway.yaml` to add the `gateway-api-experimental-features` flag to the nginx-gateway deployment spec: - -```yaml -<...> -kind: Deployment -metadata: - name: nginx-gateway -<...> -spec: - <...> - template: - <...> - spec: - containers: - - args: - <...> - - --gateway-api-experimental-features -``` +We support a subset of the additional features provided by the Gateway API experimental channel. To enable the experimental features of Gateway API which are supported by NGINX Gateway Fabric: + +- For NGINX: + + ```shell + kubectl apply -f deploy/manifests/nginx-gateway-experimental.yaml + ``` + +- For NGINX Plus + + ```shell + kubectl apply -f deploy/manifests/nginx-plus-gateway-experimental.yaml + ``` + + Update the nginx-plus-gateway-experimental.yaml file to include your chosen image from the F5 Container registry or your custom container image. {{}}Requires the Gateway APIs installed from the experimental channel.{{}}