From a7f80816a343edcf84dce7854cbf55bc6f104481 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Wed, 11 Nov 2020 19:38:40 -0800 Subject: [PATCH 01/11] Add OIDC policy --- build/DockerfileForPlus | 10 +- build/DockerfileWithOpentracingForPlus | 10 +- .../DockerfileWithAppProtectForPlus | 8 +- cmd/nginx-ingress/main.go | 13 +- .../crds-v1beta1/k8s.nginx.org_policies.yaml | 18 ++ .../common/crds/k8s.nginx.org_policies.yaml | 18 ++ .../crds/k8s.nginx.org_policies.yaml | 18 ++ examples-of-custom-resources/oidc/README.md | 88 ++++++ .../oidc/client-secret.yaml | 7 + .../oidc/keycloak.yaml | 51 ++++ .../oidc/keycloak_setup.md | 31 ++ examples-of-custom-resources/oidc/oidc.yaml | 12 + .../oidc/tls-secret.yaml | 8 + .../oidc/virtual-server.yaml | 18 ++ examples-of-custom-resources/oidc/webapp.yaml | 32 ++ go.sum | 57 ---- internal/configs/config_params.go | 1 + internal/configs/configmaps.go | 1 + internal/configs/configurator.go | 32 +- internal/configs/ingress.go | 33 ++- internal/configs/ingress_test.go | 60 +++- internal/configs/oidc/oidc.conf | 96 ++++++ internal/configs/oidc/oidc_common.conf | 26 ++ internal/configs/oidc/openid_connect.js | 275 ++++++++++++++++++ internal/configs/version1/config.go | 1 + internal/configs/version1/nginx-plus.tmpl | 8 + internal/configs/version2/http.go | 22 +- .../version2/nginx-plus.virtualserver.tmpl | 22 ++ internal/configs/virtualserver.go | 189 +++++++++--- internal/configs/virtualserver_test.go | 127 ++++++-- internal/k8s/controller.go | 56 +++- internal/k8s/controller_test.go | 194 ++++++------ internal/k8s/secrets/store.go | 71 ++--- internal/k8s/secrets/store_test.go | 68 +++-- internal/k8s/secrets/validation.go | 40 ++- internal/k8s/secrets/validation_test.go | 89 +++++- internal/nginx/manager.go | 3 + pkg/apis/configuration/v1/types.go | 12 + .../configuration/v1/zz_generated.deepcopy.go | 21 ++ pkg/apis/configuration/validation/policy.go | 150 +++++++++- .../configuration/validation/policy_test.go | 239 ++++++++++++++- 41 files changed, 1858 insertions(+), 377 deletions(-) create mode 100644 examples-of-custom-resources/oidc/README.md create mode 100644 examples-of-custom-resources/oidc/client-secret.yaml create mode 100644 examples-of-custom-resources/oidc/keycloak.yaml create mode 100644 examples-of-custom-resources/oidc/keycloak_setup.md create mode 100644 examples-of-custom-resources/oidc/oidc.yaml create mode 100644 examples-of-custom-resources/oidc/tls-secret.yaml create mode 100644 examples-of-custom-resources/oidc/virtual-server.yaml create mode 100644 examples-of-custom-resources/oidc/webapp.yaml create mode 100644 internal/configs/oidc/oidc.conf create mode 100644 internal/configs/oidc/oidc_common.conf create mode 100644 internal/configs/oidc/openid_connect.js diff --git a/build/DockerfileForPlus b/build/DockerfileForPlus index a71c196c0e..8b82c3b0e1 100644 --- a/build/DockerfileForPlus +++ b/build/DockerfileForPlus @@ -6,6 +6,7 @@ FROM debian:buster-slim AS base LABEL maintainer="NGINX Docker Maintainers " ENV NGINX_PLUS_VERSION 23-1~buster +ENV NGINX_NJS_VERSION 23+0.5.0-1~buster ARG IC_VERSION # Download certificate and key from the customer portal (https://cs.nginx.com) @@ -36,7 +37,7 @@ RUN --mount=type=secret,id=nginx-repo.crt \ && echo "Acquire::https::plus-pkgs.nginx.com::SslKey \"/etc/ssl/nginx/nginx-repo.key\";" >> /etc/apt/apt.conf.d/90nginx \ && echo "Acquire::https::plus-pkgs.nginx.com::User-Agent \"k8s-ic-$IC_VERSION-apt\";" >> /etc/apt/apt.conf.d/90nginx \ && printf "deb https://plus-pkgs.nginx.com/debian buster nginx-plus\n" > /etc/apt/sources.list.d/nginx-plus.list \ - && apt-get update && apt-get install -y nginx-plus=${NGINX_PLUS_VERSION} \ + && apt-get update && apt-get install -y nginx-plus=${NGINX_PLUS_VERSION} nginx-plus-module-njs=${NGINX_NJS_VERSION} \ && setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx \ 'cap_net_bind_service=+ep' /usr/sbin/nginx-debug \ && apt-get remove --purge --auto-remove -y gnupg1 \ @@ -54,9 +55,10 @@ RUN ln -sf /proc/1/fd/1 /var/log/nginx/access.log \ RUN mkdir -p /var/lib/nginx \ /etc/nginx/secrets \ /etc/nginx/stream-conf.d \ + /etc/nginx/oidc \ && chown -R nginx:0 /etc/nginx \ - /var/cache/nginx \ - /var/lib/nginx/ \ + /var/cache/nginx \ + /var/lib/nginx/ \ && apt-get remove --purge -y libcap2-bin \ && rm /etc/nginx/conf.d/* @@ -67,6 +69,8 @@ COPY internal/configs/version1/nginx-plus.ingress.tmpl \ internal/configs/version2/nginx-plus.virtualserver.tmpl \ internal/configs/version2/nginx-plus.transportserver.tmpl / +COPY internal/configs/oidc/* /etc/nginx/oidc/ + # Uncomment the line below if you would like to add the default.pem to the image # and use it as a certificate and key for the default server # ADD default.pem /etc/nginx/secrets/default diff --git a/build/DockerfileWithOpentracingForPlus b/build/DockerfileWithOpentracingForPlus index 124a5aea9a..884aa0a6cf 100644 --- a/build/DockerfileWithOpentracingForPlus +++ b/build/DockerfileWithOpentracingForPlus @@ -14,6 +14,7 @@ FROM debian:buster-slim AS base LABEL maintainer="NGINX Docker Maintainers " ENV NGINX_PLUS_VERSION 23-1~buster +ENV NGINX_NJS_VERSION 23+0.5.0-1~buster ENV NGINX_OPENTRACING_MODULE_VERSION 23+0.9.0-1~buster ARG IC_VERSION @@ -47,7 +48,7 @@ RUN --mount=type=secret,id=nginx-repo.crt \ && echo "Acquire::https::plus-pkgs.nginx.com::User-Agent \"k8s-ic-$IC_VERSION-apt\";" >> /etc/apt/apt.conf.d/90nginx \ && printf "deb https://plus-pkgs.nginx.com/debian buster nginx-plus\n" > /etc/apt/sources.list.d/nginx-plus.list \ && apt-get update && apt-get install -y \ - nginx-plus=${NGINX_PLUS_VERSION} \ + nginx-plus=${NGINX_PLUS_VERSION} nginx-plus-module-njs=${NGINX_NJS_VERSION} \ # Install OpenTracing module nginx-plus-module-opentracing=${NGINX_OPENTRACING_MODULE_VERSION} \ && setcap 'cap_net_bind_service=+ep' /usr/sbin/nginx \ @@ -70,9 +71,10 @@ COPY --from=tracer-downloader /usr/local/lib/libjaegertracing_plugin.so /usr/loc RUN mkdir -p /var/lib/nginx \ /etc/nginx/secrets \ /etc/nginx/stream-conf.d \ + /etc/nginx/oidc \ && chown -R nginx:0 /etc/nginx \ - /var/cache/nginx \ - /var/lib/nginx/ \ + /var/cache/nginx \ + /var/lib/nginx/ \ && apt-get remove --purge -y libcap2-bin \ && rm /etc/nginx/conf.d/* @@ -83,6 +85,8 @@ COPY internal/configs/version1/nginx-plus.ingress.tmpl \ internal/configs/version2/nginx-plus.virtualserver.tmpl \ internal/configs/version2/nginx-plus.transportserver.tmpl / +COPY internal/configs/oidc/* /etc/nginx/oidc/ + # Uncomment the line below if you would like to add the default.pem to the image # and use it as a certificate and key for the default server # ADD default.pem /etc/nginx/secrets/default diff --git a/build/appprotect/DockerfileWithAppProtectForPlus b/build/appprotect/DockerfileWithAppProtectForPlus index b054f35ce4..fdc2f2c5d8 100644 --- a/build/appprotect/DockerfileWithAppProtectForPlus +++ b/build/appprotect/DockerfileWithAppProtectForPlus @@ -13,6 +13,7 @@ ENV APPPROTECT_SIG_VERSION 2020.12.30-1~buster ENV APPPROTECT_THREAT_CAMPAIGNS_VERSION 2021.01.03-1~buster ENV NGINX_PLUS_VERSION 23-1~buster ENV NGINX_PLUS_RELEASE R23 +ENV NGINX_NJS_VERSION 23+0.5.0-1~buster ARG IC_VERSION # Download certificate and key from the customer portal (https://cs.nginx.com) @@ -49,7 +50,7 @@ RUN --mount=type=secret,id=nginx-repo.crt \ && echo "Acquire::https::app-protect-sigs.nginx.com::Verify-Host \"true\";" >> /etc/apt/apt.conf.d/90app-protect-sigs \ && echo "Acquire::https::app-protect-sigs.nginx.com::SslCert \"/etc/ssl/nginx/nginx-repo.crt\";" >> /etc/apt/apt.conf.d/90app-protect-sigs \ && echo "Acquire::https::app-protect-sigs.nginx.com::SslKey \"/etc/ssl/nginx/nginx-repo.key\";" >> /etc/apt/apt.conf.d/90app-protect-sigs \ - && apt-get update && apt-get install -y nginx-plus=$NGINX_PLUS_VERSION \ + && apt-get update && apt-get install -y nginx-plus=$NGINX_PLUS_VERSION nginx-plus-module-njs=${NGINX_NJS_VERSION} \ nginx-plus-module-appprotect=$APPPROTECT_MODULE_VERSION \ app-protect-plugin=$APPPROTECT_PLUGIN_VERSION \ app-protect-engine=$APPPROTECT_ENGINE_VERSION \ @@ -83,6 +84,7 @@ RUN mkdir -p /var/lib/nginx \ /etc/nginx/waf/nac-usersigs \ /var/log/app_protect \ /opt/app_protect \ + /etc/nginx/oidc \ && touch /etc/nginx/waf/nac-usersigs/index.conf \ && chown -R nginx:0 /etc/app_protect \ /usr/share/ts \ @@ -106,7 +108,7 @@ RUN printf "MODULE = ALL;\nLOG_LEVEL = TS_CRIT;\nFILE = 2;\n" > /etc/app_protect ; do sed -i "/\[$v/a log_level=fatal" "/etc/app_protect/tools/asm_logging.conf" \ ; done -COPY --chown=nginx:0 build/appprotect/log-default.json /etc/nginx +COPY --chown=nginx:0 build/appprotect/log-default.json /etc/nginx EXPOSE 80 443 @@ -115,6 +117,8 @@ COPY internal/configs/version1/nginx-plus.ingress.tmpl \ internal/configs/version2/nginx-plus.virtualserver.tmpl \ internal/configs/version2/nginx-plus.transportserver.tmpl / +COPY internal/configs/oidc/* /etc/nginx/oidc/ + # Uncomment the line below if you would like to add the default.pem to the image # and use it as a certificate and key for the default server # ADD default.pem /etc/nginx/secrets/default diff --git a/cmd/nginx-ingress/main.go b/cmd/nginx-ingress/main.go index ff524fe519..d7e3f36254 100644 --- a/cmd/nginx-ingress/main.go +++ b/cmd/nginx-ingress/main.go @@ -80,7 +80,7 @@ var ( - i.e have the annotation "kubernetes.io/ingress.class" equal to the class. Additionally, the Ingress Controller processes resources that do not have the class set, which can be disabled by setting the "-use-ingress-class-only" flag - + The Ingress Controller processes all the VirtualServer/VirtualServerRoute resources that do not have the "ingressClassName" field for all versions of kubernetes.`) useIngressClassOnly = flag.Bool("use-ingress-class-only", false, @@ -112,7 +112,7 @@ var ( (default for NGINX "nginx.transportserver.tmpl"; default for NGINX Plus "nginx-plus.transportserver.tmpl")`) externalService = flag.String("external-service", "", - `Specifies the name of the service with the type LoadBalancer through which the Ingress controller pods are exposed externally. + `Specifies the name of the service with the type LoadBalancer through which the Ingress controller pods are exposed externally. The external address of the service is used when reporting the status of Ingress, VirtualServer and VirtualServerRoute resources. For Ingress resources only: Requires -report-ingress-status.`) nginxCisConnector = flag.String("nginx-cis-connector", "", @@ -165,7 +165,7 @@ var ( globalConfiguration = flag.String("global-configuration", "", `A GlobalConfiguration resource for global configuration of the Ingress Controller. Requires -enable-custom-resources. If the flag is set, - but the Ingress controller is not able to fetch the corresponding resource from Kubernetes API, the Ingress Controller + but the Ingress controller is not able to fetch the corresponding resource from Kubernetes API, the Ingress Controller will fail to start. Format: /`) enableTLSPassthrough = flag.Bool("enable-tls-passthrough", false, @@ -521,6 +521,7 @@ func main() { NginxServiceMesh: *spireAgentAddress != "", MainAppProtectLoadModule: *appProtect, EnableLatencyMetrics: *enableLatencyMetrics, + EnablePreviewPolicies: *enablePreviewPolicies, } ngxConfig := configs.GenerateNginxMainConfig(staticCfgParams, cfgParams) @@ -790,8 +791,10 @@ func getAndValidateSecret(kubeClient *kubernetes.Clientset, secretNsName string) return secret, nil } -const locationFmt = `/[^\s{};]*` -const locationErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`" +const ( + locationFmt = `/[^\s{};]*` + locationErrMsg = "must start with / and must not include any whitespace character, `{`, `}` or `;`" +) var locationRegexp = regexp.MustCompile("^" + locationFmt + "$") diff --git a/deployments/common/crds-v1beta1/k8s.nginx.org_policies.yaml b/deployments/common/crds-v1beta1/k8s.nginx.org_policies.yaml index 5032e2b7c1..4409afcf79 100644 --- a/deployments/common/crds-v1beta1/k8s.nginx.org_policies.yaml +++ b/deployments/common/crds-v1beta1/k8s.nginx.org_policies.yaml @@ -87,6 +87,24 @@ spec: type: string token: type: string + oidc: + description: OIDC defines an Open ID Connect policy. + type: object + properties: + authEndpoint: + type: string + clientID: + type: string + clientSecret: + type: string + jwksURI: + type: string + redirectURI: + type: string + scope: + type: string + tokenEndpoint: + type: string rateLimit: description: 'RateLimit defines a rate limit policy. policy status: preview' type: object diff --git a/deployments/common/crds/k8s.nginx.org_policies.yaml b/deployments/common/crds/k8s.nginx.org_policies.yaml index abb501cd93..c4125a1a02 100644 --- a/deployments/common/crds/k8s.nginx.org_policies.yaml +++ b/deployments/common/crds/k8s.nginx.org_policies.yaml @@ -88,6 +88,24 @@ spec: type: string token: type: string + oidc: + description: OIDC defines an Open ID Connect policy. + type: object + properties: + authEndpoint: + type: string + clientID: + type: string + clientSecret: + type: string + jwksURI: + type: string + redirectURI: + type: string + scope: + type: string + tokenEndpoint: + type: string rateLimit: description: 'RateLimit defines a rate limit policy. policy status: preview' type: object diff --git a/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml b/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml index 5032e2b7c1..4409afcf79 100644 --- a/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml +++ b/deployments/helm-chart/crds/k8s.nginx.org_policies.yaml @@ -87,6 +87,24 @@ spec: type: string token: type: string + oidc: + description: OIDC defines an Open ID Connect policy. + type: object + properties: + authEndpoint: + type: string + clientID: + type: string + clientSecret: + type: string + jwksURI: + type: string + redirectURI: + type: string + scope: + type: string + tokenEndpoint: + type: string rateLimit: description: 'RateLimit defines a rate limit policy. policy status: preview' type: object diff --git a/examples-of-custom-resources/oidc/README.md b/examples-of-custom-resources/oidc/README.md new file mode 100644 index 0000000000..df3355c2c6 --- /dev/null +++ b/examples-of-custom-resources/oidc/README.md @@ -0,0 +1,88 @@ +# OIDC + +In this example, we deploy keycloak and a web application configure load balancing for it via a VirtualServer, and apply an OpenID Connect policy. + +## Prerequisites + +1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/) instructions to deploy the Ingress Controller. +1. Save the public IP address of the Ingress Controller into a shell variable: + ``` + $ IC_IP=XXX.YYY.ZZZ.III + ``` +1. Save the HTTP port of the Ingress Controller into a shell variable: + ``` + $ IC_HTTPS_PORT= + ``` + +## Step 1 - Deploy a Web Application + +Create the application deployment and service: +``` +$ kubectl apply -f webapp.yaml +``` + +## Step 2 - Deploy Keycloak + +Create keycloak deployment and service: +``` +$ kubectl apply -f keycloak.yaml +``` + +To set up Keycloak, you can either follow the steps in the "Configuring Keycloak" section of the documentation [here](https://docs.nginx.com/nginx/deployment-guides/single-sign-on/keycloak/#configuring-keycloak) or execute the commands [here](./keycloak_setup.md). + + +## Step 3 - Deploy the Client Secret + +1. Edit `client-secret.yaml` with your secret. + +1. Create a secret with the name `oidc-secret` that will be used for OIDC validation: +``` +$ kubectl apply -f client-secret.yaml +``` + +## Step 4 - Deploy the OIDC Policy + +1. Modify the URL `authEndpoint` in `oidc.yaml` with the public IP address or DNS of keycloak. + +1. Create a policy with the name `oidc-policy` that references the secret from the previous step: +``` +$ kubectl apply -f oidc.yaml +``` + +## Step 5 - Deploy the Service for the Ingress Controller and update ConfigMap +1. Deploy the service. + ``` + $ kubectl apply -f service/nodeport.yaml + ``` +1. Update the ConfigMap with the config required for OIDC. + ``` + data: + stream-snippets: | + resolver 10.96.0.10 valid=5s; + server { + listen 12345; + zone_sync; + zone_sync_server nginx-ic-svc.nginx-ingress.svc.cluster.local:12345 resolve; + } + ``` +1. Apply the ConfigMap. + ``` + $ kubectl apply -f common/nginx-config.yaml + ``` + +## Step 6 - Configure Load Balancing and TLS Termination +1. Create the secret with the TLS certificate and key: + ``` + $ kubectl create -f tls-secret.yaml + ``` + +2. Create a VirtualServer resource for the web application: + ``` + $ kubectl apply -f virtual-server.yaml + ``` + +Note that the VirtualServer references the policy `oidc-policy` created in Step 4. + +## Step 5 - Test the Configuration + +Open a web browser and navigate to the URL of the Ingress Controller diff --git a/examples-of-custom-resources/oidc/client-secret.yaml b/examples-of-custom-resources/oidc/client-secret.yaml new file mode 100644 index 0000000000..3fc8f1f3f7 --- /dev/null +++ b/examples-of-custom-resources/oidc/client-secret.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Secret +metadata: + name: oidc-secret +type: nginx.org/oidc +data: + client-secret: c3VwZXItc2VjcmV0IQo diff --git a/examples-of-custom-resources/oidc/keycloak.yaml b/examples-of-custom-resources/oidc/keycloak.yaml new file mode 100644 index 0000000000..a0155dd028 --- /dev/null +++ b/examples-of-custom-resources/oidc/keycloak.yaml @@ -0,0 +1,51 @@ +apiVersion: v1 +kind: Service +metadata: + name: keycloak + labels: + app: keycloak +spec: + ports: + - name: http + port: 8080 + targetPort: 8080 + selector: + app: keycloak + type: LoadBalancer +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: keycloak + namespace: default + labels: + app: keycloak +spec: + replicas: 1 + selector: + matchLabels: + app: keycloak + template: + metadata: + labels: + app: keycloak + spec: + containers: + - name: keycloak + image: quay.io/keycloak/keycloak:12.0.1 + env: + - name: KEYCLOAK_USER + value: "admin" + - name: KEYCLOAK_PASSWORD + value: "admin" + - name: PROXY_ADDRESS_FORWARDING + value: "true" + ports: + - name: http + containerPort: 8080 + - name: https + containerPort: 8443 + readinessProbe: + httpGet: + path: /auth/realms/master + port: 8080 diff --git a/examples-of-custom-resources/oidc/keycloak_setup.md b/examples-of-custom-resources/oidc/keycloak_setup.md new file mode 100644 index 0000000000..cf653a39c0 --- /dev/null +++ b/examples-of-custom-resources/oidc/keycloak_setup.md @@ -0,0 +1,31 @@ +# Keycloak Setup + +This guide will help you create a `client` with name `nginx-plus`, and a user `nginx-user` with password `test` using Keycloak's API. + +If you changed username and password for keycloak in `keycloak.yaml`, modify the commands accordingly. + +1. Save the address of Keycloak into a shell variable: + ``` + $ KEYCLOAK_ADDRESS=localhost:8080 + ``` +1. Retrieve the access token and store into a shell variable: + ``` + $ TOKEN=`curl -sS --data "username=admin&password=admin&grant_type=password&client_id=admin-cli" http://${KEYCLOAK_ADDRESS}/auth/realms/master/protocol/openid-connect/token | jq -r .access_token` + ``` +***Note***: The access token lifespan is very short. If it expires between commands, retrieve it again with the command above. +1. Create the user `nginx-user` + ``` + $ curl -sS -X POST -d '{ "username": "nginx-user", "enabled": true, "credentials":[{"type": "password", "value": "test", "temporary": false}]}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" http://${KEYCLOAK_ADDRESS}/auth/admin/realms/master/users + ``` +1. Create the Client `nginx-plus` and retrieve the secret: + ``` + $ SECRET=`curl -sS -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"] }' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" http://${KEYCLOAK_ADDRESS}/auth/realms/master/clients-registrations/default | jq -r .secret` + ``` + If everything went well you should have the secret stored in $SECRET, to double check run: + ``` + $ echo $SECRET + ``` + Now we can encode the secret and copy/paste it in the field `client-secret` inside `client-secret.yaml`: + ``` + $ echo -n $SECRET | base64 + ``` diff --git a/examples-of-custom-resources/oidc/oidc.yaml b/examples-of-custom-resources/oidc/oidc.yaml new file mode 100644 index 0000000000..71fbf590ef --- /dev/null +++ b/examples-of-custom-resources/oidc/oidc.yaml @@ -0,0 +1,12 @@ +apiVersion: k8s.nginx.org/v1 +kind: Policy +metadata: + name: oidc-policy +spec: + oidc: + clientID: nginx-plus + clientSecret: oidc-secret + authEndpoint: http://idp.example.com:8080/auth/realms/master/protocol/openid-connect/auth + tokenEndpoint: http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/token + jwksURI: http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/certs + scope: openid+profile+email diff --git a/examples-of-custom-resources/oidc/tls-secret.yaml b/examples-of-custom-resources/oidc/tls-secret.yaml new file mode 100644 index 0000000000..fe58746ab8 --- /dev/null +++ b/examples-of-custom-resources/oidc/tls-secret.yaml @@ -0,0 +1,8 @@ +apiVersion: v1 +kind: Secret +metadata: + name: tls-secret +type: kubernetes.io/tls +data: + tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURHekNDQWdPZ0F3SUJBZ0lVWU90ZXQ1cnpjd2pFMlo1QUQzQS9tdVJFVzRRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0hURWJNQmtHQTFVRUF3d1NkMlZpWVhCd0xtVjRZVzF3YkdVdVkyOXRNQjRYRFRJd01Ea3lPVEl5TVRrMQpPVm9YRFRNd01Ea3lOekl5TVRrMU9Wb3dIVEViTUJrR0ExVUVBd3dTZDJWaVlYQndMbVY0WVcxd2JHVXVZMjl0Ck1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBeU5odlg5emoraGZvN0V2TzBsNlkKNzNUTGdMZEUzWWRGcURRT1RrL3JuajhzeWRPSUdrQ0IxR0VDcHAxcmc3bk9wTWVUNHovRlNsbzQ1TzJKWWRUNQplMC9YVWRkUk9JYytoS0V3NzNoejZjc2Fjd25NdjZzZWE2UDBSY1ZDQU1pdkZoeG9sWGRDUnlsZVdrVXczd29KClFRZFozNEZCQlFoVjFvMThHeGViWWFCTjF3bjMxdXZ4ZUxqMjVyQThKOUZjWElTQzJvMGpIZmZkSTFJamJjUHUKVlhJZkh0NFVMcHpnZlpQUVVnYzUrL3BkYVVRL0JHdkdiQ3o0cnBVbzhCQnQ2N0U5RlVoVE8vYnZnU1ljQ1A4dQpvTU9uY2hyNjZMSzJ3WE82ZWQyVHR4VmQySGJZRW5TRjFFZGRqZDdRQjJMUVoxUDJBbERBZll3YmZ3R3VMVGNhCjh3SURBUUFCbzFNd1VUQWRCZ05WSFE0RUZnUVVLOXlpL0tRQXp6SjRCcHdESndGTWF1cDJCREF3SHdZRFZSMGoKQkJnd0ZvQVVLOXlpL0tRQXp6SjRCcHdESndGTWF1cDJCREF3RHdZRFZSMFRBUUgvQkFVd0F3RUIvekFOQmdrcQpoa2lHOXcwQkFRc0ZBQU9DQVFFQWVuSFIybDJVNWdNQTMyek9YY2dDeTE0RXU3S2p3ayswRzFWbkNxU1IrMmhyCjdUSHVBVDRUSGpGYzFDWDQ4YXBiLzdwV3lJVlZoZ2IybXFMSDZTZlRWOEJtMEdLZUkyUU9VSVpZb2ZJeHZMeEMKRjRzd3FLVFc1SmgyUWJ2M3owN2hvTmpSZmR0WG1GS0pUWUZzS05WN3oyMVo4WWlFQ1o4NVMwRHpoU3BaSnk3MwpkdEV5NXlsVUZvb0JyeklzajFxRHlVZ2Zlck84TkZ3b2RlNDg4QThNMVNQNWZOOTBmSHZHRU9qRzdubG1IZDJJCkdvYkd2Z0kvT3l3RWVPMVFzdEFWckVtZVRvU0ZudStxZ09iUEpxRVFETExRS21SNkRNNzI5bkw4RXRIekZwTlkKZFcxeThRdkZxVm0yN1ZVdk8ybEduM0JUUmt0dWxSckJ3Sm9hQmF3TSt3PT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo= + tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2UUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktjd2dnU2pBZ0VBQW9JQkFRREkyRzlmM09QNkYranMKUzg3U1hwanZkTXVBdDBUZGgwV29OQTVPVCt1ZVB5ekowNGdhUUlIVVlRS21uV3VEdWM2a3g1UGpQOFZLV2pqawo3WWxoMVBsN1Q5ZFIxMUU0aHo2RW9URHZlSFBweXhwekNjeS9xeDVyby9SRnhVSUF5SzhXSEdpVmQwSkhLVjVhClJURGZDZ2xCQjFuZmdVRUZDRlhXalh3YkY1dGhvRTNYQ2ZmVzYvRjR1UGJtc0R3bjBWeGNoSUxhalNNZDk5MGoKVWlOdHcrNVZjaDhlM2hRdW5PQjlrOUJTQnpuNytsMXBSRDhFYThac0xQaXVsU2p3RUczcnNUMFZTRk03OXUrQgpKaHdJL3k2Z3c2ZHlHdnJvc3JiQmM3cDUzWk8zRlYzWWR0Z1NkSVhVUjEyTjN0QUhZdEJuVS9ZQ1VNQjlqQnQvCkFhNHROeHJ6QWdNQkFBRUNnZ0VBZTJ5V05PRDNzRzhWRW5FYnJpZTM4QjlrRjd1SU5HSzJxY0Vqc1hobm9SM04KbGxISjUrZ1FZTVVrN2VMN2VUMnNBWk1zREpEWjJ2RksyVlFvQXRqd1g1a1hCeEk4dFhKWE53WWZubW4xUVkwdwp1ZFVoMy85MmVFdVBCM2xMTUZRalZJRXN1LzFIMjVkT2hrYlMyNTI5UmhzUVhjdCtlMnM5NU5XWm1NU1BGaFJuCnJMeFFpSzFiTWJxcHVHRlUzSXYwd3ZVd2dQdjFUQkhYSzAzOFZ0WGtMLzFpVVNLWVBJOHQ3QmUrUTI0cklsVXkKK01BQ3pPdWpabWduWXFpYUJ5ZFNJeDJRM3VtUTFyL2hOMG5FbkNMdnNRSVZiVlhpTFZvQkpzenJCMEgyREI2cQphR096WHlGeG1HQ2JkbWpCaTlwb1FybEtmR2JwdWpIYnJmNmU0MWRZVVFLQmdRRHlueXBxdmFZaExSTXMwZTVFClhHcStIZ2lLZnlWbnNYd2RMUWF3ajRHTkF1WEtpaXBvNU5nb3IyNFN4NGIzejNuZ1QxalprbE40VlpxcUNFQmYKMno4S2o4VWREVS9BRzZwaDhITSszUEtUWXNOcElWdi9SbSs4cktPbU9uSGZJaWpGNDdvU0dyT3RVWnVyY2xXZQpjMkZuaHhiTHdISnZHYzd2VDV6VDlNTFMvd0tCZ1FEVDY0N0Q1ZFpDTllQcytLNU9Ub1JFNFJIdlpMSUdUWHRwClZEa2Vjdis3aUxoNis3aTFHRjZuMTZ2cEJrSXRmOXUvRk9SRVpoM1lpQk5Fcy93bGl6NHRVNDhrbDQrRVNjYlcKdUh2ZVY1NktBUHI2UGM3ajBJK1lMSXdoTURqMXVPSnV2eW9iSFJZQlJPajVGd3YvUk81ME9LdytLdXpwV2s4QQoxY1FYakVHY0RRS0JnRTRUam5EZkt2RU9NbGVBRHk4TWxvVXI0US9BcnViWnBOazJ2aXBmWkE5ZTJWZitjbnRpCitYVE9UNXZYZmNXTmpPajBYK0ZVUjJ3NEVCZWJwQ3Uwd0dyRHJXa1YrWTRXMlJPL2J6YlJuM1p5bC9QaStsb0IKN3I5R3h6c2RIN3Z3b0RKZWdHaUhFejg1UGVGRVgrMG5zRGJDc0VGTll3WUJ4aWdZOUp6NDdTRTlBb0dCQUozZgpzM2kvSlpJbmVnTzA4MjNFMG9iWndWbTlnMTVzcEk3QVB0a3ZSTks1dE8xeHo1V2g5UXBIQW52VHZNTldxQ2MrCjhocitsQ2Qyb0J3amxhbUdoU2lSUW1jNVBhS0lyOGZRb2Y3dStWM0lBekVma0p4cENFQ09sMG8yT1lqZFZscTQKc1M2SHlaZmlkVWp6NFcwbk5obUJDdGc1ZEVzWGl4bU5Kc3VBSW5TVkFvR0FIY0J0UUZ2QW9vUTNjbDdMME5GVAo4c0cvR04wbnRGdXJveE5oTlhaS3hYMGYrUXBWcHJFaTlybU13WXE1WXc3Z2lqZWdNNURUbUdFcHIvc1FNZmUyCjZYbjU5NmlLSFZ4NCtBZE90dnc5NnhlTm56S2syOVdrTENzbG4vZ2lETWJCcHowNFhDZ1h3TitEZXg3emVHSGkKNXFpWlpJSlNlS0gvR3BWcno5MEE3eU09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K diff --git a/examples-of-custom-resources/oidc/virtual-server.yaml b/examples-of-custom-resources/oidc/virtual-server.yaml new file mode 100644 index 0000000000..dc913b1239 --- /dev/null +++ b/examples-of-custom-resources/oidc/virtual-server.yaml @@ -0,0 +1,18 @@ +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: webapp +spec: + host: webapp.example.com + tls: + secret: tls-secret + upstreams: + - name: webapp + service: webapp-svc + port: 80 + routes: + - path: / + policies: + - name: oidc-policy + action: + pass: webapp diff --git a/examples-of-custom-resources/oidc/webapp.yaml b/examples-of-custom-resources/oidc/webapp.yaml new file mode 100644 index 0000000000..31fde92a6e --- /dev/null +++ b/examples-of-custom-resources/oidc/webapp.yaml @@ -0,0 +1,32 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: webapp +spec: + replicas: 1 + selector: + matchLabels: + app: webapp + template: + metadata: + labels: + app: webapp + spec: + containers: + - name: webapp + image: nginxdemos/nginx-hello:plain-text + ports: + - containerPort: 8080 +--- +apiVersion: v1 +kind: Service +metadata: + name: webapp-svc +spec: + ports: + - port: 80 + targetPort: 8080 + protocol: TCP + name: http + selector: + app: webapp diff --git a/go.sum b/go.sum index 903d349b6c..40eecc5a15 100644 --- a/go.sum +++ b/go.sum @@ -390,10 +390,8 @@ github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czP github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190312143242-1de009706dbe/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63 h1:nTT4s92Dgz2HlrB2NaMgvlfqHH39OgMhA7z3PK7PGD4= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= -github.com/mailru/easyjson v0.7.0 h1:aizVhC/NAAcKWb+5QsU1iNOZb4Yws5UO2I+aIprQITM= github.com/mailru/easyjson v0.7.0/go.mod h1:KAzv3t3aY1NaHWoQz1+4F1ccyAH66Jk7yos7ldAVICs= github.com/mailru/easyjson v0.7.6 h1:8yTIVnZgCoiM1TgqoeTl+LfU5Jg6/xL3QhGQnimLYnA= github.com/mailru/easyjson v0.7.6/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= @@ -419,7 +417,6 @@ github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= @@ -435,7 +432,6 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= -github.com/nginxinc/nginx-plus-go-client v0.6.0 h1:lrGlDhZuf5ZL987AG1E8T/Jpz514QmYNLdAxZWWnBsI= github.com/nginxinc/nginx-plus-go-client v0.6.0/go.mod h1:DBAmdDP71tOhgFPdCMVusegzdKmLVpVL0nVcMX17pbY= github.com/nginxinc/nginx-plus-go-client v0.8.0 h1:J/HcWx9MxBLg0Yd9YdrXNtmOc+aVnV9Z5jAb2HmYoWg= github.com/nginxinc/nginx-plus-go-client v0.8.0/go.mod h1:DBAmdDP71tOhgFPdCMVusegzdKmLVpVL0nVcMX17pbY= @@ -454,7 +450,6 @@ github.com/onsi/ginkgo v1.11.0 h1:JAKSXpt1YjtLA7YpPiqO9ss6sNXEsPfSGdwN0UHqzrw= github.com/onsi/ginkgo v1.11.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA= github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/onsi/gomega v1.7.0 h1:XPnZz8VVBHjVsy1vzJmRwIcSwiUO+JFfrv/xGiigmME= github.com/onsi/gomega v1.7.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= github.com/onsi/gomega v1.8.1 h1:C5Dqfs/LeauYDX0jJXIe2SWmwCbGzx9yF8C8xy3Lh34= github.com/onsi/gomega v1.8.1/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= @@ -472,12 +467,10 @@ github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FI github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= -github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -489,19 +482,14 @@ github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prY github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_golang v1.3.0 h1:miYCvYqFXtl/J9FIy8eNpBfYthAEFg+Ys0XyUVEcDsc= github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= -github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A= github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4= -github.com/prometheus/client_golang v1.7.1 h1:NTGy1Ja9pByO+xAeH/qiWnLrKtr3hJPNjaVUwnjpdpA= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.9.0 h1:Rrch9mh17XcxvEu9D9DEpb4isxjGBtcevQjKvxPRQIU= github.com/prometheus/client_golang v1.9.0/go.mod h1:FqZLKOZnGdFAhOK4nqGHa7D66IdsO+O441Eve7ptJDU= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= @@ -511,11 +499,8 @@ github.com/prometheus/common v0.0.0-20181113130724-41aa239b4cce/go.mod h1:daVV7q github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/common v0.7.0 h1:L+1lyG48J1zAQXA3RBX/nG/B3gjlHq0zTt2tlbJLyCY= github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= -github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= -github.com/prometheus/common v0.10.0 h1:RyRA7RzGXQZiW+tGMr7sxa85G1z0yOpM1qq5c8lNawc= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= github.com/prometheus/common v0.15.0 h1:4fgOnadei3EZvgRwxJ7RMpG1k1pOZth5Pc13tyspaKM= github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= @@ -523,11 +508,8 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/prometheus/procfs v0.0.8 h1:+fpWZdT24pJBiqJdAwYBjPSk+5YmQzYNPYzQsdzLkt8= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= -github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI= github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= -github.com/prometheus/procfs v0.1.3 h1:F0+tqvhOksq22sc6iCHF5WGlWjdwj92p0udFh1VFBS8= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.2.0 h1:wH4vA7pcjKuZzjF7lM8awk4fnuJO6idemZXoKnULUx4= github.com/prometheus/procfs v0.2.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= @@ -558,7 +540,6 @@ github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tL github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= -github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff h1:VARhShG49tiji6mdRNp7JTNDtJ0FhuprF93GBQ37xGU= github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= @@ -575,9 +556,7 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= @@ -617,7 +596,6 @@ golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190211182817-74369b46fc67/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190320223903-b7391e95e576/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= @@ -627,7 +605,6 @@ golang.org/x/crypto v0.0.0-20190617133340-57b3e21c3d56/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200220183623-bac4c82f6975/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0 h1:hb9wdF1z5waM+dSIICn1l0DkLVDT3hqhhQsDNUmHPRE= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -659,9 +636,7 @@ golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKG golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0 h1:KU7oHjnv3XNWfa5COkzUifxZmxp1TyI7ImMXqFxLwvQ= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0 h1:RM4zey1++hCTbCVQfnWeKs9/IEsaBLA8vTkd0WVtmH4= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0 h1:8pl+sMODzuvGJkmj2W4kZihvVb5mKm8pB/X44PIQHv8= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -677,7 +652,6 @@ golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190320064053-1272bf9dcd53/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -698,24 +672,19 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344 h1:vGXIOMxbNfDTk/aXCmfdLgkrSV+Z2tcbze+pEc3v5W4= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b h1:uwuIcX0g4Yl1NC5XAz37xsr2lTtcqevgzYNVt49waME= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201216054612-986b41b23924 h1:QsnDpLLOKwHBBDa8nDws4DYNc/ryVW2vCpxCs09d4PY= golang.org/x/net v0.0.0-20201216054612-986b41b23924/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6 h1:pE8b58s1HRDMi8RDc79m0HISf9D4TzseP40cEA6IGfs= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d h1:TzXSXBo42m9gQenoE3b9BGiEpg5IG2JkU5FkPIawgtw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -732,7 +701,6 @@ golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190209173611-3b5209105503/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -759,11 +727,8 @@ golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1 h1:ogLJMz+qpzav7lGMh10LMvAkM/fAoGlaiiHYiFYdm80= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -775,19 +740,15 @@ golang.org/x/sys v0.0.0-20201218084310-7d0127a74742/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.4 h1:0YWbFKbhXG/wIiuHDSKpS0Iy7FSA+u45VtBMfQcFTTc= golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e h1:EHBhcS0mlXEAVwNyO2dLfjToGsyY4j24pTs2ScHnX7s= golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -834,15 +795,12 @@ golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= golang.org/x/tools v0.0.0-20200505023115-26f46d2f7ef8/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616133436-c1934b75d054 h1:HHeAlu5H9b71C+Fx0K+1dGgVFN1DM1/wz4aoGOA5qS8= golang.org/x/tools v0.0.0-20200616133436-c1934b75d054/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200616195046-dc31b401abb5 h1:UaoXseXAWUJUcuJ2E2oczJdLxAJXL0lOmVaBl7kuk+I= golang.org/x/tools v0.0.0-20200616195046-dc31b401abb5/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2 h1:lHDhNNs7asPT3p01mm8EP3B+bNyyVfg0bcYjhJUYgxw= golang.org/x/tools v0.0.0-20201218024724-ae774e9781d2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -866,7 +824,6 @@ google.golang.org/appengine v1.6.5 h1:tycE03LOZYQNhDpS27tcQdAzLCVMaj7QT2SXxebnpC google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7 h1:ZUjXAXmrAyrmmCPHgCA/vChHcpsX27MZ3yBonD/z1KE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -892,13 +849,11 @@ google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLD google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.22.0 h1:J0UbZOIrCAl+fpTOf8YLs4dJo8L/owV4LYVtAXQoPkw= google.golang.org/grpc v1.22.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -906,21 +861,16 @@ google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLY google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0 h1:UhZDfRO8JRQru4/+LlLE0BRKGF8L+PICnvYZmx/fEGA= google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -939,17 +889,13 @@ gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWD gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966 h1:B0J02caTR6tpSJozBJyiAzT6CtBzjclw4pgm9gg8Ys0= gopkg.in/yaml.v3 v3.0.0-20190905181640-827449938966/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776 h1:tQIYjPdBoyREyB9XMu+nnTclpTYkz2zFM+lzLJFO4gQ= @@ -981,7 +927,6 @@ k8s.io/component-base v0.18.2/go.mod h1:kqLlMuhJNHQ9lz8Z7V5bxUUtjFZnrypArGl58gmD k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200114144118-36b2048a9120/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= k8s.io/gengo v0.0.0-20200413195148-3a45101e95ac/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0= -k8s.io/gengo v0.0.0-20201113003025-83324d819ded h1:JApXBKYyB7l9xx+DK7/+mFjC7A9Bt5A93FPvFD0HIFE= k8s.io/gengo v0.0.0-20201113003025-83324d819ded/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027 h1:Uusb3oh8XcdzDF/ndlI4ToKTYVlkCSJP39SRY2mfRAw= k8s.io/gengo v0.0.0-20201214224949-b6c5ce23f027/go.mod h1:FiNAH4ZV3gBg2Kwh89tzAEV2be7d5xI0vBa/VySYy3E= @@ -990,7 +935,6 @@ k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk= k8s.io/klog v1.0.0 h1:Pt+yjF5aB1xDSVbau4VsWe+dQNzA0qv1LlXdC2dF6Q8= k8s.io/klog v1.0.0/go.mod h1:4Bi6QPql/J/LkTDqv7R/cd3hPo4k2DG6Ptcz060Ez5I= k8s.io/klog/v2 v2.0.0/go.mod h1:PBfzABfn139FHAV07az/IF9Wp1bkk3vpT2XSJ76fSDE= -k8s.io/klog/v2 v2.2.0 h1:XRvcwJozkgZ1UQJmfMGpvRthQHOvihEhYtDfAaxMz/A= k8s.io/klog/v2 v2.2.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= k8s.io/klog/v2 v2.4.0 h1:7+X0fUguPyrKEC4WjH8iGDg3laWgMo5tMnRTIGTTxGQ= k8s.io/klog/v2 v2.4.0/go.mod h1:Od+F08eJP+W3HUb4pSrPpgp9DGU4GzlpG/TmITuYh/Y= @@ -1010,7 +954,6 @@ sigs.k8s.io/structured-merge-diff/v3 v3.0.0-20200116222232-67a7b8c61874/go.mod h sigs.k8s.io/structured-merge-diff/v3 v3.0.0/go.mod h1:PlARxl6Hbt/+BC80dRLi1qAmnMqwqDg62YvvVkZjemw= sigs.k8s.io/structured-merge-diff/v4 v4.0.2 h1:YHQV7Dajm86OuqnIR6zAelnDWBRjo+YhYV9PmGrh1s8= sigs.k8s.io/structured-merge-diff/v4 v4.0.2/go.mod h1:bJZC9H9iH24zzfZ/41RGcq60oK1F7G282QMXDPYydCw= -sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs= sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= diff --git a/internal/configs/config_params.go b/internal/configs/config_params.go index 14d65f2f90..0cd07ec1ae 100644 --- a/internal/configs/config_params.go +++ b/internal/configs/config_params.go @@ -114,6 +114,7 @@ type StaticConfigParams struct { MainAppProtectLoadModule bool PodName string EnableLatencyMetrics bool + EnablePreviewPolicies bool } // GlobalConfigParams holds global configuration parameters. For now, it only holds listeners. diff --git a/internal/configs/configmaps.go b/internal/configs/configmaps.go index eba87a132a..de1efbcdd1 100644 --- a/internal/configs/configmaps.go +++ b/internal/configs/configmaps.go @@ -549,6 +549,7 @@ func GenerateNginxMainConfig(staticCfgParams *StaticConfigParams, config *Config InternalRouteServer: staticCfgParams.EnableInternalRoutes, InternalRouteServerName: staticCfgParams.PodName, LatencyMetrics: staticCfgParams.EnableLatencyMetrics, + PreviewPolicies: staticCfgParams.EnablePreviewPolicies, } return nginxCfg } diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 0ef7f0d883..9ca6b5e172 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -32,12 +32,14 @@ import ( latCollector "github.com/nginxinc/kubernetes-ingress/internal/metrics/collectors" ) -const pemFileNameForMissingTLSSecret = "/etc/nginx/secrets/default" -const pemFileNameForWildcardTLSSecret = "/etc/nginx/secrets/wildcard" -const appProtectPolicyFolder = "/etc/nginx/waf/nac-policies/" -const appProtectLogConfFolder = "/etc/nginx/waf/nac-logconfs/" -const appProtectUserSigFolder = "/etc/nginx/waf/nac-usersigs/" -const appProtectUserSigIndex = "/etc/nginx/waf/nac-usersigs/index.conf" +const ( + pemFileNameForMissingTLSSecret = "/etc/nginx/secrets/default" + pemFileNameForWildcardTLSSecret = "/etc/nginx/secrets/wildcard" + appProtectPolicyFolder = "/etc/nginx/waf/nac-policies/" + appProtectLogConfFolder = "/etc/nginx/waf/nac-logconfs/" + appProtectUserSigFolder = "/etc/nginx/waf/nac-usersigs/" + appProtectUserSigIndex = "/etc/nginx/waf/nac-usersigs/index.conf" +) // DefaultServerSecretName is the filename of the Secret with a TLS cert and a key for the default server. const DefaultServerSecretName = "default" @@ -51,6 +53,9 @@ const JWTKeyKey = "jwk" // CAKey is the key of the data field of a Secret where the cert must be stored. const CAKey = "ca.crt" +// ClientSecretKey is the key of the data field of a Secret where the OIDC client secret must be stored. +const ClientSecretKey = "client-secret" + // SPIFFE filenames and modes const ( spiffeCertFileName = "spiffe_cert.pem" @@ -373,7 +378,8 @@ func (cnf *Configurator) updateVirtualServerMetricsLabels(virtualServerEx *Virtu newZonesNames := []string{virtualServerEx.VirtualServer.Spec.Host} serverZoneLabels[virtualServerEx.VirtualServer.Spec.Host] = []string{ - "virtualserver", virtualServerEx.VirtualServer.Name, virtualServerEx.VirtualServer.Namespace} + "virtualserver", virtualServerEx.VirtualServer.Name, virtualServerEx.VirtualServer.Namespace, + } newZones[virtualServerEx.VirtualServer.Spec.Host] = true @@ -504,7 +510,8 @@ func (cnf *Configurator) updateTransportServerMetricsLabels(transportServerEx *T newZonesNames := []string{zoneName} streamServerZoneLabels[zoneName] = []string{ - "transportserver", transportServerEx.TransportServer.Name, transportServerEx.TransportServer.Namespace} + "transportserver", transportServerEx.TransportServer.Name, transportServerEx.TransportServer.Namespace, + } newZones[zoneName] = true removedZones := findRemovedKeys(cnf.metricLabelsIndex.transportServerServerZones[key], newZones) @@ -635,6 +642,12 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string { return cnf.nginxManager.CreateSecret(name, data, nginx.JWKSecretFileMode) } +func (cnf *Configurator) addOrUpdateOIDCSecret(secret *api_v1.Secret) string { + name := objectMetaToFileName(&secret.ObjectMeta) + data := secret.Data[ClientSecretKey] + return cnf.nginxManager.CreateSecret(name, data, nginx.OIDCSecretFileMode) +} + // AddOrUpdateResources adds or updates configuration for resources. func (cnf *Configurator) AddOrUpdateResources(ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { allWarnings := newWarnings() @@ -1077,7 +1090,6 @@ func (cnf *Configurator) UpdateConfig(cfgParams *ConfigParams, ingExes []*Ingres // might be removed from NGINX. func (cnf *Configurator) UpdateGlobalConfiguration(globalConfiguration *conf_v1alpha1.GlobalConfiguration, transportServerExes []*TransportServerEx) (updatedTransportServerExes []*TransportServerEx, deletedTransportServerExes []*TransportServerEx, err error) { - cnf.globalCfgParams = ParseGlobalConfiguration(globalConfiguration, cnf.staticCfgParams.TLSPassthrough) for _, tsEx := range transportServerExes { @@ -1428,6 +1440,8 @@ func (cnf *Configurator) AddOrUpdateSecret(secret *api_v1.Secret) string { return cnf.addOrUpdateCASecret(secret) case secrets.SecretTypeJWK: return cnf.addOrUpdateJWKSecret(secret) + case secrets.SecretTypeOIDC: + return cnf.addOrUpdateOIDCSecret(secret) default: return cnf.addOrUpdateTLSSecret(secret) } diff --git a/internal/configs/ingress.go b/internal/configs/ingress.go index 67764b76a2..29eacbc29b 100644 --- a/internal/configs/ingress.go +++ b/internal/configs/ingress.go @@ -259,18 +259,21 @@ func generateJWTConfig(owner runtime.Object, secretRefs map[string]*secrets.Secr redirectLocationName string) (*version1.JWTAuth, *version1.JWTRedirectLocation, Warnings) { warnings := newWarnings() - secret := secretRefs[cfgParams.JWTKey] - - if secret.Type != "" && secret.Type != secrets.SecretTypeJWK { - warnings.AddWarningf(owner, "JWK secret %s is of a wrong type '%s', must be '%s'", cfgParams.JWTKey, secret.Type, secrets.SecretTypeJWK) - } else if secret.Error != nil { - warnings.AddWarningf(owner, "JWK secret %s is invalid: %v", cfgParams.JWTKey, secret.Error) + secretRef := secretRefs[cfgParams.JWTKey] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != secrets.SecretTypeJWK { + warnings.AddWarningf(owner, "JWK secret %s is of a wrong type '%s', must be '%s'", cfgParams.JWTKey, secretType, secrets.SecretTypeJWK) + } else if secretRef.Error != nil { + warnings.AddWarningf(owner, "JWK secret %s is invalid: %v", cfgParams.JWTKey, secretRef.Error) } // Key is configured for all cases, including when the secret is (1) invalid or (2) of a wrong type. // For (1) and (2), NGINX Plus will reject such a key at runtime and return 500 to clients. jwtAuth := &version1.JWTAuth{ - Key: secret.Path, + Key: secretRef.Path, Realm: cfgParams.JWTRealm, Token: cfgParams.JWTToken, } @@ -312,15 +315,19 @@ func addSSLConfig(server *version1.Server, owner runtime.Object, host string, in var pemFile string if tlsSecret != "" { - secret := secretRefs[tlsSecret] - if secret.Type != "" && secret.Type != api_v1.SecretTypeTLS { + secretRef := secretRefs[tlsSecret] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != api_v1.SecretTypeTLS { pemFile = pemFileNameForMissingTLSSecret - warnings.AddWarningf(owner, "TLS secret %s is of a wrong type '%s', must be '%s'", tlsSecret, secret.Type, api_v1.SecretTypeTLS) - } else if secret.Error != nil { + warnings.AddWarningf(owner, "TLS secret %s is of a wrong type '%s', must be '%s'", tlsSecret, secretType, api_v1.SecretTypeTLS) + } else if secretRef.Error != nil { pemFile = pemFileNameForMissingTLSSecret - warnings.AddWarningf(owner, "TLS secret %s is invalid: %v", tlsSecret, secret.Error) + warnings.AddWarningf(owner, "TLS secret %s is invalid: %v", tlsSecret, secretRef.Error) } else { - pemFile = secret.Path + pemFile = secretRef.Path } } else if isWildcardEnabled { pemFile = pemFileNameForWildcardTLSSecret diff --git a/internal/configs/ingress_test.go b/internal/configs/ingress_test.go index 9d8af7f3e6..5eb4236387 100644 --- a/internal/configs/ingress_test.go +++ b/internal/configs/ingress_test.go @@ -40,7 +40,9 @@ func TestGenerateNginxCfgForJWT(t *testing.T) { cafeIngressEx.Ingress.Annotations["nginx.com/jwt-token"] = "$cookie_auth_token" cafeIngressEx.Ingress.Annotations["nginx.com/jwt-login-url"] = "https://login.example.com" cafeIngressEx.SecretRefs["cafe-jwk"] = &secrets.SecretReference{ - Type: "nginx.org/jwk", + Secret: &v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-cafe-jwk", } @@ -310,7 +312,9 @@ func createCafeIngressEx() IngressEx { }, SecretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: v1.SecretTypeTLS, + Secret: &v1.Secret{ + Type: v1.SecretTypeTLS, + }, Path: "/etc/nginx/secrets/default-cafe-secret", }, }, @@ -367,7 +371,9 @@ func TestGenerateNginxCfgForMergeableIngressesForJWT(t *testing.T) { mergeableIngresses.Master.Ingress.Annotations["nginx.com/jwt-token"] = "$cookie_auth_token" mergeableIngresses.Master.Ingress.Annotations["nginx.com/jwt-login-url"] = "https://login.example.com" mergeableIngresses.Master.SecretRefs["cafe-jwk"] = &secrets.SecretReference{ - Type: secrets.SecretTypeJWK, + Secret: &v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-cafe-jwk", } @@ -376,7 +382,9 @@ func TestGenerateNginxCfgForMergeableIngressesForJWT(t *testing.T) { mergeableIngresses.Minions[0].Ingress.Annotations["nginx.com/jwt-token"] = "$cookie_auth_token_coffee" mergeableIngresses.Minions[0].Ingress.Annotations["nginx.com/jwt-login-url"] = "https://login.cofee.example.com" mergeableIngresses.Minions[0].SecretRefs["coffee-jwk"] = &secrets.SecretReference{ - Type: secrets.SecretTypeJWK, + Secret: &v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-coffee-jwk", } @@ -530,7 +538,9 @@ func createMergeableCafeIngress() *MergeableIngresses { }, SecretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: v1.SecretTypeTLS, + Secret: &v1.Secret{ + Type: v1.SecretTypeTLS, + }, Path: "/etc/nginx/secrets/default-cafe-secret", Error: nil, }, @@ -901,7 +911,9 @@ func TestAddSSLConfig(t *testing.T) { }, secretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: v1.SecretTypeTLS, + Secret: &v1.Secret{ + Type: v1.SecretTypeTLS, + }, Path: "/etc/nginx/secrets/default-cafe-secret", }, }, @@ -920,7 +932,9 @@ func TestAddSSLConfig(t *testing.T) { }, secretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: v1.SecretTypeTLS, + Secret: &v1.Secret{ + Type: v1.SecretTypeTLS, + }, Path: "/etc/nginx/secrets/default-cafe-secret", }, }, @@ -943,7 +957,9 @@ func TestAddSSLConfig(t *testing.T) { }, secretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: v1.SecretTypeTLS, + Secret: &v1.Secret{ + Type: v1.SecretTypeTLS, + }, Error: errors.New("invalid secret"), }, }, @@ -971,7 +987,9 @@ func TestAddSSLConfig(t *testing.T) { }, secretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: secrets.SecretTypeCA, + Secret: &v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "/etc/nginx/secrets/default-cafe-secret", }, }, @@ -999,7 +1017,9 @@ func TestAddSSLConfig(t *testing.T) { }, secretRefs: map[string]*secrets.SecretReference{ "cafe-secret": { - Type: secrets.SecretTypeCA, + Secret: &v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "", Error: errors.New("CA secret must have the data field ca.crt"), }, @@ -1087,7 +1107,9 @@ func TestGenerateJWTConfig(t *testing.T) { { secretRefs: map[string]*secrets.SecretReference{ "cafe-jwk": { - Type: secrets.SecretTypeJWK, + Secret: &v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-cafe-jwk", }, }, @@ -1109,7 +1131,9 @@ func TestGenerateJWTConfig(t *testing.T) { { secretRefs: map[string]*secrets.SecretReference{ "cafe-jwk": { - Type: secrets.SecretTypeJWK, + Secret: &v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-cafe-jwk", }, }, @@ -1136,7 +1160,9 @@ func TestGenerateJWTConfig(t *testing.T) { { secretRefs: map[string]*secrets.SecretReference{ "cafe-jwk": { - Type: secrets.SecretTypeJWK, + Secret: &v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-cafe-jwk", Error: errors.New("invalid secret"), }, @@ -1163,7 +1189,9 @@ func TestGenerateJWTConfig(t *testing.T) { { secretRefs: map[string]*secrets.SecretReference{ "cafe-jwk": { - Type: secrets.SecretTypeCA, + Secret: &v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "/etc/nginx/secrets/default-cafe-jwk", }, }, @@ -1189,7 +1217,9 @@ func TestGenerateJWTConfig(t *testing.T) { { secretRefs: map[string]*secrets.SecretReference{ "cafe-jwk": { - Type: secrets.SecretTypeCA, + Secret: &v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "", Error: errors.New("CA secret must have the data field ca.crt"), }, diff --git a/internal/configs/oidc/oidc.conf b/internal/configs/oidc/oidc.conf new file mode 100644 index 0000000000..728933422e --- /dev/null +++ b/internal/configs/oidc/oidc.conf @@ -0,0 +1,96 @@ + # Advanced configuration START + set $internal_error_message "NGINX / OpenID Connect login failure\n"; + set $pkce_id ""; + # resolver 8.8.8.8; # For DNS lookup of IdP endpoints; + subrequest_output_buffer_size 32k; # To fit a complete tokenset response + gunzip on; # Decompress IdP responses if necessary + # Advanced configuration END + + location = /_jwks_uri { + internal; + proxy_cache jwk; # Cache the JWK Set recieved from IdP + proxy_cache_valid 200 12h; # How long to consider keys "fresh" + proxy_cache_use_stale error timeout updating; # Use old JWK Set if cannot reach IdP + proxy_ssl_server_name on; # For SNI to the IdP + proxy_method GET; # In case client request was non-GET + proxy_set_header Content-Length ""; # '' + proxy_pass $oidc_jwt_keyfile; # Expecting to find a URI here + proxy_ignore_headers Cache-Control Expires Set-Cookie; # Does not influence caching + } + + location @do_oidc_flow { + status_zone "OIDC start"; + js_content oidc.auth; + default_type text/plain; # In case we throw an error + } + + #set $redir_location "/_codexch"; + location = /_codexch { + # This location is called by the IdP after successful authentication + status_zone "OIDC code exchange"; + js_content oidc.codeExchange; + error_page 500 502 504 @oidc_error; + } + + location = /_token { + # This location is called by oidcCodeExchange(). We use the proxy_ directives + # to construct the OpenID Connect token request, as per: + # http://openid.net/specs/openid-connect-core-1_0.html#TokenRequest + internal; + proxy_ssl_server_name on; # For SNI to the IdP + proxy_set_header Content-Type "application/x-www-form-urlencoded"; + proxy_set_body "grant_type=authorization_code&client_id=$oidc_client&$args&redirect_uri=$redirect_base$redir_location"; + proxy_method POST; + proxy_pass $oidc_token_endpoint; + } + + location = /_refresh { + # This location is called by oidcAuth() when performing a token refresh. We + # use the proxy_ directives to construct the OpenID Connect token request, as per: + # https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken + internal; + proxy_ssl_server_name on; # For SNI to the IdP + proxy_set_header Content-Type "application/x-www-form-urlencoded"; + proxy_set_body "grant_type=refresh_token&refresh_token=$arg_token&client_id=$oidc_client&client_secret=$oidc_client_secret"; + proxy_method POST; + proxy_pass $oidc_token_endpoint; + } + + location = /_id_token_validation { + # This location is called by oidcCodeExchange() and oidcRefreshRequest(). We use + # the auth_jwt_module to validate the OpenID Connect token response, as per: + # https://openid.net/specs/openid-connect-core-1_0.html#IDTokenValidation + internal; + auth_jwt "" token=$arg_token; + js_content oidc.validateIdToken; + error_page 500 502 504 @oidc_error; + } + + location = /logout { + status_zone "OIDC logout"; + add_header Set-Cookie "auth_token=; $oidc_cookie_flags"; # Send empty cookie + add_header Set-Cookie "auth_redir=; $oidc_cookie_flags"; # Erase original cookie + js_content oidc.logout; + } + + location = /_logout { + # This location is the default value of $oidc_logout_redirect (in case it wasn't configured) + default_type text/plain; + return 200 "Logged out\n"; + } + + location @oidc_error { + # This location is called when oidcAuth() or oidcCodeExchange() returns an error + status_zone "OIDC error"; + default_type text/plain; + return 500 $internal_error_message; + } + + # location /api/ { + # api write=on; + # allow 127.0.0.1; # Only the NGINX host may call the NIGNX Plus API + # deny all; + # access_log off; + # } + +# vim: syntax=nginx diff --git a/internal/configs/oidc/oidc_common.conf b/internal/configs/oidc/oidc_common.conf new file mode 100644 index 0000000000..aedaa8b036 --- /dev/null +++ b/internal/configs/oidc/oidc_common.conf @@ -0,0 +1,26 @@ +map $proto $oidc_cookie_flags { + http "Path=/; SameSite=lax;"; # For HTTP/plaintext testing + https "Path=/; SameSite=lax; HttpOnly; Secure;"; # Production recommendation +} + +map $http_x_forwarded_port $redirect_base { + "" $proto://$host:$server_port; + default $proto://$host:$http_x_forwarded_port; +} + +map $http_x_forwarded_proto $proto { + "" $scheme; + default $http_x_forwarded_proto; +} + +proxy_cache_path /var/cache/nginx/jwk levels=1 keys_zone=jwk:64k max_size=1m; +keyval_zone zone=oidc_id_tokens:1M timeout=1h sync; +keyval_zone zone=refresh_tokens:1M timeout=8h sync; + +keyval $cookie_auth_token $session_jwt zone=oidc_id_tokens; # Exchange cookie for JWT +keyval $cookie_auth_token $refresh_token zone=refresh_tokens; # Exchange cookie for refresh token +keyval $request_id $new_session zone=oidc_id_tokens; # For initial session creation +keyval $request_id $new_refresh zone=refresh_tokens; # '' + +auth_jwt_claim_set $jwt_audience aud; # In case aud is an array +js_import oidc from oidc/openid_connect.js; diff --git a/internal/configs/oidc/openid_connect.js b/internal/configs/oidc/openid_connect.js new file mode 100644 index 0000000000..2d9fabaad2 --- /dev/null +++ b/internal/configs/oidc/openid_connect.js @@ -0,0 +1,275 @@ +/* + * JavaScript functions for providing OpenID Connect with NGINX Plus + * + * Copyright (C) 2020 Nginx, Inc. + */ +var newSession = false; // Used by oidcAuth() and validateIdToken() + +export default { auth, codeExchange, validateIdToken, logout }; + +function auth(r) { + if (!r.variables.refresh_token || r.variables.refresh_token == "-") { + newSession = true; + + // Check we have all necessary configuration variables (referenced only by njs) + var oidcConfigurables = ["authz_endpoint", "scopes", "hmac_key", "cookie_flags"]; + var missingConfig = []; + for (var i in oidcConfigurables) { + if (!r.variables["oidc_" + oidcConfigurables[i]] || r.variables["oidc_" + oidcConfigurables[i]] == "") { + missingConfig.push(oidcConfigurables[i]); + } + } + if (missingConfig.length) { + r.error("OIDC missing configuration variables: $oidc_" + missingConfig.join(" $oidc_")); + r.return(500, r.variables.internal_error_message); + return; + } + // Redirect the client to the IdP login page with the cookies we need for state + r.return(302, r.variables.oidc_authz_endpoint + getAuthZArgs(r)); + return; + } + + // Pass the refresh token to the /_refresh location so that it can be + // proxied to the IdP in exchange for a new id_token + r.subrequest("/_refresh", "token=" + r.variables.refresh_token, + function (reply) { + if (reply.status != 200) { + // Refresh request failed, log the reason + var error_log = "OIDC refresh failure"; + if (reply.status == 504) { + error_log += ", timeout waiting for IdP"; + } else if (reply.status == 400) { + try { + var errorset = JSON.parse(reply.responseBody); + error_log += ": " + errorset.error + " " + errorset.error_description; + } catch (e) { + error_log += ": " + reply.responseBody; + } + } else { + error_log += " " + reply.status; + } + r.error(error_log); + + // Clear the refresh token, try again + r.variables.refresh_token = "-"; + r.return(302, r.variables.request_uri); + return; + } + + // Refresh request returned 200, check response + try { + var tokenset = JSON.parse(reply.responseBody); + if (!tokenset.id_token) { + r.error("OIDC refresh response did not include id_token"); + if (tokenset.error) { + r.error("OIDC " + tokenset.error + " " + tokenset.error_description); + } + r.variables.refresh_token = "-"; + r.return(302, r.variables.request_uri); + return; + } + + // Send the new ID Token to auth_jwt location for validation + r.subrequest("/_id_token_validation", "token=" + tokenset.id_token, + function (reply) { + if (reply.status != 204) { + r.variables.refresh_token = "-"; + r.return(302, r.variables.request_uri); + return; + } + + // ID Token is valid, update keyval + r.log("OIDC refresh success, updating id_token for " + r.variables.cookie_auth_token); + r.variables.session_jwt = tokenset.id_token; // Update key-value store + + // Update refresh token (if we got a new one) + if (r.variables.refresh_token != tokenset.refresh_token) { + r.log("OIDC replacing previous refresh token (" + r.variables.refresh_token + ") with new value: " + tokenset.refresh_token); + r.variables.refresh_token = tokenset.refresh_token; // Update key-value store + } + + delete r.headersOut["WWW-Authenticate"]; // Remove evidence of original failed auth_jwt + r.internalRedirect(r.variables.request_uri); // Continue processing original request + } + ); + } catch (e) { + r.variables.refresh_token = "-"; + r.return(302, r.variables.request_uri); + return; + } + } + ); +} + +function codeExchange(r) { + // First check that we received an authorization code from the IdP + if (r.variables.arg_code.length == 0) { + if (r.variables.arg_error) { + r.error("OIDC error receiving authorization code from IdP: " + r.variables.arg_error_description); + } else { + r.error("OIDC expected authorization code from IdP but received: " + r.uri); + } + r.return(502); + return; + } + + // Pass the authorization code to the /_token location so that it can be + // proxied to the IdP in exchange for a JWT + r.subrequest("/_token", idpClientAuth(r), function (reply) { + if (reply.status == 504) { + r.error("OIDC timeout connecting to IdP when sending authorization code"); + r.return(504); + return; + } + + if (reply.status != 200) { + try { + var errorset = JSON.parse(reply.responseBody); + if (errorset.error) { + r.error("OIDC error from IdP when sending authorization code: " + errorset.error + ", " + errorset.error_description); + } else { + r.error("OIDC unexpected response from IdP when sending authorization code (HTTP " + reply.status + "). " + reply.responseBody); + } + } catch (e) { + r.error("OIDC unexpected response from IdP when sending authorization code (HTTP " + reply.status + "). " + reply.responseBody); + } + r.return(502); + return; + } + + // Code exchange returned 200, check for errors + try { + var tokenset = JSON.parse(reply.responseBody); + if (tokenset.error) { + r.error("OIDC " + tokenset.error + " " + tokenset.error_description); + r.return(500); + return; + } + + // Send the ID Token to auth_jwt location for validation + r.subrequest("/_id_token_validation", "token=" + tokenset.id_token, + function (reply) { + if (reply.status != 204) { + r.return(500); // validateIdToken() will log errors + return; + } + + // If the response includes a refresh token then store it + if (tokenset.refresh_token) { + r.variables.new_refresh = tokenset.refresh_token; // Create key-value store entry + r.log("OIDC refresh token stored"); + } else { + r.warn("OIDC no refresh token"); + } + + // Add opaque token to keyval session store + r.log("OIDC success, creating session " + r.variables.request_id); + r.variables.new_session = tokenset.id_token; // Create key-value store entry + r.headersOut["Set-Cookie"] = "auth_token=" + r.variables.request_id + "; " + r.variables.oidc_cookie_flags; + r.return(302, r.variables.redirect_base + r.variables.cookie_auth_redir); + } + ); + } catch (e) { + r.error("OIDC authorization code sent but token response is not JSON. " + reply.responseBody); + r.return(502); + } + } + ); +} + +function validateIdToken(r) { + // Check mandatory claims + var required_claims = ["iat", "iss", "sub"]; // aud is checked separately + var missing_claims = []; + for (var i in required_claims) { + if (r.variables["jwt_claim_" + required_claims[i]].length == 0) { + missing_claims.push(required_claims[i]); + } + } + if (r.variables.jwt_audience.length == 0) missing_claims.push("aud"); + if (missing_claims.length) { + r.error("OIDC ID Token validation error: missing claim(s) " + missing_claims.join(" ")); + r.return(403); + return; + } + var validToken = true; + + // Check iat is a positive integer + var iat = Math.floor(Number(r.variables.jwt_claim_iat)); + if (String(iat) != r.variables.jwt_claim_iat || iat < 1) { + r.error("OIDC ID Token validation error: iat claim is not a valid number"); + validToken = false; + } + + // Audience matching + var aud = r.variables.jwt_audience.split(","); + if (!aud.includes(r.variables.oidc_client)) { + r.error("OIDC ID Token validation error: aud claim (" + r.variables.jwt_audience + ") does not include configured $oidc_client (" + r.variables.oidc_client + ")"); + validToken = false; + } + + // If we receive a nonce in the ID Token then we will use the auth_nonce cookies + // to check that the JWT can be validated as being directly related to the + // original request by this client. This mitigates against token replay attacks. + if (newSession) { + var client_nonce_hash = ""; + if (r.variables.cookie_auth_nonce) { + var c = require('crypto'); + var h = c.createHmac('sha256', r.variables.oidc_hmac_key).update(r.variables.cookie_auth_nonce); + client_nonce_hash = h.digest('base64url'); + } + if (r.variables.jwt_claim_nonce != client_nonce_hash) { + r.error("OIDC ID Token validation error: nonce from token (" + r.variables.jwt_claim_nonce + ") does not match client (" + client_nonce_hash + ")"); + validToken = false; + } + } + + if (validToken) { + r.return(204); + } else { + r.return(403); + } +} + +function logout(r) { + r.log("OIDC logout for " + r.variables.cookie_auth_token); + r.variables.session_jwt = "-"; + r.variables.refresh_token = "-"; + r.return(302, r.variables.oidc_logout_redirect); +} + +function getAuthZArgs(r) { + // Choose a nonce for this flow for the client, and hash it for the IdP + var noncePlain = r.variables.request_id; + var c = require('crypto'); + var h = c.createHmac('sha256', r.variables.oidc_hmac_key).update(noncePlain); + var nonceHash = h.digest('base64url'); + var authZArgs = "?response_type=code&scope=" + r.variables.oidc_scopes + "&client_id=" + r.variables.oidc_client + "&redirect_uri=" + r.variables.redirect_base + r.variables.redir_location + "&nonce=" + nonceHash; + + r.headersOut['Set-Cookie'] = [ + "auth_redir=" + r.variables.request_uri + "; " + r.variables.oidc_cookie_flags, + "auth_nonce=" + noncePlain + "; " + r.variables.oidc_cookie_flags + ]; + + if (r.variables.oidc_pkce_enable == 1) { + var pkce_code_verifier = c.createHmac('sha256', r.variables.oidc_hmac_key).update(String(Math.random())).digest('hex'); + r.variables.pkce_id = c.createHash('sha256').update(String(Math.random())).digest('base64url'); + var pkce_code_challenge = c.createHash('sha256').update(pkce_code_verifier).digest('base64url'); + r.variables.pkce_code_verifier = pkce_code_verifier; + + authZArgs += "&code_challenge_method=S256&code_challenge=" + pkce_code_challenge + "&state=" + r.variables.pkce_id; + } else { + authZArgs += "&state=0"; + } + return authZArgs; +} + +function idpClientAuth(r) { + // If PKCE is enabled we have to use the code_verifier + if (r.variables.oidc_pkce_enable == 1) { + r.variables.pkce_id = r.variables.arg_state; + return "code=" + r.variables.arg_code + "&code_verifier=" + r.variables.pkce_code_verifier; + } else { + return "code=" + r.variables.arg_code + "&client_secret=" + r.variables.oidc_client_secret; + } +} diff --git a/internal/configs/version1/config.go b/internal/configs/version1/config.go index ac9f160385..7d2206961b 100644 --- a/internal/configs/version1/config.go +++ b/internal/configs/version1/config.go @@ -197,6 +197,7 @@ type MainConfig struct { InternalRouteServer bool InternalRouteServerName string LatencyMetrics bool + PreviewPolicies bool } // NewUpstreamWithDefaultServer creates an upstream with the default server. diff --git a/internal/configs/version1/nginx-plus.tmpl b/internal/configs/version1/nginx-plus.tmpl index 4a7f72a9dc..0326359459 100644 --- a/internal/configs/version1/nginx-plus.tmpl +++ b/internal/configs/version1/nginx-plus.tmpl @@ -23,6 +23,10 @@ load_module modules/ngx_http_app_protect_module.so; {{$value}}{{end}} {{- end}} +{{if .PreviewPolicies}} +load_module modules/ngx_http_js_module.so; +{{- end}} + events { worker_connections {{.WorkerConnections}}; } @@ -104,6 +108,10 @@ http { {{if .ResolverTimeout}}resolver_timeout {{.ResolverTimeout}};{{end}} {{end}} + {{if .PreviewPolicies}} + include oidc/oidc_common.conf; + {{- end}} + server { # required to support the Websocket protocol in VirtualServer/VirtualServerRoutes set $default_connection_header ""; diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index fb24f3d3cb..d0c9c136be 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -12,14 +12,14 @@ type UpstreamLabels struct { // VirtualServerConfig holds NGINX configuration for a VirtualServer. type VirtualServerConfig struct { + HTTPSnippets []string + LimitReqZones []LimitReqZone + Maps []Map Server Server - Upstreams []Upstream + SpiffeCerts bool SplitClients []SplitClient - Maps []Map StatusMatches []StatusMatch - LimitReqZones []LimitReqZone - HTTPSnippets []string - SpiffeCerts bool + Upstreams []Upstream } // Upstream defines an upstream. @@ -69,6 +69,7 @@ type Server struct { JWTAuth *JWTAuth IngressMTLS *IngressMTLS EgressMTLS *EgressMTLS + OIDC *OIDC PoliciesErrorReturn *Return VSNamespace string VSName string @@ -103,6 +104,16 @@ type EgressMTLS struct { SSLName string } +type OIDC struct { + AuthEndpoint string + ClientID string + ClientSecret string + JwksURI string + Scope string + TokenEndpoint string + RedirectURI string +} + // Location defines a location. type Location struct { Path string @@ -139,6 +150,7 @@ type Location struct { LimitReqs []LimitReq JWTAuth *JWTAuth EgressMTLS *EgressMTLS + OIDC *bool PoliciesErrorReturn *Return ServiceName string IsVSR bool diff --git a/internal/configs/version2/nginx-plus.virtualserver.tmpl b/internal/configs/version2/nginx-plus.virtualserver.tmpl index a1bd2e33c4..facc41dbd5 100644 --- a/internal/configs/version2/nginx-plus.virtualserver.tmpl +++ b/internal/configs/version2/nginx-plus.virtualserver.tmpl @@ -64,6 +64,22 @@ server { set $resource_name "{{$s.VSName}}"; set $resource_namespace "{{$s.VSNamespace}}"; + {{ with $oidc := $s.OIDC }} + include oidc/oidc.conf; + + set $oidc_pkce_enable 0; + set $oidc_logout_redirect "/_logout"; + set $oidc_hmac_key "{{ $s.VSName }}"; + + set $oidc_authz_endpoint "{{ $oidc.AuthEndpoint }}"; + set $oidc_token_endpoint "{{ $oidc.TokenEndpoint }}"; + set $oidc_jwt_keyfile "{{ $oidc.JwksURI }}"; + set $oidc_scopes "{{ $oidc.Scope }}"; + set $oidc_client "{{ $oidc.ClientID }}"; + set $oidc_client_secret "{{ $oidc.ClientSecret }}"; + set $redir_location "{{ $oidc.RedirectURI }}"; + {{ end }} + {{ with $ssl := $s.SSL }} {{ if $s.TLSPassthrough }} listen unix:/var/lib/nginx/passthrough-https.sock{{ if $ssl.HTTP2 }} http2{{ end }} proxy_protocol; @@ -282,6 +298,12 @@ server { proxy_ssl_name {{ .SSLName }}; {{ end }} + {{ if $l.OIDC }} + auth_jwt "" token=$session_jwt; + error_page 401 = @do_oidc_flow; + auth_jwt_key_request /_jwks_uri; + proxy_set_header username $jwt_claim_sub; + {{ end }} {{ range $e := $l.ErrorPages }} error_page {{ $e.Codes }} {{ if ne 0 $e.ResponseCode }}={{ $e.ResponseCode }}{{ end }} "{{ $e.Name }}"; diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index b03648ae85..748d9e67c0 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -193,6 +193,12 @@ type virtualServerConfigurator struct { enableSnippets bool warnings Warnings spiffeCerts bool + oidcPolCfg *oidcPolicyCfg +} + +type oidcPolicyCfg struct { + oidc *version2.OIDC + key string } func (vsc *virtualServerConfigurator) addWarningf(obj runtime.Object, msgFmt string, args ...interface{}) { @@ -224,6 +230,7 @@ func newVirtualServerConfigurator( enableSnippets: staticParams.EnableSnippets, warnings: make(map[runtime.Object][]string), spiffeCerts: staticParams.NginxServiceMesh, + oidcPolCfg: &oidcPolicyCfg{}, } } @@ -339,10 +346,10 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS var splitClients []version2.SplitClient var maps []version2.Map var errorPageLocations []version2.ErrorPageLocation - var vsrErrorPagesFromVs = make(map[string][]conf_v1.ErrorPage) - var vsrErrorPagesRouteIndex = make(map[string]int) - var vsrLocationSnippetsFromVs = make(map[string]string) - var vsrPoliciesFromVs = make(map[string][]conf_v1.PolicyReference) + vsrErrorPagesFromVs := make(map[string][]conf_v1.ErrorPage) + vsrErrorPagesRouteIndex := make(map[string]int) + vsrLocationSnippetsFromVs := make(map[string]string) + vsrPoliciesFromVs := make(map[string][]conf_v1.PolicyReference) isVSR := false matchesRoutes := 0 @@ -387,6 +394,9 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS vsName: vsEx.VirtualServer.Name, } routePoliciesCfg := vsc.generatePolicies(ownerDetails, r.Policies, vsEx.Policies, routeContext, policyOpts) + if policiesCfg.OIDC != nil { + routePoliciesCfg.OIDC = policiesCfg.OIDC + } limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...) if len(r.Matches) > 0 { @@ -487,6 +497,9 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS context = subRouteContext } routePoliciesCfg := vsc.generatePolicies(ownerDetails, policyRefs, vsEx.Policies, context, policyOpts) + if policiesCfg.OIDC != nil { + routePoliciesCfg.OIDC = policiesCfg.OIDC + } limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...) if len(r.Matches) > 0 { cfg := generateMatchesConfig( @@ -578,6 +591,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS JWTAuth: policiesCfg.JWTAuth, IngressMTLS: policiesCfg.IngressMTLS, EgressMTLS: policiesCfg.EgressMTLS, + OIDC: vsc.oidcPolCfg.oidc, PoliciesErrorReturn: policiesCfg.ErrorReturn, VSNamespace: vsEx.VirtualServer.Namespace, VSName: vsEx.VirtualServer.Name, @@ -597,6 +611,7 @@ type policiesCfg struct { JWTAuth *version2.JWTAuth IngressMTLS *version2.IngressMTLS EgressMTLS *version2.EgressMTLS + OIDC *bool ErrorReturn *version2.Return } @@ -683,19 +698,23 @@ func (p *policiesCfg) addJWTAuthConfig( } jwtSecretKey := fmt.Sprintf("%v/%v", polNamespace, jwtAuth.Secret) - secret := secretRefs[jwtSecretKey] - if secret.Type != "" && secret.Type != secrets.SecretTypeJWK { - res.addWarningf("JWT policy %q references a Secret of an incorrect type %q", polKey, secret.Type) + secretRef := secretRefs[jwtSecretKey] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != secrets.SecretTypeJWK { + res.addWarningf("JWT policy %q references a Secret of an incorrect type %q", polKey, secretType) res.isError = true return res - } else if secret.Error != nil { - res.addWarningf("JWT policy %q references an invalid Secret: %v", polKey, secret.Error) + } else if secretRef.Error != nil { + res.addWarningf("JWT policy %q references an invalid Secret: %v", polKey, secretRef.Error) res.isError = true return res } p.JWTAuth = &version2.JWTAuth{ - Secret: secret.Path, + Secret: secretRef.Path, Realm: jwtAuth.Realm, Token: jwtAuth.Token, } @@ -727,13 +746,17 @@ func (p *policiesCfg) addIngressMTLSConfig( } secretKey := fmt.Sprintf("%v/%v", polNamespace, ingressMTLS.ClientCertSecret) - secret := secretRefs[secretKey] - if secret.Type != "" && secret.Type != secrets.SecretTypeCA { - res.addWarningf("IngressMTLS policy %q references a Secret of an incorrect type %q", polKey, secret.Type) + secretRef := secretRefs[secretKey] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != secrets.SecretTypeCA { + res.addWarningf("IngressMTLS policy %q references a Secret of an incorrect type %q", polKey, secretType) res.isError = true return res - } else if secret.Error != nil { - res.addWarningf("IngressMTLS policy %q references an invalid Secret: %v", polKey, secret.Error) + } else if secretRef.Error != nil { + res.addWarningf("IngressMTLS policy %q references an invalid Secret: %v", polKey, secretRef.Error) res.isError = true return res } @@ -748,7 +771,7 @@ func (p *policiesCfg) addIngressMTLSConfig( } p.IngressMTLS = &version2.IngressMTLS{ - ClientCert: secret.Path, + ClientCert: secretRef.Path, VerifyClient: verifyClient, VerifyDepth: verifyDepth, } @@ -775,18 +798,22 @@ func (p *policiesCfg) addEgressMTLSConfig( if egressMTLS.TLSSecret != "" { egressTLSSecret := fmt.Sprintf("%v/%v", polNamespace, egressMTLS.TLSSecret) - tlsSecret := secretRefs[egressTLSSecret] - if tlsSecret.Type != "" && tlsSecret.Type != api_v1.SecretTypeTLS { - res.addWarningf("EgressMTLS policy %q references a Secret of an incorrect type %q", polKey, tlsSecret.Type) + secretRef := secretRefs[egressTLSSecret] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != api_v1.SecretTypeTLS { + res.addWarningf("EgressMTLS policy %q references a Secret of an incorrect type %q", polKey, secretType) res.isError = true return res - } else if tlsSecret.Error != nil { - res.addWarningf("EgressMTLS policy %q references an invalid Secret: %v", polKey, tlsSecret.Error) + } else if secretRef.Error != nil { + res.addWarningf("EgressMTLS policy %q references an invalid Secret: %v", polKey, secretRef.Error) res.isError = true return res } - tlsSecretPath = tlsSecret.Path + tlsSecretPath = secretRef.Path } var trustedSecretPath string @@ -794,18 +821,22 @@ func (p *policiesCfg) addEgressMTLSConfig( if egressMTLS.TrustedCertSecret != "" { trustedCertSecret := fmt.Sprintf("%v/%v", polNamespace, egressMTLS.TrustedCertSecret) - trustedSecret := secretRefs[trustedCertSecret] - if trustedSecret.Type != "" && trustedSecret.Type != secrets.SecretTypeCA { - res.addWarningf("EgressMTLS policy %q references a Secret of an incorrect type %q", polKey, trustedSecret.Type) + secretRef := secretRefs[trustedCertSecret] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != secrets.SecretTypeCA { + res.addWarningf("EgressMTLS policy %q references a Secret of an incorrect type %q", polKey, secretType) res.isError = true return res - } else if trustedSecret.Error != nil { - res.addWarningf("EgressMTLS policy %q references an invalid Secret: %v", polKey, trustedSecret.Error) + } else if secretRef.Error != nil { + res.addWarningf("EgressMTLS policy %q references an invalid Secret: %v", polKey, secretRef.Error) res.isError = true return res } - trustedSecretPath = trustedSecret.Path + trustedSecretPath = secretRef.Path } p.EgressMTLS = &version2.EgressMTLS{ @@ -823,6 +854,84 @@ func (p *policiesCfg) addEgressMTLSConfig( return res } +func (p *policiesCfg) addOIDCConfig( + oidc *conf_v1.OIDC, + polKey string, + polNamespace string, + secretRefs map[string]*secrets.SecretReference, + oidcPolCfg *oidcPolicyCfg, +) *validationResults { + res := newValidationResults() + if p.OIDC != nil { + res.addWarningf( + "Multiple oidc policies in the same context is not valid. OIDC policy %q will be ignored", + polKey, + ) + return res + } + + if oidcPolCfg.oidc != nil { + if oidcPolCfg.key != polKey { + res.addWarningf( + "Only one OIDC policy is allowed in a VirtualServer and its VirtualServerRoutes. Can't use %q. Use %q", + polKey, + oidcPolCfg.key, + ) + res.isError = true + return res + } + } else { + secretKey := fmt.Sprintf("%v/%v", polNamespace, oidc.ClientSecret) + secretRef, exists := secretRefs[secretKey] + if !exists { + res.addWarningf("OIDC policy %q references a non-existent Secret %v", polKey, secretKey) + res.isError = true + return res + } + + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } + if secretType != "" && secretType != secrets.SecretTypeOIDC { + res.addWarningf("OIDC policy %q references a Secret of an incorrect type %q", polKey, secretType) + res.isError = true + return res + } else if secretRef.Error != nil { + res.addWarningf("OIDC policy %q references an invalid Secret: %v", polKey, secretRef.Error) + res.isError = true + return res + } + + clientSecret, exists := secretRef.Secret.Data[ClientSecretKey] + if !exists { + res.addWarningf("OIDC policy %q references a Secret without the data field %v", polKey, ClientSecretKey) + res.isError = true + return res + } + + redirectURI := oidc.RedirectURI + if redirectURI == "" { + redirectURI = "/_codexch" + } + + oidcPolCfg.oidc = &version2.OIDC{ + AuthEndpoint: oidc.AuthEndpoint, + TokenEndpoint: oidc.TokenEndpoint, + JwksURI: oidc.JWKSURI, + ClientID: oidc.ClientID, + ClientSecret: string(clientSecret), + Scope: oidc.Scope, + RedirectURI: redirectURI, + } + } + + trueVal := true + p.OIDC = &trueVal + + return res +} + func (vsc *virtualServerConfigurator) generatePolicies( ownerDetails policyOwnerDetails, policyRefs []conf_v1.PolicyReference, @@ -867,6 +976,8 @@ func (vsc *virtualServerConfigurator) generatePolicies( ) case pol.Spec.EgressMTLS != nil: res = config.addEgressMTLSConfig(pol.Spec.EgressMTLS, key, polNamespace, policyOpts.secretRefs) + case pol.Spec.OIDC != nil: + res = config.addOIDCConfig(pol.Spec.OIDC, key, polNamespace, policyOpts.secretRefs, vsc.oidcPolCfg) default: res = newValidationResults() } @@ -945,6 +1056,7 @@ func addPoliciesCfgToLocation(cfg policiesCfg, location *version2.Location) { location.LimitReqs = cfg.LimitReqs location.JWTAuth = cfg.JWTAuth location.EgressMTLS = cfg.EgressMTLS + location.OIDC = cfg.OIDC location.PoliciesErrorReturn = cfg.ErrorReturn } @@ -1272,7 +1384,6 @@ func generateReturnBlock(text string, code int, defaultCode int) *version2.Retur func generateLocation(path string, upstreamName string, upstream conf_v1.Upstream, action *conf_v1.Action, cfgParams *ConfigParams, errorPages []conf_v1.ErrorPage, internal bool, errPageIndex int, proxySSLName string, originalPath string, locSnippets string, enableSnippets bool, retLocIndex int, isVSR bool, vsrName string, vsrNamespace string) (version2.Location, *version2.ReturnLocation) { - locationSnippets := generateSnippets(enableSnippets, locSnippets, cfgParams.LocationSnippets) if action.Redirect != nil { @@ -1485,7 +1596,6 @@ func generateSplits( vsrName string, vsrNamespace string, ) (version2.SplitClient, []version2.Location, []version2.ReturnLocation) { - var distributions []version2.Distribution for i, s := range splits { @@ -1539,7 +1649,6 @@ func generateDefaultSplitsConfig( vsrName string, vsrNamespace string, ) routingCfg { - sc, locs, returnLocs := generateSplits(route.Splits, upstreamNamer, crUpstreams, variableNamer, scIndex, cfgParams, errorPages, errPageIndex, originalPath, locSnippets, enableSnippets, retLocIndex, isVSR, vsrName, vsrNamespace) @@ -1797,21 +1906,23 @@ func (vsc *virtualServerConfigurator) generateSSLConfig(owner runtime.Object, tl return nil } - secret := secretRefs[fmt.Sprintf("%s/%s", namespace, tls.Secret)] - + secretRef := secretRefs[fmt.Sprintf("%s/%s", namespace, tls.Secret)] + var secretType api_v1.SecretType + if secretRef.Secret != nil { + secretType = secretRef.Secret.Type + } var name string var ciphers string - - if secret.Type != "" && secret.Type != api_v1.SecretTypeTLS { + if secretType != "" && secretType != api_v1.SecretTypeTLS { name = pemFileNameForMissingTLSSecret ciphers = "NULL" - vsc.addWarningf(owner, "TLS secret %s is of a wrong type '%s', must be '%s'", tls.Secret, secret.Type, api_v1.SecretTypeTLS) - } else if secret.Error != nil { + vsc.addWarningf(owner, "TLS secret %s is of a wrong type '%s', must be '%s'", tls.Secret, secretType, api_v1.SecretTypeTLS) + } else if secretRef.Error != nil { name = pemFileNameForMissingTLSSecret ciphers = "NULL" - vsc.addWarningf(owner, "TLS secret %s is invalid: %v", tls.Secret, secret.Error) + vsc.addWarningf(owner, "TLS secret %s is invalid: %v", tls.Secret, secretRef.Error) } else { - name = secret.Path + name = secretRef.Path } ssl := version2.SSL{ diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 85657b32e6..b20f6bc68a 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -750,6 +750,7 @@ func TestGenerateVirtualServerConfigWithSpiffeCerts(t *testing.T) { t.Errorf("GenerateVirtualServerConfig returned warnings: %v", vsc.warnings) } } + func TestGenerateVirtualServerConfigForVirtualServerWithSplits(t *testing.T) { virtualServerEx := VirtualServerEx{ VirtualServer: &conf_v1.VirtualServer{ @@ -1829,21 +1830,38 @@ func TestGeneratePolicies(t *testing.T) { tls: true, secretRefs: map[string]*secrets.SecretReference{ "default/ingress-mtls-secret": { - Type: "nginx.org/ca", + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: ingressMTLSCertPath, }, "default/egress-mtls-secret": { - Type: api_v1.SecretTypeTLS, + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, Path: "/etc/nginx/secrets/default-egress-mtls-secret", }, "default/egress-trusted-ca-secret": { - Type: "nginx.org/ca", + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "/etc/nginx/secrets/default-egress-trusted-ca-secret", }, "default/jwt-secret": { - Type: secrets.SecretTypeJWK, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-jwt-secret", }, + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": []byte("super_secret_123"), + }, + }, + Path: "/etc/nginx/secrets/default-oidc-secret", + }, }, } @@ -2123,6 +2141,37 @@ func TestGeneratePolicies(t *testing.T) { }, msg: "egressMTLS reference", }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + AuthEndpoint: "http://example.com/auth", + TokenEndpoint: "http://example.com/token", + JWKSURI: "http://example.com/jwks", + ClientID: "client-id", + ClientSecret: "oidc-secret", + Scope: "scope", + RedirectURI: "/redirect", + }, + }, + }, + }, + expected: policiesCfg{ + OIDC: createPointerFromBool(true), + }, + msg: "oidc reference", + }, } vsc := newVirtualServerConfigurator(&ConfigParams{}, false, false, &StaticConfigParams{}) @@ -2313,7 +2362,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/jwt-secret": { - Type: secrets.SecretTypeJWK, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Error: errors.New("secret is invalid"), }, }, @@ -2354,7 +2405,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/jwt-secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, }, }, }, @@ -2410,11 +2463,15 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/jwt-secret": { - Type: secrets.SecretTypeJWK, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-jwt-secret", }, "default/jwt-secret2": { - Type: secrets.SecretTypeJWK, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeJWK, + }, Path: "/etc/nginx/secrets/default-jwt-secret2", }, }, @@ -2497,7 +2554,9 @@ func TestGeneratePoliciesFails(t *testing.T) { tls: true, secretRefs: map[string]*secrets.SecretReference{ "default/ingress-mtls-secret": { - Type: api_v1.SecretTypeTLS, + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, }, }, }, @@ -2549,7 +2608,9 @@ func TestGeneratePoliciesFails(t *testing.T) { tls: true, secretRefs: map[string]*secrets.SecretReference{ "default/ingress-mtls-secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "/etc/nginx/secrets/default-ingress-mtls-secret", }, }, @@ -2593,7 +2654,9 @@ func TestGeneratePoliciesFails(t *testing.T) { tls: true, secretRefs: map[string]*secrets.SecretReference{ "default/ingress-mtls-secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "/etc/nginx/secrets/default-ingress-mtls-secret", }, }, @@ -2635,7 +2698,9 @@ func TestGeneratePoliciesFails(t *testing.T) { tls: false, secretRefs: map[string]*secrets.SecretReference{ "default/ingress-mtls-secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, Path: "/etc/nginx/secrets/default-ingress-mtls-secret", }, }, @@ -2691,7 +2756,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/egress-mtls-secret": { - Type: api_v1.SecretTypeTLS, + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, Path: "/etc/nginx/secrets/default-egress-mtls-secret", }, }, @@ -2740,7 +2807,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/egress-trusted-secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, Error: errors.New("secret is invalid"), }, }, @@ -2782,7 +2851,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/egress-mtls-secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, }, }, }, @@ -2823,7 +2894,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/egress-trusted-secret": { - Type: api_v1.SecretTypeTLS, + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, }, }, }, @@ -2864,7 +2937,9 @@ func TestGeneratePoliciesFails(t *testing.T) { policyOpts: policyOptions{ secretRefs: map[string]*secrets.SecretReference{ "default/egress-mtls-secret": { - Type: api_v1.SecretTypeTLS, + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, Error: errors.New("secret is invalid"), }, }, @@ -3359,7 +3434,6 @@ func TestGenerateReturnBlock(t *testing.T) { t.Errorf("generateReturnBlock() returned %v but expected %v", result, test.expected) } } - } func TestGenerateLocationForReturn(t *testing.T) { @@ -3558,7 +3632,9 @@ func TestGenerateSSLConfig(t *testing.T) { inputCfgParams: &ConfigParams{}, inputSecretRefs: map[string]*secrets.SecretReference{ "default/secret": { - Type: secrets.SecretTypeCA, + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeCA, + }, }, }, expectedSSL: &version2.SSL{ @@ -3578,7 +3654,9 @@ func TestGenerateSSLConfig(t *testing.T) { }, inputSecretRefs: map[string]*secrets.SecretReference{ "default/secret": { - Type: api_v1.SecretTypeTLS, + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, Path: "secret.pem", }, }, @@ -4132,7 +4210,6 @@ func TestGenerateSplits(t *testing.T) { if !reflect.DeepEqual(resultReturnLocations, expectedReturnLocations) { t.Errorf("generateSplits() returned \n%+v but expected \n%+v", resultReturnLocations, expectedReturnLocations) } - } func TestGenerateDefaultSplitsConfig(t *testing.T) { @@ -5600,7 +5677,7 @@ func TestGenerateSlowStartForPlusWithInCompatibleLBMethods(t *testing.T) { upstream := conf_v1.Upstream{Service: serviceName, Port: 80, SlowStart: "10s"} expected := "" - var tests = []string{ + tests := []string{ "random", "ip_hash", "hash 123", @@ -5622,7 +5699,6 @@ func TestGenerateSlowStartForPlusWithInCompatibleLBMethods(t *testing.T) { t.Errorf("generateSlowStartForPlus returned no warnings for %v but warnings expected", upstream) } } - } func TestGenerateSlowStartForPlus(t *testing.T) { @@ -5751,7 +5827,6 @@ func TestGenerateUpstreamWithQueue(t *testing.T) { t.Errorf("generateUpstream() returned %v but expected %v for the case of %v", result, test.expected, test.msg) } } - } func TestGenerateQueueForPlus(t *testing.T) { @@ -5783,7 +5858,6 @@ func TestGenerateQueueForPlus(t *testing.T) { t.Errorf("generateQueueForPlus() returned %v but expected %v for the case of %v", result, test.expected, test.msg) } } - } func TestGenerateSessionCookie(t *testing.T) { @@ -6090,7 +6164,6 @@ func TestIsTLSEnabled(t *testing.T) { t.Errorf("isTLSEnabled(%v, %v) returned %v but expected %v", test.upstream, test.spiffeCert, result, test.expected) } } - } func TestGenerateRewrites(t *testing.T) { @@ -6461,7 +6534,7 @@ func TestGenerateProxyAddHeaders(t *testing.T) { } func TestGetUpstreamResourceLabels(t *testing.T) { - var tests = []struct { + tests := []struct { owner runtime.Object expected version2.UpstreamLabels }{ diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index aeb3c0b3d0..f2e8d5af5c 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -1243,7 +1243,6 @@ func (lbc *LoadBalancerController) UpdateVirtualServerStatusAndEventsOnDelete(vs if lbc.reportVsVsrStatusEnabled() { err := lbc.statusUpdater.UpdateVirtualServerStatus(vsConfig.VirtualServer, state, eventTitle, msg) - if err != nil { glog.Errorf("Error when updating the status for VirtualServer %v/%v: %v", vsConfig.VirtualServer.Namespace, vsConfig.VirtualServer.Name, err) } @@ -1979,7 +1978,7 @@ func (lbc *LoadBalancerController) createIngressEx(ing *networking.Ingress, vali secretName := tls.SecretName secretKey := ing.Namespace + "/" + secretName - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) if secretRef.Error != nil { glog.Warningf("Error trying to get the secret %v for Ingress %v: %v", secretName, ing.Name, secretRef.Error) } @@ -1992,7 +1991,7 @@ func (lbc *LoadBalancerController) createIngressEx(ing *networking.Ingress, vali secretName := jwtKey secretKey := ing.Namespace + "/" + secretName - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) if secretRef.Error != nil { glog.Warningf("Error trying to get the secret %v for Ingress %v/%v: %v", secretName, ing.Namespace, ing.Name, secretRef.Error) } @@ -2168,7 +2167,7 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. if virtualServer.Spec.TLS != nil && virtualServer.Spec.TLS.Secret != "" { secretKey := virtualServer.Namespace + "/" + virtualServer.Spec.TLS.Secret - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) if secretRef.Error != nil { glog.Warningf("Error trying to get the secret %v for VirtualServer %v: %v", secretKey, virtualServer.Name, secretRef.Error) } @@ -2196,6 +2195,11 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. glog.Warningf("Error getting EgressMTLS secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) } + err = lbc.addOIDCSecretRefs(virtualServerEx.SecretRefs, policies) + if err != nil { + glog.Warningf("Error getting OIDC secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) + } + endpoints := make(map[string][]string) externalNameSvcs := make(map[string]bool) podsByIP := make(map[string]configs.PodInfo) @@ -2241,7 +2245,7 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. } policies = append(policies, vsRoutePolicies...) - err = lbc.addEgressMTLSSecretRefs(virtualServerEx.SecretRefs, policies) + err = lbc.addEgressMTLSSecretRefs(virtualServerEx.SecretRefs, vsRoutePolicies) if err != nil { glog.Warningf("Error getting EgressMTLS secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) } @@ -2249,6 +2253,10 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. if err != nil { glog.Warningf("Error getting JWT secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) } + err = lbc.addOIDCSecretRefs(virtualServerEx.SecretRefs, vsRoutePolicies) + if err != nil { + glog.Warningf("Error getting OIDC secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) + } } for _, vsr := range virtualServerRoutes { @@ -2264,9 +2272,14 @@ func (lbc *LoadBalancerController) createVirtualServerEx(virtualServer *conf_v1. glog.Warningf("Error getting JWT secrets for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) } - err = lbc.addEgressMTLSSecretRefs(virtualServerEx.SecretRefs, policies) + err = lbc.addEgressMTLSSecretRefs(virtualServerEx.SecretRefs, vsrSubroutePolicies) if err != nil { - glog.Warningf("Error getting EgressMTLS secrets for VirtualServer %v/%v: %v", virtualServer.Namespace, virtualServer.Name, err) + glog.Warningf("Error getting EgressMTLS secrets for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) + } + + err = lbc.addOIDCSecretRefs(virtualServerEx.SecretRefs, vsrSubroutePolicies) + if err != nil { + glog.Warningf("Error getting OIDC secrets for VirtualServerRoute %v/%v: %v", vsr.Namespace, vsr.Name, err) } } @@ -2385,7 +2398,7 @@ func (lbc *LoadBalancerController) addJWTSecretRefs(secretRefs map[string]*secre } secretKey := fmt.Sprintf("%v/%v", pol.Namespace, pol.Spec.JWTAuth.Secret) - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) secretRefs[secretKey] = secretRef @@ -2404,7 +2417,7 @@ func (lbc *LoadBalancerController) addIngressMTLSSecretRefs(secretRefs map[strin } secretKey := fmt.Sprintf("%v/%v", pol.Namespace, pol.Spec.IngressMTLS.ClientCertSecret) - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) secretRefs[secretKey] = secretRef @@ -2421,7 +2434,7 @@ func (lbc *LoadBalancerController) addEgressMTLSSecretRefs(secretRefs map[string } if pol.Spec.EgressMTLS.TLSSecret != "" { secretKey := fmt.Sprintf("%v/%v", pol.Namespace, pol.Spec.EgressMTLS.TLSSecret) - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) secretRefs[secretKey] = secretRef @@ -2431,7 +2444,7 @@ func (lbc *LoadBalancerController) addEgressMTLSSecretRefs(secretRefs map[string } if pol.Spec.EgressMTLS.TrustedCertSecret != "" { secretKey := fmt.Sprintf("%v/%v", pol.Namespace, pol.Spec.EgressMTLS.TrustedCertSecret) - secretRef := lbc.secretStore.GetSecretReference(secretKey) + secretRef := lbc.secretStore.GetSecret(secretKey) secretRefs[secretKey] = secretRef @@ -2444,6 +2457,25 @@ func (lbc *LoadBalancerController) addEgressMTLSSecretRefs(secretRefs map[string return nil } +func (lbc *LoadBalancerController) addOIDCSecretRefs(secretRefs map[string]*secrets.SecretReference, policies []*conf_v1.Policy) error { + for _, pol := range policies { + if pol.Spec.OIDC == nil { + continue + } + + secretKey := fmt.Sprintf("%v/%v", pol.Namespace, pol.Spec.OIDC.ClientSecret) + secretRef := lbc.secretStore.GetSecret(secretKey) + + secretRefs[secretKey] = secretRef + + if secretRef.Error != nil { + return secretRef.Error + } + } + + return nil +} + func (lbc *LoadBalancerController) getPoliciesForSecret(secretNamespace string, secretName string) []*conf_v1.Policy { return findPoliciesForSecret(lbc.getAllPolicies(), secretNamespace, secretName) } @@ -2894,7 +2926,6 @@ func (lbc *LoadBalancerController) syncAppProtectPolicy(task task) { lbc.processAppProtectChanges(changes) lbc.processAppProtectProblems(problems) - } func (lbc *LoadBalancerController) syncAppProtectLogConf(task task) { @@ -2921,7 +2952,6 @@ func (lbc *LoadBalancerController) syncAppProtectLogConf(task task) { lbc.processAppProtectChanges(changes) lbc.processAppProtectProblems(problems) - } func (lbc *LoadBalancerController) syncAppProtectUserSig(task task) { diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go index e22f1f0f58..225b64f8b5 100644 --- a/internal/k8s/controller_test.go +++ b/internal/k8s/controller_test.go @@ -31,7 +31,7 @@ func TestHasCorrectIngressClass(t *testing.T) { incorrectIngressClass := "gce" emptyClass := "" - var testsWithoutIngressClassOnly = []struct { + testsWithoutIngressClassOnly := []struct { lbc *LoadBalancerController ing *networking.Ingress expected bool @@ -90,7 +90,7 @@ func TestHasCorrectIngressClass(t *testing.T) { }, } - var testsWithIngressClassOnly = []struct { + testsWithIngressClassOnly := []struct { lbc *LoadBalancerController ing *networking.Ingress expected bool @@ -225,7 +225,6 @@ func TestHasCorrectIngressClass(t *testing.T) { test.lbc.ingressClass, test.lbc.useIngressClassOnly, ingressClassKey, classAnnotation, result, test.expected) } } - } func TestHasCorrectIngressClassVS(t *testing.T) { @@ -236,7 +235,7 @@ func TestHasCorrectIngressClassVS(t *testing.T) { metricsCollector: collectors.NewControllerFakeCollector(), } - var testsWithIngressClassOnlyVS = []struct { + testsWithIngressClassOnlyVS := []struct { lbc *LoadBalancerController ing *conf_v1.VirtualServer expected bool @@ -280,7 +279,7 @@ func TestHasCorrectIngressClassVS(t *testing.T) { useIngressClassOnly: false, metricsCollector: collectors.NewControllerFakeCollector(), } - var testsWithoutIngressClassOnlyVS = []struct { + testsWithoutIngressClassOnlyVS := []struct { lbc *LoadBalancerController ing *conf_v1.VirtualServer expected bool @@ -515,7 +514,6 @@ func TestFindProbeForPods(t *testing.T) { if probe != nil { t.Errorf("ServicePort.Port mismatch: %+v", probe) } - } func TestGetServicePortForIngressPort(t *testing.T) { @@ -828,7 +826,7 @@ func TestGetPolicies(t *testing.T) { expectedPolicies := []*conf_v1.Policy{validPolicy} expectedErrors := []error{ - errors.New("Policy default/invalid-policy is invalid: spec: Invalid value: \"\": must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`, `jwt`"), + errors.New("Policy default/invalid-policy is invalid: spec: Invalid value: \"\": must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`, `jwt`, `oidc`"), errors.New("Policy nginx-ingress/valid-policy doesn't exist"), errors.New("Failed to get policy nginx-ingress/some-policy: GetByKey error"), } @@ -1226,6 +1224,20 @@ func errorComparer(e1, e2 error) bool { func TestAddJWTSecrets(t *testing.T) { invalidErr := errors.New("invalid") + validJWKSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "valid-jwk-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeJWK, + } + invalidJWKSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "invalid-jwk-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeJWK, + } tests := []struct { policies []*conf_v1.Policy @@ -1250,8 +1262,8 @@ func TestAddJWTSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/valid-jwk-secret": { - Type: secrets.SecretTypeJWK, - Path: "/etc/nginx/secrets/default-valid-jwk-secret", + Secret: validJWKSecret, + Path: "/etc/nginx/secrets/default-valid-jwk-secret", }, }, wantErr: false, @@ -1298,8 +1310,8 @@ func TestAddJWTSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/invalid-jwk-secret": { - Type: secrets.SecretTypeJWK, - Error: invalidErr, + Secret: invalidJWKSecret, + Error: invalidErr, }, }, wantErr: true, @@ -1308,26 +1320,14 @@ func TestAddJWTSecrets(t *testing.T) { } lbc := LoadBalancerController{ - secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.StoredSecret{ + secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.SecretReference{ "default/valid-jwk-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "valid-jwk-secret", - Namespace: "default", - }, - Type: secrets.SecretTypeJWK, - }, - Path: "/etc/nginx/secrets/default-valid-jwk-secret", + Secret: validJWKSecret, + Path: "/etc/nginx/secrets/default-valid-jwk-secret", }, "default/invalid-jwk-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "invalid-jwk-secret", - Namespace: "default", - }, - Type: secrets.SecretTypeJWK, - }, - ValidationErr: invalidErr, + Secret: invalidJWKSecret, + Error: invalidErr, }, }), } @@ -1348,6 +1348,20 @@ func TestAddJWTSecrets(t *testing.T) { func TestAddIngressMTLSSecret(t *testing.T) { invalidErr := errors.New("invalid") + validSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "valid-ingress-mtls-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeCA, + } + invalidSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "invalid-ingress-mtls-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeCA, + } tests := []struct { policies []*conf_v1.Policy @@ -1371,8 +1385,8 @@ func TestAddIngressMTLSSecret(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/valid-ingress-mtls-secret": { - Type: secrets.SecretTypeCA, - Path: "/etc/nginx/secrets/default-valid-ingress-mtls-secret", + Secret: validSecret, + Path: "/etc/nginx/secrets/default-valid-ingress-mtls-secret", }, }, wantErr: false, @@ -1418,8 +1432,8 @@ func TestAddIngressMTLSSecret(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/invalid-ingress-mtls-secret": { - Type: secrets.SecretTypeCA, - Error: invalidErr, + Secret: invalidSecret, + Error: invalidErr, }, }, wantErr: true, @@ -1428,26 +1442,14 @@ func TestAddIngressMTLSSecret(t *testing.T) { } lbc := LoadBalancerController{ - secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.StoredSecret{ + secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.SecretReference{ "default/valid-ingress-mtls-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "valid-ingress-mtls-secret", - Namespace: "default", - }, - Type: secrets.SecretTypeCA, - }, - Path: "/etc/nginx/secrets/default-valid-ingress-mtls-secret", + Secret: validSecret, + Path: "/etc/nginx/secrets/default-valid-ingress-mtls-secret", }, "default/invalid-ingress-mtls-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "invalid-ingress-mtls-secret", - Namespace: "default", - }, - Type: secrets.SecretTypeCA, - }, - ValidationErr: invalidErr, + Secret: invalidSecret, + Error: invalidErr, }, }), } @@ -1468,6 +1470,34 @@ func TestAddIngressMTLSSecret(t *testing.T) { func TestAddEgressMTLSSecrets(t *testing.T) { invalidErr := errors.New("invalid") + validMTLSSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "valid-egress-mtls-secret", + Namespace: "default", + }, + Type: api_v1.SecretTypeTLS, + } + validTrustedSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "valid-egress-trusted-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeCA, + } + invalidMTLSSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "invalid-egress-mtls-secret", + Namespace: "default", + }, + Type: api_v1.SecretTypeTLS, + } + invalidTrustedSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "invalid-egress-trusted-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeCA, + } tests := []struct { policies []*conf_v1.Policy @@ -1491,8 +1521,8 @@ func TestAddEgressMTLSSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/valid-egress-mtls-secret": { - Type: api_v1.SecretTypeTLS, - Path: "/etc/nginx/secrets/default-valid-egress-mtls-secret", + Secret: validMTLSSecret, + Path: "/etc/nginx/secrets/default-valid-egress-mtls-secret", }, }, wantErr: false, @@ -1514,8 +1544,8 @@ func TestAddEgressMTLSSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/valid-egress-trusted-secret": { - Type: secrets.SecretTypeCA, - Path: "/etc/nginx/secrets/default-valid-egress-trusted-secret", + Secret: validTrustedSecret, + Path: "/etc/nginx/secrets/default-valid-egress-trusted-secret", }, }, wantErr: false, @@ -1538,12 +1568,12 @@ func TestAddEgressMTLSSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/valid-egress-mtls-secret": { - Type: api_v1.SecretTypeTLS, - Path: "/etc/nginx/secrets/default-valid-egress-mtls-secret", + Secret: validMTLSSecret, + Path: "/etc/nginx/secrets/default-valid-egress-mtls-secret", }, "default/valid-egress-trusted-secret": { - Type: secrets.SecretTypeCA, - Path: "/etc/nginx/secrets/default-valid-egress-trusted-secret", + Secret: validTrustedSecret, + Path: "/etc/nginx/secrets/default-valid-egress-trusted-secret", }, }, wantErr: false, @@ -1589,8 +1619,8 @@ func TestAddEgressMTLSSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/invalid-egress-mtls-secret": { - Type: api_v1.SecretTypeTLS, - Error: invalidErr, + Secret: invalidMTLSSecret, + Error: invalidErr, }, }, wantErr: true, @@ -1612,8 +1642,8 @@ func TestAddEgressMTLSSecrets(t *testing.T) { }, expectedSecretRefs: map[string]*secrets.SecretReference{ "default/invalid-egress-trusted-secret": { - Type: secrets.SecretTypeCA, - Error: invalidErr, + Secret: invalidTrustedSecret, + Error: invalidErr, }, }, wantErr: true, @@ -1622,46 +1652,22 @@ func TestAddEgressMTLSSecrets(t *testing.T) { } lbc := LoadBalancerController{ - secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.StoredSecret{ + secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.SecretReference{ "default/valid-egress-mtls-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "valid-egress-mtls-secret", - Namespace: "default", - }, - Type: api_v1.SecretTypeTLS, - }, - Path: "/etc/nginx/secrets/default-valid-egress-mtls-secret", + Secret: validMTLSSecret, + Path: "/etc/nginx/secrets/default-valid-egress-mtls-secret", }, "default/valid-egress-trusted-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "valid-egress-trusted-secret", - Namespace: "default", - }, - Type: secrets.SecretTypeCA, - }, - Path: "/etc/nginx/secrets/default-valid-egress-trusted-secret", + Secret: validTrustedSecret, + Path: "/etc/nginx/secrets/default-valid-egress-trusted-secret", }, "default/invalid-egress-mtls-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "invalid-egress-mtls-secret", - Namespace: "default", - }, - Type: api_v1.SecretTypeTLS, - }, - ValidationErr: invalidErr, + Secret: invalidMTLSSecret, + Error: invalidErr, }, "default/invalid-egress-trusted-secret": { - Secret: &v1.Secret{ - ObjectMeta: meta_v1.ObjectMeta{ - Name: "invalid-egress-trusted-secret", - Namespace: "default", - }, - Type: secrets.SecretTypeCA, - }, - ValidationErr: invalidErr, + Secret: invalidTrustedSecret, + Error: invalidErr, }, }), } diff --git a/internal/k8s/secrets/store.go b/internal/k8s/secrets/store.go index 13cadb085e..b8348404c5 100644 --- a/internal/k8s/secrets/store.go +++ b/internal/k8s/secrets/store.go @@ -7,18 +7,11 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -// StoredSecret holds a secret, its validation status and the path on the file system. -type StoredSecret struct { - Secret *api_v1.Secret - Path string - ValidationErr error -} - // SecretReference holds a reference to a secret stored on the file system. type SecretReference struct { - Type api_v1.SecretType - Path string - Error error + Secret *api_v1.Secret + Path string + Error error } // SecretFileManager manages secrets on the file system. @@ -31,20 +24,20 @@ type SecretFileManager interface { type SecretStore interface { AddOrUpdateSecret(secret *api_v1.Secret) DeleteSecret(key string) - GetSecretReference(key string) *SecretReference + GetSecret(key string) *SecretReference } // LocalSecretStore implements SecretStore interface. // It validates the secrets and manages them on the file system (via SecretFileManager). type LocalSecretStore struct { - secrets map[string]*StoredSecret + secrets map[string]*SecretReference manager SecretFileManager } // NewLocalSecretStore creates a new LocalSecretStore. func NewLocalSecretStore(manager SecretFileManager) *LocalSecretStore { return &LocalSecretStore{ - secrets: make(map[string]*StoredSecret), + secrets: make(map[string]*SecretReference), manager: manager, } } @@ -53,27 +46,25 @@ func NewLocalSecretStore(manager SecretFileManager) *LocalSecretStore { // The secret will only be updated on the file system if it is valid and if it is already on the file system. // If the secret becomes invalid, it will be removed from the filesystem. func (s *LocalSecretStore) AddOrUpdateSecret(secret *api_v1.Secret) { - storedSecret, exists := s.secrets[getResourceKey(&secret.ObjectMeta)] + secretRef, exists := s.secrets[getResourceKey(&secret.ObjectMeta)] if !exists { - storedSecret = &StoredSecret{ - Secret: secret, - } + secretRef = &SecretReference{Secret: secret} } else { - storedSecret.Secret = secret + secretRef.Secret = secret } - storedSecret.ValidationErr = ValidateSecret(secret) + secretRef.Error = ValidateSecret(secret) - if storedSecret.Path != "" { - if storedSecret.ValidationErr != nil { + if secretRef.Path != "" { + if secretRef.Error != nil { s.manager.DeleteSecret(getResourceKey(&secret.ObjectMeta)) - storedSecret.Path = "" + secretRef.Path = "" } else { - storedSecret.Path = s.manager.AddOrUpdateSecret(secret) + secretRef.Path = s.manager.AddOrUpdateSecret(secret) } } - s.secrets[getResourceKey(&secret.ObjectMeta)] = storedSecret + s.secrets[getResourceKey(&secret.ObjectMeta)] = secretRef } // DeleteSecret deletes a secret. @@ -92,26 +83,22 @@ func (s *LocalSecretStore) DeleteSecret(key string) { s.manager.DeleteSecret(key) } -// GetSecretReference returns a SecretReference. +// GetSecret returns a SecretReference. // If the secret doesn't exist, is of an unsupported type, or invalid, the Error field will include an error. // If the secret is valid but isn't present on the file system, the secret will be written to the file system. -func (s *LocalSecretStore) GetSecretReference(key string) *SecretReference { - storedSecret, exists := s.secrets[key] +func (s *LocalSecretStore) GetSecret(key string) *SecretReference { + secretRef, exists := s.secrets[key] if !exists { return &SecretReference{ Error: fmt.Errorf("secret doesn't exist or of an unsupported type"), } } - if storedSecret.ValidationErr == nil && storedSecret.Path == "" { - storedSecret.Path = s.manager.AddOrUpdateSecret(storedSecret.Secret) + if secretRef.Error == nil && secretRef.Path == "" { + secretRef.Path = s.manager.AddOrUpdateSecret(secretRef.Secret) } - return &SecretReference{ - Type: storedSecret.Secret.Type, - Path: storedSecret.Path, - Error: storedSecret.ValidationErr, - } + return secretRef } func getResourceKey(meta *metav1.ObjectMeta) string { @@ -120,11 +107,11 @@ func getResourceKey(meta *metav1.ObjectMeta) string { // FakeSecretStore is a fake implementation of SecretStore. type FakeSecretStore struct { - secrets map[string]*StoredSecret + secrets map[string]*SecretReference } // NewFakeSecretsStore creates a new FakeSecretStore. -func NewFakeSecretsStore(secrets map[string]*StoredSecret) SecretStore { +func NewFakeSecretsStore(secrets map[string]*SecretReference) SecretStore { return &FakeSecretStore{ secrets: secrets, } @@ -138,18 +125,14 @@ func (s *FakeSecretStore) AddOrUpdateSecret(secret *api_v1.Secret) { func (s *FakeSecretStore) DeleteSecret(key string) { } -// GetSecretReference is a fake implementation of GetSecretReference. -func (s *FakeSecretStore) GetSecretReference(key string) *SecretReference { - storedSecret, exists := s.secrets[key] +// GetSecret is a fake implementation of GetSecret. +func (s *FakeSecretStore) GetSecret(key string) *SecretReference { + secretRef, exists := s.secrets[key] if !exists { return &SecretReference{ Error: fmt.Errorf("secret doesn't exist"), } } - return &SecretReference{ - Type: storedSecret.Secret.Type, - Path: storedSecret.Path, - Error: storedSecret.ValidationErr, - } + return secretRef } diff --git a/internal/k8s/secrets/store_test.go b/internal/k8s/secrets/store_test.go index c867bf6236..21a835c5d9 100644 --- a/internal/k8s/secrets/store_test.go +++ b/internal/k8s/secrets/store_test.go @@ -79,22 +79,22 @@ func TestAddOrUpdateSecret(t *testing.T) { // Get the secret expectedSecretRef := &SecretReference{ - Type: api_v1.SecretTypeTLS, - Path: "testpath", - Error: nil, + Secret: validSecret, + Path: "testpath", + Error: nil, } expectedManager = &fakeSecretFileManager{ AddedOrUpdatedSecret: validSecret, } manager.Reset() - secretRef := store.GetSecretReference("default/tls-secret") + secretRef := store.GetSecret("default/tls-secret") if diff := cmp.Diff(expectedSecretRef, secretRef, cmp.Comparer(errorComparer)); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } if diff := cmp.Diff(expectedManager, manager); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } // Make the secret invalid @@ -113,20 +113,20 @@ func TestAddOrUpdateSecret(t *testing.T) { // Get the secret expectedSecretRef = &SecretReference{ - Type: api_v1.SecretTypeTLS, - Path: "", - Error: errors.New("Failed to validate TLS cert and key: asn1: syntax error: sequence truncated"), + Secret: invalidSecret, + Path: "", + Error: errors.New("Failed to validate TLS cert and key: asn1: syntax error: sequence truncated"), } expectedManager = &fakeSecretFileManager{} manager.Reset() - secretRef = store.GetSecretReference("default/tls-secret") + secretRef = store.GetSecret("default/tls-secret") if diff := cmp.Diff(expectedSecretRef, secretRef, cmp.Comparer(errorComparer)); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } if diff := cmp.Diff(expectedManager, manager); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } // Restore the valid secret @@ -143,22 +143,22 @@ func TestAddOrUpdateSecret(t *testing.T) { // Get the secret expectedSecretRef = &SecretReference{ - Type: api_v1.SecretTypeTLS, - Path: "testpath", - Error: nil, + Secret: validSecret, + Path: "testpath", + Error: nil, } expectedManager = &fakeSecretFileManager{ AddedOrUpdatedSecret: validSecret, } manager.Reset() - secretRef = store.GetSecretReference("default/tls-secret") + secretRef = store.GetSecret("default/tls-secret") if diff := cmp.Diff(expectedSecretRef, secretRef, cmp.Comparer(errorComparer)); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } if diff := cmp.Diff(expectedManager, manager); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } // Update the secret @@ -178,20 +178,20 @@ func TestAddOrUpdateSecret(t *testing.T) { // Get the secret expectedSecretRef = &SecretReference{ - Type: api_v1.SecretTypeTLS, - Path: "testpath", - Error: nil, + Secret: validSecret, + Path: "testpath", + Error: nil, } expectedManager = &fakeSecretFileManager{} manager.Reset() - secretRef = store.GetSecretReference("default/tls-secret") + secretRef = store.GetSecret("default/tls-secret") if diff := cmp.Diff(expectedSecretRef, secretRef, cmp.Comparer(errorComparer)); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } if diff := cmp.Diff(expectedManager, manager); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } } @@ -225,22 +225,22 @@ func TestDeleteSecretValidSecret(t *testing.T) { // Get the secret expectedSecretRef := &SecretReference{ - Type: api_v1.SecretTypeTLS, - Path: "testpath", - Error: nil, + Secret: validSecret, + Path: "testpath", + Error: nil, } expectedManager = &fakeSecretFileManager{ AddedOrUpdatedSecret: validSecret, } manager.Reset() - secretRef := store.GetSecretReference("default/tls-secret") + secretRef := store.GetSecret("default/tls-secret") if diff := cmp.Diff(expectedSecretRef, secretRef, cmp.Comparer(errorComparer)); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } if diff := cmp.Diff(expectedManager, manager); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } // Delete the secret @@ -259,20 +259,18 @@ func TestDeleteSecretValidSecret(t *testing.T) { // Get the secret expectedSecretRef = &SecretReference{ - Type: "", - Path: "", Error: errors.New("secret doesn't exist or of an unsupported type"), } expectedManager = &fakeSecretFileManager{} manager.Reset() - secretRef = store.GetSecretReference("default/tls-secret") + secretRef = store.GetSecret("default/tls-secret") if diff := cmp.Diff(expectedSecretRef, secretRef, cmp.Comparer(errorComparer)); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } if diff := cmp.Diff(expectedManager, manager); diff != "" { - t.Errorf("GetSecretReference() returned unexpected result (-want +got):\n%s", diff) + t.Errorf("GetSecret() returned unexpected result (-want +got):\n%s", diff) } } diff --git a/internal/k8s/secrets/validation.go b/internal/k8s/secrets/validation.go index fea22d6156..716a30a8f2 100644 --- a/internal/k8s/secrets/validation.go +++ b/internal/k8s/secrets/validation.go @@ -5,6 +5,7 @@ import ( "crypto/x509" "encoding/pem" "fmt" + "regexp" api_v1 "k8s.io/api/core/v1" ) @@ -15,12 +16,18 @@ const JWTKeyKey = "jwk" // CAKey is the key of the data field of a Secret where the certificate authority must be stored. const CAKey = "ca.crt" +// ClientSecretKey is the key of the data field of a Secret where the OIDC client secret must be stored. +const ClientSecretKey = "client-secret" + // SecretTypeCA contains a certificate authority for TLS certificate verification. const SecretTypeCA api_v1.SecretType = "nginx.org/ca" // SecretTypeJWK contains a JWK (JSON Web Key) for validating JWTs (JSON Web Tokens). const SecretTypeJWK api_v1.SecretType = "nginx.org/jwk" +// SecretTypeOIDC contains an OIDC client secret for use in oauth flows. +const SecretTypeOIDC api_v1.SecretType = "nginx.org/oidc" + // ValidateTLSSecret validates the secret. If it is valid, the function returns nil. func ValidateTLSSecret(secret *api_v1.Secret) error { if secret.Type != api_v1.SecretTypeTLS { @@ -79,9 +86,29 @@ func ValidateCASecret(secret *api_v1.Secret) error { return nil } +// ValidateOIDCSecret validates the secret. If it is valid, the function returns nil. +func ValidateOIDCSecret(secret *api_v1.Secret) error { + if secret.Type != SecretTypeOIDC { + return fmt.Errorf("OIDC secret must be of the type %v", SecretTypeOIDC) + } + + clientSecret, exists := secret.Data[ClientSecretKey] + if !exists { + return fmt.Errorf("OIDC secret must have the data field %v", ClientSecretKey) + } + + if msg, ok := isValidClientSecretValue(string(clientSecret)); !ok { + return fmt.Errorf("OIDC client secret %v is invalid: %s", clientSecret, msg) + } + return nil +} + // IsSupportedSecretType checks if the secret type is supported. func IsSupportedSecretType(secretType api_v1.SecretType) bool { - return secretType == api_v1.SecretTypeTLS || secretType == SecretTypeCA || secretType == SecretTypeJWK + return secretType == api_v1.SecretTypeTLS || + secretType == SecretTypeCA || + secretType == SecretTypeJWK || + secretType == SecretTypeOIDC } // ValidateSecret validates the secret. If it is valid, the function returns nil. @@ -93,7 +120,18 @@ func ValidateSecret(secret *api_v1.Secret) error { return ValidateJWKSecret(secret) case SecretTypeCA: return ValidateCASecret(secret) + case SecretTypeOIDC: + return ValidateOIDCSecret(secret) } return fmt.Errorf("Secret is of the unsupported type %v", secret.Type) } + +var clientSecretValueFmtRegexp = regexp.MustCompile(`^([^"$\\]|\\[^$])*$`) + +func isValidClientSecretValue(s string) (string, bool) { + if ok := clientSecretValueFmtRegexp.MatchString(s); !ok { + return `It must contain valid ASCII characters, must have all '"' escaped and must not contain any '$' or end with an unescaped '\'`, false + } + return "", true +} diff --git a/internal/k8s/secrets/validation_test.go b/internal/k8s/secrets/validation_test.go index 3e4a3440a9..5f0f5f4534 100644 --- a/internal/k8s/secrets/validation_test.go +++ b/internal/k8s/secrets/validation_test.go @@ -230,6 +230,75 @@ func TestValidateTLSSecretFails(t *testing.T) { } } +func TestValidateOIDCSecret(t *testing.T) { + secret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-secret", + Namespace: "default", + }, + Type: SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": nil, + }, + } + + err := ValidateOIDCSecret(secret) + if err != nil { + t.Errorf("ValidateOIDCSecret() returned error %v", err) + } +} + +func TestValidateOIDCSecretFails(t *testing.T) { + tests := []struct { + secret *v1.Secret + msg string + }{ + { + secret: &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-secret", + Namespace: "default", + }, + Type: "some-type", + Data: map[string][]byte{ + "client-secret": nil, + }, + }, + msg: "Incorrect type for OIDC secret", + }, + { + secret: &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-secret", + Namespace: "default", + }, + Type: SecretTypeOIDC, + }, + msg: "Missing client-secret for OIDC secret", + }, + { + secret: &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-secret", + Namespace: "default", + }, + Type: SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": []byte("hello$$$"), + }, + }, + msg: "Invalid characters in OIDC client secret", + }, + } + + for _, test := range tests { + err := ValidateOIDCSecret(test.secret) + if err == nil { + t.Errorf("ValidateOIDCSecret() returned no error for the case of %s", test.msg) + } + } +} + func TestValidateSecret(t *testing.T) { tests := []struct { secret *v1.Secret @@ -261,7 +330,8 @@ func TestValidateSecret(t *testing.T) { }, }, msg: "Valid CA secret", - }, { + }, + { secret: &v1.Secret{ ObjectMeta: meta_v1.ObjectMeta{ Name: "jwk-secret", @@ -274,6 +344,19 @@ func TestValidateSecret(t *testing.T) { }, msg: "Valid JWK secret", }, + { + secret: &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-secret", + Namespace: "default", + }, + Type: SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": nil, + }, + }, + msg: "Valid OIDC secret", + }, } for _, test := range tests { @@ -348,6 +431,10 @@ func TestHasCorrectSecretType(t *testing.T) { secretType: SecretTypeJWK, expected: true, }, + { + secretType: SecretTypeOIDC, + expected: true, + }, { secretType: "some-type", expected: false, diff --git a/internal/nginx/manager.go b/internal/nginx/manager.go index 953768e748..16f4436546 100644 --- a/internal/nginx/manager.go +++ b/internal/nginx/manager.go @@ -28,6 +28,9 @@ const TLSSecretFileMode = 0600 // JWKSecretFileMode defines the default filemode for files with JWK Secrets. const JWKSecretFileMode = 0644 +// OIDCSecretFileMode defines the default filemode for files with OIDC Secrets. +const OIDCSecretFileMode = 0644 + const configFileMode = 0644 const jsonFileForOpenTracingTracer = "/var/lib/nginx/tracer-config.json" diff --git a/pkg/apis/configuration/v1/types.go b/pkg/apis/configuration/v1/types.go index 52e3f3b8c2..dcbb766b4d 100644 --- a/pkg/apis/configuration/v1/types.go +++ b/pkg/apis/configuration/v1/types.go @@ -336,6 +336,7 @@ type PolicySpec struct { JWTAuth *JWTAuth `json:"jwt"` IngressMTLS *IngressMTLS `json:"ingressMTLS"` EgressMTLS *EgressMTLS `json:"egressMTLS"` + OIDC *OIDC `json:"oidc"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object @@ -398,3 +399,14 @@ type EgressMTLS struct { ServerName bool `json:"serverName"` SSLName string `json:"sslName"` } + +// OIDC defines an Open ID Connect policy. +type OIDC struct { + AuthEndpoint string `json:"authEndpoint"` + TokenEndpoint string `json:"tokenEndpoint"` + JWKSURI string `json:"jwksURI"` + ClientID string `json:"clientID"` + ClientSecret string `json:"clientSecret"` + Scope string `json:"scope"` + RedirectURI string `json:"redirectURI"` +} diff --git a/pkg/apis/configuration/v1/zz_generated.deepcopy.go b/pkg/apis/configuration/v1/zz_generated.deepcopy.go index cad8de5695..604ded5b0c 100644 --- a/pkg/apis/configuration/v1/zz_generated.deepcopy.go +++ b/pkg/apis/configuration/v1/zz_generated.deepcopy.go @@ -380,6 +380,22 @@ func (in *Match) DeepCopy() *Match { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *OIDC) DeepCopyInto(out *OIDC) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new OIDC. +func (in *OIDC) DeepCopy() *OIDC { + if in == nil { + return nil + } + out := new(OIDC) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Policy) DeepCopyInto(out *Policy) { *out = *in @@ -484,6 +500,11 @@ func (in *PolicySpec) DeepCopyInto(out *PolicySpec) { *out = new(EgressMTLS) (*in).DeepCopyInto(*out) } + if in.OIDC != nil { + in, out := &in.OIDC, &out.OIDC + *out = new(OIDC) + **out = **in + } return } diff --git a/pkg/apis/configuration/validation/policy.go b/pkg/apis/configuration/validation/policy.go index acd87cad7a..11ff285d8e 100644 --- a/pkg/apis/configuration/validation/policy.go +++ b/pkg/apis/configuration/validation/policy.go @@ -3,6 +3,7 @@ package validation import ( "fmt" "net" + "net/url" "regexp" "strconv" "strings" @@ -68,10 +69,23 @@ func validatePolicySpec(spec *v1.PolicySpec, fieldPath *field.Path, isPlus bool, fieldCount++ } + if spec.OIDC != nil { + if !enablePreviewPolicies { + allErrs = append(allErrs, field.Forbidden(fieldPath.Child("oidc"), + "oidc is a preview policy. Preview policies must be enabled to use via cli argument -enable-preview-policies")) + } + if !isPlus { + return append(allErrs, field.Forbidden(fieldPath.Child("oidc"), "OIDC is only supported in NGINX Plus")) + } + + allErrs = append(allErrs, validateOIDC(spec.OIDC, fieldPath.Child("oidc"))...) + fieldCount++ + } + if fieldCount != 1 { msg := "must specify exactly one of: `accessControl`, `rateLimit`, `ingressMTLS`, `egressMTLS`" if isPlus { - msg = fmt.Sprint(msg, ", `jwt`") + msg = fmt.Sprint(msg, ", `jwt`, `oidc`") } allErrs = append(allErrs, field.Invalid(fieldPath, "", msg)) @@ -185,6 +199,128 @@ func validateEgressMTLS(egressMTLS *v1.EgressMTLS, fieldPath *field.Path) field. return allErrs } +func validateOIDC(oidc *v1.OIDC, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if oidc.AuthEndpoint == "" { + return append(allErrs, field.Required(fieldPath.Child("authEndpoint"), "")) + } + if oidc.TokenEndpoint == "" { + return append(allErrs, field.Required(fieldPath.Child("tokenEndpoint"), "")) + } + if oidc.JWKSURI == "" { + return append(allErrs, field.Required(fieldPath.Child("jwksURI"), "")) + } + if oidc.ClientID == "" { + return append(allErrs, field.Required(fieldPath.Child("clientID"), "")) + } + if oidc.ClientSecret == "" { + return append(allErrs, field.Required(fieldPath.Child("clientSecret"), "")) + } + + if oidc.Scope != "" { + allErrs = append(allErrs, validateOIDCScope(oidc.Scope, fieldPath.Child("scope"))...) + } + + if oidc.RedirectURI != "" { + allErrs = append(allErrs, validatePath(oidc.RedirectURI, fieldPath.Child("redirectURI"))...) + } + + allErrs = append(allErrs, validateURL(oidc.AuthEndpoint, fieldPath.Child("authEndpoint"))...) + allErrs = append(allErrs, validateURL(oidc.TokenEndpoint, fieldPath.Child("tokenEndpoint"))...) + allErrs = append(allErrs, validateURL(oidc.JWKSURI, fieldPath.Child("jwksURI"))...) + allErrs = append(allErrs, validateSecretName(oidc.ClientSecret, fieldPath.Child("clientSecret"))...) + allErrs = append(allErrs, validateClientID(oidc.ClientID, fieldPath.Child("clientID"))...) + + return allErrs +} + +func validateClientID(client string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // isValidHeaderValue checks for $ and " in the string + if isValidHeaderValue(client) != nil { + allErrs = append(allErrs, field.Invalid( + fieldPath, + client, + `invalid string. String must contain valid ASCII characters, must have all '"' escaped and must not contain any '$' or end with an unescaped '\' + `)) + } + + return allErrs +} + +var validScopes = map[string]bool{ + "openid": true, + "profile": true, + "email": true, + "address": true, + "phone": true, +} + +// https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims +func validateOIDCScope(scope string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + if !strings.Contains(scope, "openid") { + return append(allErrs, field.Required(fieldPath, "openid scope")) + } + + s := strings.Split(scope, "+") + for _, v := range s { + if !validScopes[v] { + msg := fmt.Sprintf("invalid Scope. Accepted scopes are: %v", mapToPrettyString(validScopes)) + allErrs = append(allErrs, field.Invalid(fieldPath, v, msg)) + } + } + + return allErrs +} + +func validateURL(name string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + u, err := url.Parse(name) + if err != nil { + return append(allErrs, field.Invalid(fieldPath, name, err.Error())) + } + var msg string + if u.Scheme == "" { + msg = "scheme required, please use the prefix http(s)://" + return append(allErrs, field.Invalid(fieldPath, name, msg)) + } + if u.Host == "" { + msg = "hostname required" + return append(allErrs, field.Invalid(fieldPath, name, msg)) + } + if u.Path == "" { + msg = "path required" + return append(allErrs, field.Invalid(fieldPath, name, msg)) + } + + host, port, err := net.SplitHostPort(u.Host) + if err != nil { + host = u.Host + } + + allErrs = append(allErrs, validateSSLName(host, fieldPath)...) + if port != "" { + allErrs = append(allErrs, validatePortNumber(port, fieldPath)...) + } + + return allErrs +} + +func validatePortNumber(port string, fieldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + portInt, _ := strconv.Atoi(port) + msg := validation.IsValidPortNum(portInt) + if msg != nil { + allErrs = append(allErrs, field.Invalid(fieldPath, port, msg[0])) + } + return allErrs +} + func validateSSLName(name string, fieldPath *field.Path) field.ErrorList { allErrs := field.ErrorList{} @@ -211,8 +347,10 @@ func validateIngressMTLSVerifyClient(verifyClient string, fieldPath *field.Path) return allErrs } -const rateFmt = `[1-9]\d*r/[sSmM]` -const rateErrMsg = "must consist of numeric characters followed by a valid rate suffix. 'r/s|r/m" +const ( + rateFmt = `[1-9]\d*r/[sSmM]` + rateErrMsg = "must consist of numeric characters followed by a valid rate suffix. 'r/s|r/m" +) var rateRegexp = regexp.MustCompile("^" + rateFmt + "$") @@ -331,8 +469,10 @@ func validateRateLimitLogLevel(logLevel string, fieldPath *field.Path) field.Err return allErrs } -const jwtRealmFmt = `([^"$\\]|\\[^$])*` -const jwtRealmFmtErrMsg string = `a valid realm must have all '"' escaped and must not contain any '$' or end with an unescaped '\'` +const ( + jwtRealmFmt = `([^"$\\]|\\[^$])*` + jwtRealmFmtErrMsg string = `a valid realm must have all '"' escaped and must not contain any '$' or end with an unescaped '\'` +) var jwtRealmFmtRegexp = regexp.MustCompile("^" + jwtRealmFmt + "$") diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 8e81c49ffb..6587665376 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -38,6 +38,23 @@ func TestValidatePolicy(t *testing.T) { enablePreviewPolicies: true, msg: "use jwt(plus only) policy", }, + { + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + OIDC: &v1.OIDC{ + AuthEndpoint: "https://foo.bar/auth", + TokenEndpoint: "https://foo.bar/token", + JWKSURI: "https://foo.bar/certs", + ClientID: "random-string", + ClientSecret: "random-secret", + Scope: "openid", + }, + }, + }, + isPlus: true, + enablePreviewPolicies: true, + msg: "use OIDC (plus only)", + }, } for _, test := range tests { err := ValidatePolicy(test.policy, test.isPlus, test.enablePreviewPolicies) @@ -46,6 +63,7 @@ func TestValidatePolicy(t *testing.T) { } } } + func TestValidatePolicyFails(t *testing.T) { tests := []struct { policy *v1.Policy @@ -142,6 +160,40 @@ func TestValidatePolicyFails(t *testing.T) { enablePreviewPolicies: false, msg: "egressMTLS policy with preview policies disabled", }, + { + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + OIDC: &v1.OIDC{ + AuthEndpoint: "https://foo.bar/auth", + TokenEndpoint: "https://foo.bar/token", + JWKSURI: "https://foo.bar/certs", + ClientID: "random-string", + ClientSecret: "random-secret", + Scope: "openid", + }, + }, + }, + isPlus: true, + enablePreviewPolicies: false, + msg: "OIDC policy with preview policies disabled", + }, + { + policy: &v1.Policy{ + Spec: v1.PolicySpec{ + OIDC: &v1.OIDC{ + AuthEndpoint: "https://foo.bar/auth", + TokenEndpoint: "https://foo.bar/token", + JWKSURI: "https://foo.bar/certs", + ClientID: "random-string", + ClientSecret: "random-secret", + Scope: "openid", + }, + }, + }, + isPlus: false, + enablePreviewPolicies: true, + msg: "OIDC policy in OSS", + }, } for _, test := range tests { err := ValidatePolicy(test.policy, test.isPlus, test.enablePreviewPolicies) @@ -498,7 +550,7 @@ func TestValidatePositiveInt(t *testing.T) { } func TestValidateRateLimitZoneSize(t *testing.T) { - var validInput = []string{"32", "32k", "32K", "10m"} + validInput := []string{"32", "32k", "32K", "10m"} for _, test := range validInput { allErrs := validateRateLimitZoneSize(test, field.NewPath("size")) @@ -507,7 +559,7 @@ func TestValidateRateLimitZoneSize(t *testing.T) { } } - var invalidInput = []string{"", "31", "31k", "0", "0M"} + invalidInput := []string{"", "31", "31k", "0", "0M"} for _, test := range invalidInput { allErrs := validateRateLimitZoneSize(test, field.NewPath("size")) @@ -518,7 +570,7 @@ func TestValidateRateLimitZoneSize(t *testing.T) { } func TestValidateRateLimitLogLevel(t *testing.T) { - var validInput = []string{"error", "info", "warn", "notice"} + validInput := []string{"error", "info", "warn", "notice"} for _, test := range validInput { allErrs := validateRateLimitLogLevel(test, field.NewPath("logLevel")) @@ -527,7 +579,7 @@ func TestValidateRateLimitLogLevel(t *testing.T) { } } - var invalidInput = []string{"warn ", "info error", ""} + invalidInput := []string{"warn ", "info error", ""} for _, test := range invalidInput { allErrs := validateRateLimitLogLevel(test, field.NewPath("logLevel")) @@ -677,7 +729,7 @@ func TestValidateIngressMTLSInvalid(t *testing.T) { } func TestValidateIngressMTLSVerifyClient(t *testing.T) { - var validInput = []string{"on", "off", "optional", "optional_no_ca"} + validInput := []string{"on", "off", "optional", "optional_no_ca"} for _, test := range validInput { allErrs := validateIngressMTLSVerifyClient(test, field.NewPath("verifyClient")) @@ -686,7 +738,7 @@ func TestValidateIngressMTLSVerifyClient(t *testing.T) { } } - var invalidInput = []string{"true", "false"} + invalidInput := []string{"true", "false"} for _, test := range invalidInput { allErrs := validateIngressMTLSVerifyClient(test, field.NewPath("verifyClient")) @@ -777,3 +829,178 @@ func TestValidateEgressMTLSInvalid(t *testing.T) { } } } + +func TestValidateOIDCValid(t *testing.T) { + tests := []struct { + oidc *v1.OIDC + msg string + }{ + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://accounts.google.com/o/oauth2/v2/auth", + TokenEndpoint: "https://oauth2.googleapis.com/token", + JWKSURI: "https://www.googleapis.com/oauth2/v3/certs", + ClientID: "random-string", + ClientSecret: "random-secret", + Scope: "openid", + RedirectURI: "/foo", + }, + msg: "verify full oidc", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", + TokenEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/token", + JWKSURI: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/discovery/v2.0/keys", + ClientID: "ff", + ClientSecret: "ff", + Scope: "openid+profile", + RedirectURI: "/_codexe", + }, + msg: "verify azure endpoint", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/auth", + TokenEndpoint: "http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/token", + JWKSURI: "http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/certs", + ClientID: "bar", + ClientSecret: "foo", + Scope: "openid", + }, + msg: "domain with port number", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/auth", + TokenEndpoint: "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/token", + JWKSURI: "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs", + ClientID: "client", + ClientSecret: "secret", + Scope: "openid", + }, + msg: "ip address", + }, + } + + for _, test := range tests { + allErrs := validateOIDC(test.oidc, field.NewPath("oidc")) + if len(allErrs) != 0 { + t.Errorf("validateOIDC() returned errors %v for valid input for the case of %v", allErrs, test.msg) + } + } +} + +func TestValidateOIDCInvalid(t *testing.T) { + tests := []struct { + oidc *v1.OIDC + msg string + }{ + { + oidc: &v1.OIDC{ + RedirectURI: "/foo", + }, + msg: "missing required field", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", + TokenEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/token", + JWKSURI: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/discovery/v2.0/keys", + ClientID: "ff", + ClientSecret: "-ff-", + Scope: "openid+profile", + }, + msg: "invalid secret name", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "http://foo.\bar.com", + TokenEndpoint: "http://keycloak.default", + JWKSURI: "http://keycloak.default", + ClientID: "bar", + ClientSecret: "foo", + Scope: "openid", + }, + msg: "invalid URL", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/auth", + TokenEndpoint: "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/token", + JWKSURI: "http://127.0.0.1:8080/auth/realms/master/protocol/openid-connect/certs", + ClientID: "$foo$bar", + ClientSecret: "secret", + Scope: "openid", + }, + msg: "ip address", + }, + } + + for _, test := range tests { + allErrs := validateOIDC(test.oidc, field.NewPath("oidc")) + if len(allErrs) == 0 { + t.Errorf("validateOIDC() returned no errors for invalid input for the case of %v", test.msg) + } + } +} + +func TestValidateClientID(t *testing.T) { + validInput := []string{"myid", "your.id", "id-sf-sjfdj.com", "foo_bar~vni"} + + for _, test := range validInput { + allErrs := validateClientID(test, field.NewPath("clientID")) + if len(allErrs) != 0 { + t.Errorf("validateClientID(%q) returned errors %v for valid input", allErrs, test) + } + } + + invalidInput := []string{"$boo", "foo$bar", `foo_bar"vni`, `client\`} + + for _, test := range invalidInput { + allErrs := validateClientID(test, field.NewPath("clientID")) + if len(allErrs) == 0 { + t.Errorf("validateClientID(%q) didn't return error for invalid input", test) + } + } +} + +func TestValidateOIDCScope(t *testing.T) { + validInput := []string{"openid", "openid+profile", "openid+email", "openid+phone"} + + for _, test := range validInput { + allErrs := validateOIDCScope(test, field.NewPath("scope")) + if len(allErrs) != 0 { + t.Errorf("validateOIDCScope(%q) returned errors %v for valid input", allErrs, test) + } + } + + invalidInput := []string{"profile", "openid+web", `openid+foobar.com`} + + for _, test := range invalidInput { + allErrs := validateOIDCScope(test, field.NewPath("scope")) + if len(allErrs) == 0 { + t.Errorf("validateOIDCScope(%q) didn't return error for invalid input", test) + } + } +} + +func TestValidatURL(t *testing.T) { + validInput := []string{"http://google.com/auth", "https://foo.bar/baz", "http://127.0.0.1/bar", "http://openid.connect.com:8080/foo"} + + for _, test := range validInput { + allErrs := validateURL(test, field.NewPath("authEndpoint")) + if len(allErrs) != 0 { + t.Errorf("validateURL(%q) returned errors %v for valid input", allErrs, test) + } + } + + invalidInput := []string{"www.google..foo.com", "http://{foo.bar", `https://google.foo\bar`, "http://foo.bar:8080", "http://foo.bar:812345/fooo"} + + for _, test := range invalidInput { + allErrs := validateURL(test, field.NewPath("authEndpoint")) + if len(allErrs) == 0 { + t.Errorf("validateURL(%q) didn't return error for invalid input", test) + } + } +} From a2c724f7b3e934a5be97aca6450f7fa2d5f412e7 Mon Sep 17 00:00:00 2001 From: Mike Stephen Date: Thu, 14 Jan 2021 09:57:15 +0000 Subject: [PATCH 02/11] No longer write OIDC ClientSecret to a file --- internal/configs/configurator.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 9ca6b5e172..673fa1984f 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -643,9 +643,8 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string { } func (cnf *Configurator) addOrUpdateOIDCSecret(secret *api_v1.Secret) string { - name := objectMetaToFileName(&secret.ObjectMeta) - data := secret.Data[ClientSecretKey] - return cnf.nginxManager.CreateSecret(name, data, nginx.OIDCSecretFileMode) + // OIDC ClientSecret is not required on the filesystem, it is written directly to the config file. + return "" } // AddOrUpdateResources adds or updates configuration for resources. From c474bcfdc4de8bb7309532f0b17cf28c6a8161d0 Mon Sep 17 00:00:00 2001 From: Mike Stephen Date: Thu, 14 Jan 2021 16:20:39 +0000 Subject: [PATCH 03/11] Remove misleading function --- internal/configs/configurator.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/internal/configs/configurator.go b/internal/configs/configurator.go index 673fa1984f..51fe1b88ac 100644 --- a/internal/configs/configurator.go +++ b/internal/configs/configurator.go @@ -642,11 +642,6 @@ func (cnf *Configurator) addOrUpdateJWKSecret(secret *api_v1.Secret) string { return cnf.nginxManager.CreateSecret(name, data, nginx.JWKSecretFileMode) } -func (cnf *Configurator) addOrUpdateOIDCSecret(secret *api_v1.Secret) string { - // OIDC ClientSecret is not required on the filesystem, it is written directly to the config file. - return "" -} - // AddOrUpdateResources adds or updates configuration for resources. func (cnf *Configurator) AddOrUpdateResources(ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, virtualServerExes []*VirtualServerEx) (Warnings, error) { allWarnings := newWarnings() @@ -1440,7 +1435,8 @@ func (cnf *Configurator) AddOrUpdateSecret(secret *api_v1.Secret) string { case secrets.SecretTypeJWK: return cnf.addOrUpdateJWKSecret(secret) case secrets.SecretTypeOIDC: - return cnf.addOrUpdateOIDCSecret(secret) + // OIDC ClientSecret is not required on the filesystem, it is written directly to the config file. + return "" default: return cnf.addOrUpdateTLSSecret(secret) } From 41a966088c3ec788ab94a3f647e7b593aa2576d3 Mon Sep 17 00:00:00 2001 From: Mike Stephen Date: Thu, 14 Jan 2021 16:22:19 +0000 Subject: [PATCH 04/11] Update internal/nginx/manager.go Co-authored-by: Dean Coakley --- internal/nginx/manager.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/internal/nginx/manager.go b/internal/nginx/manager.go index 16f4436546..12535c2882 100644 --- a/internal/nginx/manager.go +++ b/internal/nginx/manager.go @@ -28,8 +28,6 @@ const TLSSecretFileMode = 0600 // JWKSecretFileMode defines the default filemode for files with JWK Secrets. const JWKSecretFileMode = 0644 -// OIDCSecretFileMode defines the default filemode for files with OIDC Secrets. -const OIDCSecretFileMode = 0644 const configFileMode = 0644 const jsonFileForOpenTracingTracer = "/var/lib/nginx/tracer-config.json" From 8c725b629840bc27677dfcdb36d73ee7b344ae70 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 10:53:00 -0800 Subject: [PATCH 05/11] Fix linter --- internal/nginx/manager.go | 30 ++++++++++++------------------ 1 file changed, 12 insertions(+), 18 deletions(-) diff --git a/internal/nginx/manager.go b/internal/nginx/manager.go index 12535c2882..c7790a6a00 100644 --- a/internal/nginx/manager.go +++ b/internal/nginx/manager.go @@ -16,27 +16,22 @@ import ( "github.com/nginxinc/nginx-plus-go-client/client" ) -// ReloadForEndpointsUpdate means that is caused by an endpoints update. -const ReloadForEndpointsUpdate = true - -// ReloadForOtherUpdate means that a reload is caused by an update for a resource(s) other than endpoints. -const ReloadForOtherUpdate = false - -// TLSSecretFileMode defines the default filemode for files with TLS Secrets. -const TLSSecretFileMode = 0600 - -// JWKSecretFileMode defines the default filemode for files with JWK Secrets. -const JWKSecretFileMode = 0644 - - -const configFileMode = 0644 -const jsonFileForOpenTracingTracer = "/var/lib/nginx/tracer-config.json" +const ( + ReloadForEndpointsUpdate = true // ReloadForEndpointsUpdate means that is caused by an endpoints update. + ReloadForOtherUpdate = false // ReloadForOtherUpdate means that a reload is caused by an update for a resource(s) other than endpoints. + TLSSecretFileMode = 0600 // TLSSecretFileMode defines the default filemode for files with TLS Secrets. + JWKSecretFileMode = 0644 // JWKSecretFileMode defines the default filemode for files with JWK Secrets. + configFileMode = 0644 + jsonFileForOpenTracingTracer = "/var/lib/nginx/tracer-config.json" +) // appPluginParams is the configuration of App-Protect plugin const appPluginParams = "tmm_count 4 proc_cpuinfo_cpu_mhz 2000000 total_xml_memory 307200000 total_umu_max_size 3129344 sys_max_account_id 1024 no_static_config" -const appProtectPluginStartCmd = "/usr/share/ts/bin/bd-socket-plugin" -const appProtectAgentStartCmd = "/opt/app_protect/bin/bd_agent" +const ( + appProtectPluginStartCmd = "/usr/share/ts/bin/bd-socket-plugin" + appProtectAgentStartCmd = "/opt/app_protect/bin/bd_agent" +) // ServerConfig holds the config data for an upstream server in NGINX Plus. type ServerConfig struct { @@ -496,7 +491,6 @@ func (lm *LocalManager) AppProtectPluginStart(appDone chan error) { go func() { appDone <- cmd.Wait() }() - } // AppProtectPluginQuit gracefully ends AppProtect Agent. From fece153afd27a15028e31bb28ba6ee5241542753 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 11:04:04 -0800 Subject: [PATCH 06/11] fix test name --- pkg/apis/configuration/validation/policy_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 6587665376..750789218b 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -933,7 +933,7 @@ func TestValidateOIDCInvalid(t *testing.T) { ClientSecret: "secret", Scope: "openid", }, - msg: "ip address", + msg: "invalid chars in clientID", }, } From 741500f1cb719fe24158b1cb98a279cc8614f7bb Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 17:05:25 -0800 Subject: [PATCH 07/11] Feedback from PR --- examples-of-custom-resources/oidc/README.md | 30 ++- .../oidc/client-secret.yaml | 2 +- .../oidc/keycloak_setup.md | 8 +- examples-of-custom-resources/oidc/oidc.yaml | 2 +- .../oidc/virtual-server-idp.yaml | 16 ++ internal/configs/version2/http.go | 2 +- internal/configs/virtualserver.go | 17 +- internal/configs/virtualserver_test.go | 172 +++++++++++++++++- internal/k8s/controller.go | 2 + internal/k8s/controller_test.go | 123 +++++++++++++ internal/k8s/secrets/validation.go | 2 +- .../configuration/validation/policy_test.go | 43 ++++- 12 files changed, 391 insertions(+), 28 deletions(-) create mode 100644 examples-of-custom-resources/oidc/virtual-server-idp.yaml diff --git a/examples-of-custom-resources/oidc/README.md b/examples-of-custom-resources/oidc/README.md index df3355c2c6..71e7c16888 100644 --- a/examples-of-custom-resources/oidc/README.md +++ b/examples-of-custom-resources/oidc/README.md @@ -5,13 +5,12 @@ In this example, we deploy keycloak and a web application configure load balanci ## Prerequisites 1. Follow the [installation](https://docs.nginx.com/nginx-ingress-controller/installation/installation-with-manifests/) instructions to deploy the Ingress Controller. -1. Save the public IP address of the Ingress Controller into a shell variable: +1. Save the public IP address of the Ingress Controller into `/etc/hosts`: ``` - $ IC_IP=XXX.YYY.ZZZ.III - ``` -1. Save the HTTP port of the Ingress Controller into a shell variable: - ``` - $ IC_HTTPS_PORT= + ... + + XXX.YYY.ZZZ.III webapp.example.com + XXX.YYY.ZZZ.III keycloak.example.com ``` ## Step 1 - Deploy a Web Application @@ -23,10 +22,14 @@ $ kubectl apply -f webapp.yaml ## Step 2 - Deploy Keycloak -Create keycloak deployment and service: +1. Create keycloak deployment and service: ``` $ kubectl apply -f keycloak.yaml ``` +1. Create a VirtualServer resource for Keycloak: + ``` + $ kubectl apply -f virtual-server-idp.yaml + ``` To set up Keycloak, you can either follow the steps in the "Configuring Keycloak" section of the documentation [here](https://docs.nginx.com/nginx/deployment-guides/single-sign-on/keycloak/#configuring-keycloak) or execute the commands [here](./keycloak_setup.md). @@ -50,20 +53,27 @@ $ kubectl apply -f oidc.yaml ``` ## Step 5 - Deploy the Service for the Ingress Controller and update ConfigMap -1. Deploy the service. +1. Deploy the service for Ingress Controller. ``` $ kubectl apply -f service/nodeport.yaml ``` 1. Update the ConfigMap with the config required for OIDC. - ``` + ```yaml + kind: ConfigMap + apiVersion: v1 + metadata: + name: nginx-config + namespace: nginx-ingress data: stream-snippets: | resolver 10.96.0.10 valid=5s; server { listen 12345; zone_sync; - zone_sync_server nginx-ic-svc.nginx-ingress.svc.cluster.local:12345 resolve; + zone_sync_server nginx-ingress.nginx-ingress.svc.cluster.local:12345 resolve; } + resolver: 10.96.0.10 + resolver-valid: 5s ``` 1. Apply the ConfigMap. ``` diff --git a/examples-of-custom-resources/oidc/client-secret.yaml b/examples-of-custom-resources/oidc/client-secret.yaml index 3fc8f1f3f7..86a60a4aed 100644 --- a/examples-of-custom-resources/oidc/client-secret.yaml +++ b/examples-of-custom-resources/oidc/client-secret.yaml @@ -4,4 +4,4 @@ metadata: name: oidc-secret type: nginx.org/oidc data: - client-secret: c3VwZXItc2VjcmV0IQo + client-secret: ZDk2YzdiODktOWJlMi00ZDM5LTg1OTItZDUzOTkzMmIwMmFh diff --git a/examples-of-custom-resources/oidc/keycloak_setup.md b/examples-of-custom-resources/oidc/keycloak_setup.md index cf653a39c0..ae8187474a 100644 --- a/examples-of-custom-resources/oidc/keycloak_setup.md +++ b/examples-of-custom-resources/oidc/keycloak_setup.md @@ -6,20 +6,20 @@ If you changed username and password for keycloak in `keycloak.yaml`, modify the 1. Save the address of Keycloak into a shell variable: ``` - $ KEYCLOAK_ADDRESS=localhost:8080 + $ KEYCLOAK_ADDRESS=keycloak.example.com ``` 1. Retrieve the access token and store into a shell variable: ``` - $ TOKEN=`curl -sS --data "username=admin&password=admin&grant_type=password&client_id=admin-cli" http://${KEYCLOAK_ADDRESS}/auth/realms/master/protocol/openid-connect/token | jq -r .access_token` + $ TOKEN=`curl -sS -k --data "username=admin&password=admin&grant_type=password&client_id=admin-cli" https://${KEYCLOAK_ADDRESS}/auth/realms/master/protocol/openid-connect/token | jq -r .access_token` ``` ***Note***: The access token lifespan is very short. If it expires between commands, retrieve it again with the command above. 1. Create the user `nginx-user` ``` - $ curl -sS -X POST -d '{ "username": "nginx-user", "enabled": true, "credentials":[{"type": "password", "value": "test", "temporary": false}]}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" http://${KEYCLOAK_ADDRESS}/auth/admin/realms/master/users + $ curl -sS -k -X POST -d '{ "username": "nginx-user", "enabled": true, "credentials":[{"type": "password", "value": "test", "temporary": false}]}' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/auth/admin/realms/master/users ``` 1. Create the Client `nginx-plus` and retrieve the secret: ``` - $ SECRET=`curl -sS -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"] }' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" http://${KEYCLOAK_ADDRESS}/auth/realms/master/clients-registrations/default | jq -r .secret` + $ SECRET=`curl -sS -k -X POST -d '{ "clientId": "nginx-plus", "redirectUris": ["https://webapp.example.com:443/_codexch"] }' -H "Content-Type:application/json" -H "Authorization: bearer ${TOKEN}" https://${KEYCLOAK_ADDRESS}/auth/realms/master/clients-registrations/default | jq -r .secret` ``` If everything went well you should have the secret stored in $SECRET, to double check run: ``` diff --git a/examples-of-custom-resources/oidc/oidc.yaml b/examples-of-custom-resources/oidc/oidc.yaml index 71fbf590ef..4676b53720 100644 --- a/examples-of-custom-resources/oidc/oidc.yaml +++ b/examples-of-custom-resources/oidc/oidc.yaml @@ -6,7 +6,7 @@ spec: oidc: clientID: nginx-plus clientSecret: oidc-secret - authEndpoint: http://idp.example.com:8080/auth/realms/master/protocol/openid-connect/auth + authEndpoint: http://keycloak.example.com/auth/realms/master/protocol/openid-connect/auth tokenEndpoint: http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/token jwksURI: http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/certs scope: openid+profile+email diff --git a/examples-of-custom-resources/oidc/virtual-server-idp.yaml b/examples-of-custom-resources/oidc/virtual-server-idp.yaml new file mode 100644 index 0000000000..2151791e5c --- /dev/null +++ b/examples-of-custom-resources/oidc/virtual-server-idp.yaml @@ -0,0 +1,16 @@ +apiVersion: k8s.nginx.org/v1 +kind: VirtualServer +metadata: + name: keycloak +spec: + host: keycloak.example.com + tls: + secret: tls-secret + upstreams: + - name: keycloak + service: keycloak + port: 8080 + routes: + - path: / + action: + pass: keycloak diff --git a/internal/configs/version2/http.go b/internal/configs/version2/http.go index d0c9c136be..00968e7ed8 100644 --- a/internal/configs/version2/http.go +++ b/internal/configs/version2/http.go @@ -150,7 +150,7 @@ type Location struct { LimitReqs []LimitReq JWTAuth *JWTAuth EgressMTLS *EgressMTLS - OIDC *bool + OIDC bool PoliciesErrorReturn *Return ServiceName string IsVSR bool diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 748d9e67c0..8887f6b9cc 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -394,7 +394,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS vsName: vsEx.VirtualServer.Name, } routePoliciesCfg := vsc.generatePolicies(ownerDetails, r.Policies, vsEx.Policies, routeContext, policyOpts) - if policiesCfg.OIDC != nil { + if policiesCfg.OIDC { routePoliciesCfg.OIDC = policiesCfg.OIDC } limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...) @@ -497,7 +497,7 @@ func (vsc *virtualServerConfigurator) GenerateVirtualServerConfig(vsEx *VirtualS context = subRouteContext } routePoliciesCfg := vsc.generatePolicies(ownerDetails, policyRefs, vsEx.Policies, context, policyOpts) - if policiesCfg.OIDC != nil { + if policiesCfg.OIDC { routePoliciesCfg.OIDC = policiesCfg.OIDC } limitReqZones = append(limitReqZones, routePoliciesCfg.LimitReqZones...) @@ -611,7 +611,7 @@ type policiesCfg struct { JWTAuth *version2.JWTAuth IngressMTLS *version2.IngressMTLS EgressMTLS *version2.EgressMTLS - OIDC *bool + OIDC bool ErrorReturn *version2.Return } @@ -862,7 +862,7 @@ func (p *policiesCfg) addOIDCConfig( oidcPolCfg *oidcPolicyCfg, ) *validationResults { res := newValidationResults() - if p.OIDC != nil { + if p.OIDC { res.addWarningf( "Multiple oidc policies in the same context is not valid. OIDC policy %q will be ignored", polKey, @@ -914,6 +914,10 @@ func (p *policiesCfg) addOIDCConfig( if redirectURI == "" { redirectURI = "/_codexch" } + scope := oidc.Scope + if scope == "" { + scope = "openid" + } oidcPolCfg.oidc = &version2.OIDC{ AuthEndpoint: oidc.AuthEndpoint, @@ -921,13 +925,12 @@ func (p *policiesCfg) addOIDCConfig( JwksURI: oidc.JWKSURI, ClientID: oidc.ClientID, ClientSecret: string(clientSecret), - Scope: oidc.Scope, + Scope: scope, RedirectURI: redirectURI, } } - trueVal := true - p.OIDC = &trueVal + p.OIDC = true return res } diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index b20f6bc68a..736c4add4c 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -1860,7 +1860,6 @@ func TestGeneratePolicies(t *testing.T) { "client-secret": []byte("super_secret_123"), }, }, - Path: "/etc/nginx/secrets/default-oidc-secret", }, }, } @@ -2168,7 +2167,7 @@ func TestGeneratePolicies(t *testing.T) { }, }, expected: policiesCfg{ - OIDC: createPointerFromBool(true), + OIDC: true, }, msg: "oidc reference", }, @@ -2206,6 +2205,7 @@ func TestGeneratePoliciesFails(t *testing.T) { context string expected policiesCfg expectedWarnings Warnings + expectedOidc *version2.OIDC msg string }{ { @@ -2957,6 +2957,169 @@ func TestGeneratePoliciesFails(t *testing.T) { }, msg: "egress mtls referencing missing tls secret", }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + }, + Error: errors.New("secret is invalid"), + }, + }, + }, + context: "route", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `OIDC policy "default/oidc-policy" references an invalid Secret: secret is invalid`, + }, + }, + msg: "oidc referencing missing oidc secret", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + AuthEndpoint: "http://foo.com/bar", + TokenEndpoint: "http://foo.com/bar", + JWKSURI: "http://foo.com/bar", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: api_v1.SecretTypeTLS, + }, + }, + }, + }, + context: "spec", + expected: policiesCfg{ + ErrorReturn: &version2.Return{ + Code: 500, + }, + }, + expectedWarnings: Warnings{ + nil: { + `OIDC policy "default/oidc-policy" references a Secret of an incorrect type "kubernetes.io/tls"`, + }, + }, + msg: "oidc secret referencing wrong secret type", + }, + { + policyRefs: []conf_v1.PolicyReference{ + { + Name: "oidc-policy", + Namespace: "default", + }, + { + Name: "oidc-policy2", + Namespace: "default", + }, + }, + policies: map[string]*conf_v1.Policy{ + "default/oidc-policy": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JWKSURI: "https://foo.com/certs", + ClientID: "foo", + }, + }, + }, + "default/oidc-policy2": { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy2", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret2", + AuthEndpoint: "https://bar.com/auth", + TokenEndpoint: "https://bar.com/token", + JWKSURI: "https://bar.com/certs", + ClientID: "bar", + }, + }, + }, + }, + policyOpts: policyOptions{ + secretRefs: map[string]*secrets.SecretReference{ + "default/oidc-secret": { + Secret: &api_v1.Secret{ + Type: secrets.SecretTypeOIDC, + Data: map[string][]byte{ + "client-secret": []byte("super_secret_123"), + }, + }, + }, + }, + }, + context: "route", + expected: policiesCfg{ + OIDC: true, + }, + expectedWarnings: Warnings{ + nil: { + `Multiple oidc policies in the same context is not valid. OIDC policy "default/oidc-policy2" will be ignored`, + }, + }, + expectedOidc: &version2.OIDC{ + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JwksURI: "https://foo.com/certs", + ClientID: "foo", + ClientSecret: "super_secret_123", + RedirectURI: "/_codexch", + Scope: "openid", + }, + msg: "multi oidc", + }, } for _, test := range tests { @@ -2974,6 +3137,11 @@ func TestGeneratePoliciesFails(t *testing.T) { test.msg, ) } + if vsc.oidcPolCfg.oidc != nil { + if diff := cmp.Diff(test.expectedOidc, vsc.oidcPolCfg.oidc); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + } } } diff --git a/internal/k8s/controller.go b/internal/k8s/controller.go index f2e8d5af5c..a74ba7680d 100644 --- a/internal/k8s/controller.go +++ b/internal/k8s/controller.go @@ -2492,6 +2492,8 @@ func findPoliciesForSecret(policies []*conf_v1.Policy, secretNamespace string, s res = append(res, pol) } else if pol.Spec.EgressMTLS != nil && pol.Spec.EgressMTLS.TrustedCertSecret == secretName && pol.Namespace == secretNamespace { res = append(res, pol) + } else if pol.Spec.OIDC != nil && pol.Spec.OIDC.ClientSecret == secretName && pol.Namespace == secretNamespace { + res = append(res, pol) } } diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go index 225b64f8b5..cf69944b21 100644 --- a/internal/k8s/controller_test.go +++ b/internal/k8s/controller_test.go @@ -1684,3 +1684,126 @@ func TestAddEgressMTLSSecrets(t *testing.T) { } } } + +func TestAddOidcSecret(t *testing.T) { + invalidErr := errors.New("invalid") + validSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "valid-oidc-secret", + Namespace: "default", + }, + Data: map[string][]byte{ + "client-secret": nil, + }, + Type: secrets.SecretTypeOIDC, + } + invalidSecret := &v1.Secret{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "invalid-oidc-secret", + Namespace: "default", + }, + Type: secrets.SecretTypeOIDC, + } + + tests := []struct { + policies []*conf_v1.Policy + expectedSecretRefs map[string]*secrets.SecretReference + wantErr bool + msg string + }{ + { + policies: []*conf_v1.Policy{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "valid-oidc-secret", + }, + }, + }, + }, + expectedSecretRefs: map[string]*secrets.SecretReference{ + "default/valid-oidc-secret": { + Secret: validSecret, + }, + }, + wantErr: false, + msg: "test getting valid secret", + }, + { + policies: []*conf_v1.Policy{}, + expectedSecretRefs: map[string]*secrets.SecretReference{}, + wantErr: false, + msg: "test getting valid secret with no policy", + }, + { + policies: []*conf_v1.Policy{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + AccessControl: &conf_v1.AccessControl{ + Allow: []string{"127.0.0.1"}, + }, + }, + }, + }, + expectedSecretRefs: map[string]*secrets.SecretReference{}, + wantErr: false, + msg: "test getting valid secret with wrong policy", + }, + { + policies: []*conf_v1.Policy{ + { + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "invalid-oidc-secret", + }, + }, + }, + }, + expectedSecretRefs: map[string]*secrets.SecretReference{ + "default/invalid-oidc-secret": { + Secret: invalidSecret, + Error: invalidErr, + }, + }, + wantErr: true, + msg: "test getting invalid secret", + }, + } + + lbc := LoadBalancerController{ + secretStore: secrets.NewFakeSecretsStore(map[string]*secrets.SecretReference{ + "default/valid-oidc-secret": { + Secret: validSecret, + }, + "default/invalid-oidc-secret": { + Secret: invalidSecret, + Error: invalidErr, + }, + }), + } + + for _, test := range tests { + result := make(map[string]*secrets.SecretReference) + + err := lbc.addOIDCSecretRefs(result, test.policies) + if (err != nil) != test.wantErr { + t.Errorf("addOIDCSecretRefs() returned %v, for the case of %v", err, test.msg) + } + + if diff := cmp.Diff(test.expectedSecretRefs, result, cmp.Comparer(errorComparer)); diff != "" { + t.Errorf("addOIDCSecretRefs() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + } +} diff --git a/internal/k8s/secrets/validation.go b/internal/k8s/secrets/validation.go index 716a30a8f2..d31235ddb1 100644 --- a/internal/k8s/secrets/validation.go +++ b/internal/k8s/secrets/validation.go @@ -98,7 +98,7 @@ func ValidateOIDCSecret(secret *api_v1.Secret) error { } if msg, ok := isValidClientSecretValue(string(clientSecret)); !ok { - return fmt.Errorf("OIDC client secret %v is invalid: %s", clientSecret, msg) + return fmt.Errorf("OIDC client secret is invalid: %s", msg) } return nil } diff --git a/pkg/apis/configuration/validation/policy_test.go b/pkg/apis/configuration/validation/policy_test.go index 750789218b..5770905124 100644 --- a/pkg/apis/configuration/validation/policy_test.go +++ b/pkg/apis/configuration/validation/policy_test.go @@ -900,8 +900,49 @@ func TestValidateOIDCInvalid(t *testing.T) { oidc: &v1.OIDC{ RedirectURI: "/foo", }, - msg: "missing required field", + msg: "missing required field auth", }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", + JWKSURI: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/discovery/v2.0/keys", + ClientID: "ff", + ClientSecret: "ff", + Scope: "openid+profile", + }, + msg: "missing required field token", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", + TokenEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/token", + ClientID: "ff", + ClientSecret: "ff", + Scope: "openid+profile", + }, + msg: "missing required field jwk", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", + TokenEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/token", + JWKSURI: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/discovery/v2.0/keys", + ClientSecret: "ff", + Scope: "openid+profile", + }, + msg: "missing required field clientid", + }, + { + oidc: &v1.OIDC{ + AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", + TokenEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/token", + JWKSURI: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/discovery/v2.0/keys", + ClientID: "ff", + Scope: "openid+profile", + }, + msg: "missing required field client secret", + }, + { oidc: &v1.OIDC{ AuthEndpoint: "https://login.microsoftonline.com/dd-fff-eee-1234-9be/oauth2/v2.0/authorize", From b24ecd0b1ec1544bac728354d6ab676733b26615 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 17:27:59 -0800 Subject: [PATCH 08/11] Fix missing key --- internal/configs/virtualserver.go | 1 + internal/configs/virtualserver_test.go | 26 ++++++++++++++++---------- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/internal/configs/virtualserver.go b/internal/configs/virtualserver.go index 8887f6b9cc..abfcca71ad 100644 --- a/internal/configs/virtualserver.go +++ b/internal/configs/virtualserver.go @@ -928,6 +928,7 @@ func (p *policiesCfg) addOIDCConfig( Scope: scope, RedirectURI: redirectURI, } + oidcPolCfg.key = polKey } p.OIDC = true diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 736c4add4c..8d9928c09a 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -2205,7 +2205,7 @@ func TestGeneratePoliciesFails(t *testing.T) { context string expected policiesCfg expectedWarnings Warnings - expectedOidc *version2.OIDC + expectedOidc *oidcPolicyCfg msg string }{ { @@ -3109,14 +3109,17 @@ func TestGeneratePoliciesFails(t *testing.T) { `Multiple oidc policies in the same context is not valid. OIDC policy "default/oidc-policy2" will be ignored`, }, }, - expectedOidc: &version2.OIDC{ - AuthEndpoint: "https://foo.com/auth", - TokenEndpoint: "https://foo.com/token", - JwksURI: "https://foo.com/certs", - ClientID: "foo", - ClientSecret: "super_secret_123", - RedirectURI: "/_codexch", - Scope: "openid", + expectedOidc: &oidcPolicyCfg{ + &version2.OIDC{ + AuthEndpoint: "https://foo.com/auth", + TokenEndpoint: "https://foo.com/token", + JwksURI: "https://foo.com/certs", + ClientID: "foo", + ClientSecret: "super_secret_123", + RedirectURI: "/_codexch", + Scope: "openid", + }, + "default/oidc-policy", }, msg: "multi oidc", }, @@ -3138,7 +3141,10 @@ func TestGeneratePoliciesFails(t *testing.T) { ) } if vsc.oidcPolCfg.oidc != nil { - if diff := cmp.Diff(test.expectedOidc, vsc.oidcPolCfg.oidc); diff != "" { + if diff := cmp.Diff(test.expectedOidc.oidc, vsc.oidcPolCfg.oidc); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedOidc.key, vsc.oidcPolCfg.key); diff != "" { t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) } } From a315e72599bcd7c045bd0b76a69cefbb7c0ad610 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 17:37:48 -0800 Subject: [PATCH 09/11] Revert client secret change --- examples-of-custom-resources/oidc/client-secret.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples-of-custom-resources/oidc/client-secret.yaml b/examples-of-custom-resources/oidc/client-secret.yaml index 86a60a4aed..3fc8f1f3f7 100644 --- a/examples-of-custom-resources/oidc/client-secret.yaml +++ b/examples-of-custom-resources/oidc/client-secret.yaml @@ -4,4 +4,4 @@ metadata: name: oidc-secret type: nginx.org/oidc data: - client-secret: ZDk2YzdiODktOWJlMi00ZDM5LTg1OTItZDUzOTkzMmIwMmFh + client-secret: c3VwZXItc2VjcmV0IQo From cbd27de7b41493ad488448c58682eb8ec0517386 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 17:59:32 -0800 Subject: [PATCH 10/11] PR feedback --- internal/configs/virtualserver_test.go | 66 ++++++++++++++++---------- internal/k8s/controller_test.go | 25 ++++++++++ 2 files changed, 66 insertions(+), 25 deletions(-) diff --git a/internal/configs/virtualserver_test.go b/internal/configs/virtualserver_test.go index 8d9928c09a..6b5ee21eb1 100644 --- a/internal/configs/virtualserver_test.go +++ b/internal/configs/virtualserver_test.go @@ -2227,7 +2227,8 @@ func TestGeneratePoliciesFails(t *testing.T) { "Policy default/allow-policy is missing or invalid", }, }, - msg: "missing policy", + expectedOidc: &oidcPolicyCfg{}, + msg: "missing policy", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2264,7 +2265,8 @@ func TestGeneratePoliciesFails(t *testing.T) { "AccessControl policy (or policies) with deny rules is overridden by policy (or policies) with allow rules", }, }, - msg: "conflicting policies", + expectedOidc: &oidcPolicyCfg{}, + msg: "conflicting policies", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2336,7 +2338,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `RateLimit policy "default/rateLimit-policy2" with limit request option rejectCode=505 is overridden to rejectCode=503 by the first policy reference in this context`, }, }, - msg: "rate limit policy limit request option override", + expectedOidc: &oidcPolicyCfg{}, + msg: "rate limit policy limit request option override", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2379,7 +2382,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `JWT policy "default/jwt-policy" references an invalid Secret: secret is invalid`, }, }, - msg: "jwt reference missing secret", + expectedOidc: &oidcPolicyCfg{}, + msg: "jwt reference missing secret", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2421,7 +2425,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `JWT policy "default/jwt-policy" references a Secret of an incorrect type "nginx.org/ca"`, }, }, - msg: "jwt references wrong secret type", + expectedOidc: &oidcPolicyCfg{}, + msg: "jwt references wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2487,7 +2492,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `Multiple jwt policies in the same context is not valid. JWT policy "default/jwt-policy2" will be ignored`, }, }, - msg: "multi jwt reference", + expectedOidc: &oidcPolicyCfg{}, + msg: "multi jwt reference", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2528,7 +2534,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `IngressMTLS policy "default/ingress-mtls-policy" references an invalid Secret: secret is invalid`, }, }, - msg: "ingress mtls reference an invalid secret", + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls reference an invalid secret", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2571,7 +2578,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `IngressMTLS policy "default/ingress-mtls-policy" references a Secret of an incorrect type "kubernetes.io/tls"`, }, }, - msg: "ingress mtls references wrong secret type", + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls references wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2628,7 +2636,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `Multiple ingressMTLS policies are not allowed. IngressMTLS policy "default/ingress-mtls-policy2" will be ignored`, }, }, - msg: "multi ingress mtls", + expectedOidc: &oidcPolicyCfg{}, + msg: "multi ingress mtls", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2672,7 +2681,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `IngressMTLS policy is not allowed in the route context`, }, }, - msg: "ingress mtls in the wrong context", + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls in the wrong context", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2716,7 +2726,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `TLS configuration needed for IngressMTLS policy`, }, }, - msg: "ingress mtls missing TLS config", + expectedOidc: &oidcPolicyCfg{}, + msg: "ingress mtls missing TLS config", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2781,7 +2792,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `Multiple egressMTLS policies in the same context is not valid. EgressMTLS policy "default/egress-mtls-policy2" will be ignored`, }, }, - msg: "multi egress mtls", + expectedOidc: &oidcPolicyCfg{}, + msg: "multi egress mtls", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2825,7 +2837,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `EgressMTLS policy "default/egress-mtls-policy" references an invalid Secret: secret is invalid`, }, }, - msg: "egress mtls referencing an invalid CA secret", + expectedOidc: &oidcPolicyCfg{}, + msg: "egress mtls referencing an invalid CA secret", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2868,7 +2881,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `EgressMTLS policy "default/egress-mtls-policy" references a Secret of an incorrect type "nginx.org/ca"`, }, }, - msg: "egress mtls referencing wrong secret type", + expectedOidc: &oidcPolicyCfg{}, + msg: "egress mtls referencing wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2911,7 +2925,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `EgressMTLS policy "default/egress-mtls-policy" references a Secret of an incorrect type "kubernetes.io/tls"`, }, }, - msg: "egress trusted secret referencing wrong secret type", + expectedOidc: &oidcPolicyCfg{}, + msg: "egress trusted secret referencing wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2955,7 +2970,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `EgressMTLS policy "default/egress-mtls-policy" references an invalid Secret: secret is invalid`, }, }, - msg: "egress mtls referencing missing tls secret", + expectedOidc: &oidcPolicyCfg{}, + msg: "egress mtls referencing missing tls secret", }, { policyRefs: []conf_v1.PolicyReference{ @@ -2998,7 +3014,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `OIDC policy "default/oidc-policy" references an invalid Secret: secret is invalid`, }, }, - msg: "oidc referencing missing oidc secret", + expectedOidc: &oidcPolicyCfg{}, + msg: "oidc referencing missing oidc secret", }, { policyRefs: []conf_v1.PolicyReference{ @@ -3043,7 +3060,8 @@ func TestGeneratePoliciesFails(t *testing.T) { `OIDC policy "default/oidc-policy" references a Secret of an incorrect type "kubernetes.io/tls"`, }, }, - msg: "oidc secret referencing wrong secret type", + expectedOidc: &oidcPolicyCfg{}, + msg: "oidc secret referencing wrong secret type", }, { policyRefs: []conf_v1.PolicyReference{ @@ -3140,13 +3158,11 @@ func TestGeneratePoliciesFails(t *testing.T) { test.msg, ) } - if vsc.oidcPolCfg.oidc != nil { - if diff := cmp.Diff(test.expectedOidc.oidc, vsc.oidcPolCfg.oidc); diff != "" { - t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } - if diff := cmp.Diff(test.expectedOidc.key, vsc.oidcPolCfg.key); diff != "" { - t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) - } + if diff := cmp.Diff(test.expectedOidc.oidc, vsc.oidcPolCfg.oidc); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) + } + if diff := cmp.Diff(test.expectedOidc.key, vsc.oidcPolCfg.key); diff != "" { + t.Errorf("generatePolicies() '%v' mismatch (-want +got):\n%s", test.msg, diff) } } } diff --git a/internal/k8s/controller_test.go b/internal/k8s/controller_test.go index cf69944b21..cfa7adbd8c 100644 --- a/internal/k8s/controller_test.go +++ b/internal/k8s/controller_test.go @@ -1134,6 +1134,17 @@ func TestFindPoliciesForSecret(t *testing.T) { }, }, } + oidcPol := &conf_v1.Policy{ + ObjectMeta: meta_v1.ObjectMeta{ + Name: "oidc-policy", + Namespace: "default", + }, + Spec: conf_v1.PolicySpec{ + OIDC: &conf_v1.OIDC{ + ClientSecret: "oidc-secret", + }, + }, + } tests := []struct { policies []*conf_v1.Policy @@ -1205,6 +1216,20 @@ func TestFindPoliciesForSecret(t *testing.T) { expected: []*conf_v1.Policy{egTLSPol2}, msg: "Find policy in default ns, ignore other types", }, + { + policies: []*conf_v1.Policy{oidcPol}, + secretNamespace: "default", + secretName: "oidc-secret", + expected: []*conf_v1.Policy{oidcPol}, + msg: "Find policy in default ns", + }, + { + policies: []*conf_v1.Policy{ingTLSPol, oidcPol}, + secretNamespace: "default", + secretName: "oidc-secret", + expected: []*conf_v1.Policy{oidcPol}, + msg: "Find policy in default ns, ignore other types", + }, } for _, test := range tests { result := findPoliciesForSecret(test.policies, test.secretNamespace, test.secretName) From b0187877a95d90ba7cb6b92d6b9ce088b22b4880 Mon Sep 17 00:00:00 2001 From: Luca Comellini Date: Thu, 14 Jan 2021 18:01:10 -0800 Subject: [PATCH 11/11] Update examples-of-custom-resources/oidc/oidc.yaml Co-authored-by: Michael Pleshakov --- examples-of-custom-resources/oidc/oidc.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples-of-custom-resources/oidc/oidc.yaml b/examples-of-custom-resources/oidc/oidc.yaml index 4676b53720..091ce79aba 100644 --- a/examples-of-custom-resources/oidc/oidc.yaml +++ b/examples-of-custom-resources/oidc/oidc.yaml @@ -6,7 +6,7 @@ spec: oidc: clientID: nginx-plus clientSecret: oidc-secret - authEndpoint: http://keycloak.example.com/auth/realms/master/protocol/openid-connect/auth + authEndpoint: https://keycloak.example.com/auth/realms/master/protocol/openid-connect/auth tokenEndpoint: http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/token jwksURI: http://keycloak.default.svc.cluster.local:8080/auth/realms/master/protocol/openid-connect/certs scope: openid+profile+email