synced with upstream
This commit is contained in:
commit
fc4d1f009f
133 changed files with 4386 additions and 686 deletions
51
.github/workflows/ci.yaml
vendored
51
.github/workflows/ci.yaml
vendored
|
@ -158,7 +158,7 @@ jobs:
|
|||
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: docker/setup-buildx-action@2a1a44ac4aa01993040736bd95bb470da1a38365 # v2.9.0
|
||||
uses: docker/setup-buildx-action@4c0219f9ac95b02789c1075625400b2acbff50b1 # v2.9.1
|
||||
with:
|
||||
version: latest
|
||||
|
||||
|
@ -319,6 +319,55 @@ jobs:
|
|||
name: e2e-test-reports-${{ matrix.k8s }}
|
||||
path: 'test/junitreports/report*.xml'
|
||||
|
||||
kubernetes-validations:
|
||||
name: Kubernetes with Validations
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- changes
|
||||
- build
|
||||
if: |
|
||||
(needs.changes.outputs.go == 'true') || ${{ inputs.run_e2e }}
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
k8s: [v1.27.1]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
|
||||
|
||||
- name: cache
|
||||
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2
|
||||
with:
|
||||
name: docker.tar.gz
|
||||
|
||||
- name: Create Kubernetes ${{ matrix.k8s }} cluster
|
||||
id: kind
|
||||
run: |
|
||||
kind create cluster --image=kindest/node:${{ matrix.k8s }} --config test/e2e/kind.yaml
|
||||
|
||||
- name: Load images from cache
|
||||
run: |
|
||||
echo "loading docker images..."
|
||||
pigz -dc docker.tar.gz | docker load
|
||||
|
||||
- name: Run e2e tests
|
||||
env:
|
||||
KIND_CLUSTER_NAME: kind
|
||||
SKIP_CLUSTER_CREATION: true
|
||||
SKIP_IMAGE_CREATION: true
|
||||
ENABLE_VALIDATIONS: true
|
||||
run: |
|
||||
kind get kubeconfig > $HOME/.kube/kind-config-kind
|
||||
make kind-e2e-test
|
||||
|
||||
- name: Upload e2e junit-reports
|
||||
uses: actions/upload-artifact@0b7f8abb1508181956e8e162db84b466c27e18ce # v3.1.2
|
||||
if: success() || failure()
|
||||
with:
|
||||
name: e2e-test-reports-${{ matrix.k8s }}
|
||||
path: 'test/junitreports/report*.xml'
|
||||
|
||||
|
||||
kubernetes-chroot:
|
||||
name: Kubernetes chroot
|
||||
|
|
|
@ -294,6 +294,7 @@ As of version `1.26.0` of this chart, by simply not providing any clusterIP valu
|
|||
| controller.dnsConfig | object | `{}` | Optionally customize the pod dnsConfig. |
|
||||
| controller.dnsPolicy | string | `"ClusterFirst"` | Optionally change this to ClusterFirstWithHostNet in case you have 'hostNetwork: true'. By default, while using host network, name resolution uses the host's DNS. If you wish nginx-controller to keep resolving names inside the k8s network, use ClusterFirstWithHostNet. |
|
||||
| controller.electionID | string | `""` | Election ID to use for status update, by default it uses the controller name combined with a suffix of 'leader' |
|
||||
| controller.enableAnnotationValidations | bool | `false` | |
|
||||
| controller.enableMimalloc | bool | `true` | Enable mimalloc as a drop-in replacement for malloc. # ref: https://github.com/microsoft/mimalloc # |
|
||||
| controller.enableTopologyAwareRouting | bool | `false` | This configuration enables Topology Aware Routing feature, used together with service annotation service.kubernetes.io/topology-aware-hints="auto" Defaults to false |
|
||||
| controller.existingPsp | string | `""` | Use an existing PSP instead of creating one |
|
||||
|
@ -399,14 +400,14 @@ As of version `1.26.0` of this chart, by simply not providing any clusterIP valu
|
|||
| controller.scope.enabled | bool | `false` | Enable 'scope' or not |
|
||||
| controller.scope.namespace | string | `""` | Namespace to limit the controller to; defaults to $(POD_NAMESPACE) |
|
||||
| controller.scope.namespaceSelector | string | `""` | When scope.enabled == false, instead of watching all namespaces, we watching namespaces whose labels only match with namespaceSelector. Format like foo=bar. Defaults to empty, means watching all namespaces. |
|
||||
| controller.service.annotations | object | `{}` | |
|
||||
| controller.service.annotations | object | `{}` | Annotations are mandatory for the load balancer to come up. Varies with the cloud service. Values passed through helm tpl engine. |
|
||||
| controller.service.appProtocol | bool | `true` | If enabled is adding an appProtocol option for Kubernetes service. An appProtocol field replacing annotations that were using for setting a backend protocol. Here is an example for AWS: service.beta.kubernetes.io/aws-load-balancer-backend-protocol: http It allows choosing the protocol for each backend specified in the Kubernetes service. See the following GitHub issue for more details about the purpose: https://github.com/kubernetes/kubernetes/issues/40244 Will be ignored for Kubernetes versions older than 1.20 # |
|
||||
| controller.service.enableHttp | bool | `true` | |
|
||||
| controller.service.enableHttps | bool | `true` | |
|
||||
| controller.service.enabled | bool | `true` | |
|
||||
| controller.service.external.enabled | bool | `true` | |
|
||||
| controller.service.externalIPs | list | `[]` | List of IP addresses at which the controller services are available # Ref: https://kubernetes.io/docs/concepts/services-networking/service/#external-ips # |
|
||||
| controller.service.internal.annotations | object | `{}` | Annotations are mandatory for the load balancer to come up. Varies with the cloud service. |
|
||||
| controller.service.internal.annotations | object | `{}` | Annotations are mandatory for the load balancer to come up. Varies with the cloud service. Values passed through helm tpl engine. |
|
||||
| controller.service.internal.enabled | bool | `false` | Enables an additional internal load balancer (besides the external one). |
|
||||
| controller.service.internal.loadBalancerIP | string | `""` | Used by cloud providers to connect the resulting internal LoadBalancer to a pre-existing static IP. Make sure to add to the service the needed annotation to specify the subnet which the static IP belongs to. For instance, `networking.gke.io/internal-load-balancer-subnet` for GCP and `service.beta.kubernetes.io/aws-load-balancer-subnets` for AWS. |
|
||||
| controller.service.internal.loadBalancerSourceRanges | list | `[]` | Restrict access For LoadBalancer service. Defaults to 0.0.0.0/0. |
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
{{- define "ingress-nginx.params" -}}
|
||||
- /nginx-ingress-controller
|
||||
{{- if .Values.controller.enableAnnotationValidations }}
|
||||
- --enable-annotation-validation=true
|
||||
{{- end }}
|
||||
{{- if .Values.defaultBackend.enabled }}
|
||||
- --default-backend-service=$(POD_NAMESPACE)/{{ include "ingress-nginx.defaultBackend.fullname" . }}
|
||||
{{- end }}
|
||||
|
|
|
@ -19,7 +19,7 @@ spec:
|
|||
matchLabels:
|
||||
{{- include "ingress-nginx.selectorLabels" . | nindent 6 }}
|
||||
app.kubernetes.io/component: controller
|
||||
{{- if not .Values.controller.autoscaling.enabled }}
|
||||
{{- if not (or .Values.controller.autoscaling.enabled .Values.controller.keda.enabled) }}
|
||||
replicas: {{ .Values.controller.replicaCount }}
|
||||
{{- end }}
|
||||
revisionHistoryLimit: {{ .Values.revisionHistoryLimit }}
|
||||
|
|
|
@ -4,7 +4,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.controller.service.internal.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{ $key }}: {{ tpl ($value | toString) $ | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "ingress-nginx.labels" . | nindent 4 }}
|
||||
|
|
|
@ -4,7 +4,7 @@ kind: Service
|
|||
metadata:
|
||||
annotations:
|
||||
{{- range $key, $value := .Values.controller.service.annotations }}
|
||||
{{ $key }}: {{ $value | quote }}
|
||||
{{ $key }}: {{ tpl ($value | toString) $ | quote }}
|
||||
{{- end }}
|
||||
labels:
|
||||
{{- include "ingress-nginx.labels" . | nindent 4 }}
|
||||
|
|
|
@ -15,6 +15,7 @@ commonLabels: {}
|
|||
|
||||
controller:
|
||||
name: controller
|
||||
enableAnnotationValidations: false
|
||||
image:
|
||||
## Keep false as default for now!
|
||||
chroot: false
|
||||
|
@ -415,6 +416,7 @@ controller:
|
|||
# Will be ignored for Kubernetes versions older than 1.20
|
||||
##
|
||||
appProtocol: true
|
||||
# -- Annotations are mandatory for the load balancer to come up. Varies with the cloud service. Values passed through helm tpl engine.
|
||||
annotations: {}
|
||||
labels: {}
|
||||
# clusterIP: ""
|
||||
|
@ -476,7 +478,7 @@ controller:
|
|||
internal:
|
||||
# -- Enables an additional internal load balancer (besides the external one).
|
||||
enabled: false
|
||||
# -- Annotations are mandatory for the load balancer to come up. Varies with the cloud service.
|
||||
# -- Annotations are mandatory for the load balancer to come up. Varies with the cloud service. Values passed through helm tpl engine.
|
||||
annotations: {}
|
||||
# -- Used by cloud providers to connect the resulting internal LoadBalancer to a pre-existing static IP. Make sure to add to the service the needed annotation to specify the subnet which the static IP belongs to. For instance, `networking.gke.io/internal-load-balancer-subnet` for GCP and `service.beta.kubernetes.io/aws-load-balancer-subnets` for AWS.
|
||||
loadBalancerIP: ""
|
||||
|
|
|
@ -15,6 +15,7 @@ They are set in the container spec of the `ingress-nginx-controller` Deployment
|
|||
| `--default-backend-service` | Service used to serve HTTP requests not matching any known server name (catch-all). Takes the form "namespace/name". The controller configures NGINX to forward requests to the first port of this Service. |
|
||||
| `--default-server-port` | Port to use for exposing the default server (catch-all). (default 8181) |
|
||||
| `--default-ssl-certificate` | Secret containing a SSL certificate to be used by the default HTTPS server (catch-all). Takes the form "namespace/name". |
|
||||
| `--enable-annotation-validation` | If true, will enable the annotation validation feature. This value will be defaulted to true on a future release. |
|
||||
| `--disable-catch-all` | Disable support for catch-all Ingresses. (default false) |
|
||||
| `--disable-full-test` | Disable full test of all merged ingresses at the admission stage and tests the template of the ingress being created or updated (full test of all ingresses is enabled by default). |
|
||||
| `--disable-svc-external-name` | Disable support for Services of type ExternalName. (default false) |
|
||||
|
|
|
@ -29,7 +29,9 @@ The following table shows a configuration option's name, type, and the default v
|
|||
|:---|:---|:------|:----|
|
||||
|[add-headers](#add-headers)|string|""||
|
||||
|[allow-backend-server-header](#allow-backend-server-header)|bool|"false"||
|
||||
|[allow-cross-namespace-resources](#allow-cross-namespace-resources)|bool|"true"||
|
||||
|[allow-snippet-annotations](#allow-snippet-annotations)|bool|true||
|
||||
|[annotations-risk-level](#annotations-risk-level)|string|Critical||
|
||||
|[annotation-value-word-blocklist](#annotation-value-word-blocklist)|string array|""||
|
||||
|[hide-headers](#hide-headers)|string array|empty||
|
||||
|[access-log-params](#access-log-params)|string|""||
|
||||
|
@ -240,6 +242,20 @@ Sets custom headers from named configmap before sending traffic to the client. S
|
|||
|
||||
Enables the return of the header Server from the backend instead of the generic nginx string. _**default:**_ is disabled
|
||||
|
||||
## allow-cross-namespace-resources
|
||||
|
||||
Enables users to consume cross namespace resource on annotations, when was previously enabled . _**default:**_ true
|
||||
|
||||
**Annotations that may be impacted with this change**:
|
||||
* `auth-secret`
|
||||
* `auth-proxy-set-header`
|
||||
* `auth-tls-secret`
|
||||
* `fastcgi-params-configmap`
|
||||
* `proxy-ssl-secret`
|
||||
|
||||
|
||||
**This option will be defaulted to false in the next major release**
|
||||
|
||||
## allow-snippet-annotations
|
||||
|
||||
Enables Ingress to parse and add *-snippet annotations/directives created by the user. _**default:**_ `true`
|
||||
|
@ -247,6 +263,16 @@ Enables Ingress to parse and add *-snippet annotations/directives created by the
|
|||
Warning: We recommend enabling this option only if you TRUST users with permission to create Ingress objects, as this
|
||||
may allow a user to add restricted configurations to the final nginx.conf file
|
||||
|
||||
**This option will be defaulted to false in the next major release**
|
||||
|
||||
## annotations-risk-level
|
||||
|
||||
Represents the risk accepted on an annotation. If the risk is, for instance `Medium`, annotations with risk High and Critical will not be accepted.
|
||||
|
||||
Accepted values are `Critical`, `High`, `Medium` and `Low`.
|
||||
|
||||
Defaults to `Critical` but will be changed to `High` on the next minor release
|
||||
|
||||
## annotation-value-word-blocklist
|
||||
|
||||
Contains a comma-separated value of chars/words that are well known of being used to abuse Ingress configuration
|
||||
|
|
2
go.mod
2
go.mod
|
@ -26,7 +26,7 @@ require (
|
|||
github.com/yudai/gojsondiff v1.0.0
|
||||
github.com/zakjan/cert-chain-resolver v0.0.0-20211122211144-c6b0b792af9a
|
||||
golang.org/x/crypto v0.11.0
|
||||
google.golang.org/grpc v1.56.1
|
||||
google.golang.org/grpc v1.56.2
|
||||
google.golang.org/grpc/examples v0.0.0-20221220003428-4f16fbe410f7
|
||||
gopkg.in/go-playground/pool.v3 v3.1.1
|
||||
gopkg.in/mcuadros/go-syslog.v2 v2.3.0
|
||||
|
|
4
go.sum
4
go.sum
|
@ -661,8 +661,8 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa
|
|||
google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
|
||||
google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.56.1 h1:z0dNfjIl0VpaZ9iSVjA6daGatAYwPGstTjt5vkRMFkQ=
|
||||
google.golang.org/grpc v1.56.1/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI=
|
||||
google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s=
|
||||
google.golang.org/grpc/examples v0.0.0-20221220003428-4f16fbe410f7 h1:pPsdyuBif+uoyUoL19yuj/TCfUPsmpJHJZhWQ98JGLU=
|
||||
google.golang.org/grpc/examples v0.0.0-20221220003428-4f16fbe410f7/go.mod h1:8pQa1yxxkh+EsxUK8/455D5MSbv3vgmEJqKCH3y17mI=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
|
|
|
@ -23,8 +23,6 @@ TAG ?=v$(shell date +%Y%m%d)-$(SHORT_SHA)
|
|||
|
||||
REGISTRY ?= local
|
||||
|
||||
|
||||
|
||||
IMAGE = $(REGISTRY)/e2e-test-cfssl
|
||||
|
||||
# required to enable buildx
|
||||
|
|
|
@ -6,8 +6,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -17,6 +15,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/cfssl && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
|
||||
FROM alpine:3.18.0
|
||||
FROM alpine:3.18.2
|
||||
|
||||
|
||||
RUN echo "@testing http://nl.alpinelinux.org/alpine/edge/testing" >> /etc/apk/repositories
|
||||
|
|
|
@ -6,8 +6,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -17,6 +15,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/custom-error-pages && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
|
|
@ -6,8 +6,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -17,6 +15,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/echo && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
|
|
@ -18,7 +18,9 @@ SHELL=/bin/bash -o pipefail -o errexit
|
|||
DIR:=$(strip $(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))))
|
||||
INIT_BUILDX=$(DIR)/../../hack/init-buildx.sh
|
||||
|
||||
TAG ?=v1.0.0
|
||||
SHORT_SHA ?=$(shell git rev-parse --short HEAD)
|
||||
TAG ?=v$(shell date +%Y%m%d)-$(SHORT_SHA)
|
||||
|
||||
REGISTRY ?= local
|
||||
|
||||
IMAGE = $(REGISTRY)/ext-auth-example-authsvc
|
||||
|
|
|
@ -8,8 +8,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -19,6 +17,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/ext-auth-example-authsvc && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
|
|
@ -6,8 +6,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -17,6 +15,4 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/fastcgi-helloserver && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
||||
|
|
|
@ -8,8 +8,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -19,6 +17,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/go-grpc-greeter-server && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
|
|
@ -8,8 +8,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -19,6 +17,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/httpbun && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "master"
|
||||
|
|
|
@ -21,8 +21,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -32,6 +30,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/kube-webhook-certgen && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "main"
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
FROM alpine:3.18.0 as builder
|
||||
FROM alpine:3.18.2 as builder
|
||||
|
||||
COPY . /
|
||||
|
||||
|
@ -21,7 +21,7 @@ RUN apk update \
|
|||
&& /build.sh
|
||||
|
||||
# Use a multi-stage build
|
||||
FROM alpine:3.18.0
|
||||
FROM alpine:3.18.2
|
||||
|
||||
ENV PATH=$PATH:/usr/local/luajit/bin:/usr/local/nginx/sbin:/usr/local/nginx/bin
|
||||
|
||||
|
|
|
@ -20,14 +20,14 @@ set -o pipefail
|
|||
|
||||
export NGINX_VERSION=1.21.6
|
||||
|
||||
# Check for recent changes: https://github.com/vision5/ngx_devel_kit/compare/v0.3.1...master
|
||||
export NDK_VERSION=0.3.1
|
||||
# Check for recent changes: https://github.com/vision5/ngx_devel_kit/compare/v0.3.2...master
|
||||
export NDK_VERSION=0.3.2
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/set-misc-nginx-module/compare/v0.33...master
|
||||
export SETMISC_VERSION=0.33
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/headers-more-nginx-module/compare/v0.33...master
|
||||
export MORE_HEADERS_VERSION=0.33
|
||||
# Check for recent changes: https://github.com/openresty/headers-more-nginx-module/compare/v0.34...master
|
||||
export MORE_HEADERS_VERSION=0.34
|
||||
|
||||
# Check for recent changes: https://github.com/atomx/nginx-http-auth-digest/compare/v1.0.0...atomx:master
|
||||
export NGINX_DIGEST_AUTH=1.0.0
|
||||
|
@ -65,32 +65,32 @@ export MODSECURITY_LIB_VERSION=e9a7ba4a60be48f761e0328c6dfcc668d70e35a0
|
|||
# Check for recent changes: https://github.com/coreruleset/coreruleset/compare/v3.3.2...v3.3/master
|
||||
export OWASP_MODSECURITY_CRS_VERSION=v3.3.4
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-nginx-module/compare/v0.10.21...master
|
||||
export LUA_NGX_VERSION=0.10.21
|
||||
# Check for recent changes: https://github.com/openresty/lua-nginx-module/compare/v0.10.25...master
|
||||
export LUA_NGX_VERSION=0.10.25
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/stream-lua-nginx-module/compare/v0.0.11...master
|
||||
export LUA_STREAM_NGX_VERSION=0.0.11
|
||||
# Check for recent changes: https://github.com/openresty/stream-lua-nginx-module/compare/v0.0.13...master
|
||||
export LUA_STREAM_NGX_VERSION=0.0.13
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-upstream-nginx-module/compare/8aa93ead98ba2060d4efd594ae33a35d153589bf...master
|
||||
export LUA_UPSTREAM_VERSION=8aa93ead98ba2060d4efd594ae33a35d153589bf
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-cjson/compare/2.1.0.10...openresty:master
|
||||
export LUA_CJSON_VERSION=2.1.0.10
|
||||
# Check for recent changes: https://github.com/openresty/lua-cjson/compare/2.1.0.11...openresty:master
|
||||
export LUA_CJSON_VERSION=2.1.0.11
|
||||
|
||||
# Check for recent changes: https://github.com/leev/ngx_http_geoip2_module/compare/3.3...master
|
||||
export GEOIP2_VERSION=a26c6beed77e81553686852dceb6c7fdacc5970d
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/luajit2/compare/v2.1-20220411...v2.1-agentzh
|
||||
export LUAJIT_VERSION=2.1-20220411
|
||||
# Check for recent changes: https://github.com/openresty/luajit2/compare/v2.1-20230410...v2.1-agentzh
|
||||
export LUAJIT_VERSION=2.1-20230410
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-balancer/compare/v0.04...master
|
||||
export LUA_RESTY_BALANCER=0.04
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-lrucache/compare/v0.11...master
|
||||
export LUA_RESTY_CACHE=0.11
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-lrucache/compare/v0.13...master
|
||||
export LUA_RESTY_CACHE=0.13
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-core/compare/v0.1.23...master
|
||||
export LUA_RESTY_CORE=0.1.23
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-core/compare/v0.1.27...master
|
||||
export LUA_RESTY_CORE=0.1.27
|
||||
|
||||
# Check for recent changes: https://github.com/cloudflare/lua-resty-cookie/compare/v0.1.0...master
|
||||
export LUA_RESTY_COOKIE_VERSION=303e32e512defced053a6484bc0745cf9dc0d39e
|
||||
|
@ -101,17 +101,17 @@ export LUA_RESTY_DNS=0.22
|
|||
# Check for recent changes: https://github.com/ledgetech/lua-resty-http/compare/v0.16.1...master
|
||||
export LUA_RESTY_HTTP=0ce55d6d15da140ecc5966fa848204c6fd9074e8
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-lock/compare/v0.08...master
|
||||
export LUA_RESTY_LOCK=0.08
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-lock/compare/v0.09...master
|
||||
export LUA_RESTY_LOCK=0.09
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-upload/compare/v0.10...master
|
||||
export LUA_RESTY_UPLOAD_VERSION=0.10
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-upload/compare/v0.11...master
|
||||
export LUA_RESTY_UPLOAD_VERSION=0.11
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-string/compare/v0.15...master
|
||||
export LUA_RESTY_STRING_VERSION=0.15
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-memcached/compare/v0.16...master
|
||||
export LUA_RESTY_MEMCACHED_VERSION=0.16
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-memcached/compare/v0.17...master
|
||||
export LUA_RESTY_MEMCACHED_VERSION=0.17
|
||||
|
||||
# Check for recent changes: https://github.com/openresty/lua-resty-redis/compare/v0.30...master
|
||||
export LUA_RESTY_REDIS_VERSION=0.30
|
||||
|
@ -199,13 +199,13 @@ cd "$BUILD_PATH"
|
|||
get_src 66dc7081488811e9f925719e34d1b4504c2801c81dee2920e5452a86b11405ae \
|
||||
"https://nginx.org/download/nginx-$NGINX_VERSION.tar.gz"
|
||||
|
||||
get_src 0e971105e210d272a497567fa2e2c256f4e39b845a5ba80d373e26ba1abfbd85 \
|
||||
"https://github.com/simpl/ngx_devel_kit/archive/v$NDK_VERSION.tar.gz"
|
||||
get_src aa961eafb8317e0eb8da37eb6e2c9ff42267edd18b56947384e719b85188f58b \
|
||||
"https://github.com/vision5/ngx_devel_kit/archive/v$NDK_VERSION.tar.gz"
|
||||
|
||||
get_src cd5e2cc834bcfa30149e7511f2b5a2183baf0b70dc091af717a89a64e44a2985 \
|
||||
"https://github.com/openresty/set-misc-nginx-module/archive/v$SETMISC_VERSION.tar.gz"
|
||||
|
||||
get_src a3dcbab117a9c103bc1ea5200fc00a7b7d2af97ff7fd525f16f8ac2632e30fbf \
|
||||
get_src 0c0d2ced2ce895b3f45eb2b230cd90508ab2a773299f153de14a43e44c1209b3 \
|
||||
"https://github.com/openresty/headers-more-nginx-module/archive/v$MORE_HEADERS_VERSION.tar.gz"
|
||||
|
||||
get_src f09851e6309560a8ff3e901548405066c83f1f6ff88aa7171e0763bd9514762b \
|
||||
|
@ -241,10 +241,10 @@ get_src 7d5f3439c8df56046d0564b5857fd8a30296ab1bd6df0f048aed7afb56a0a4c2 \
|
|||
get_src 99c47c75c159795c9faf76bbb9fa58e5a50b75286c86565ffcec8514b1c74bf9 \
|
||||
"https://github.com/openresty/stream-lua-nginx-module/archive/v$LUA_STREAM_NGX_VERSION.tar.gz"
|
||||
else
|
||||
get_src 9db756000578efaecb43bea4fc6cf631aaa80988d86ffe5d3afeb9927895ffad \
|
||||
get_src bc764db42830aeaf74755754b900253c233ad57498debe7a441cee2c6f4b07c2 \
|
||||
"https://github.com/openresty/lua-nginx-module/archive/v$LUA_NGX_VERSION.tar.gz"
|
||||
|
||||
get_src c7924f28cb014a99636e747ea907724dd55f60e180cb92cde6e8ed48d2278f27 \
|
||||
get_src 01b715754a8248cc7228e0c8f97f7488ae429d90208de0481394e35d24cef32f \
|
||||
"https://github.com/openresty/stream-lua-nginx-module/archive/v$LUA_STREAM_NGX_VERSION.tar.gz"
|
||||
|
||||
fi
|
||||
|
@ -256,7 +256,7 @@ if [[ ${ARCH} == "s390x" ]]; then
|
|||
get_src 266ed1abb70a9806d97cb958537a44b67db6afb33d3b32292a2d68a2acedea75 \
|
||||
"https://github.com/openresty/luajit2/archive/$LUAJIT_VERSION.tar.gz"
|
||||
else
|
||||
get_src d3f2c870f8f88477b01726b32accab30f6e5d57ae59c5ec87374ff73d0794316 \
|
||||
get_src 77bbcbb24c3c78f51560017288f3118d995fe71240aa379f5818ff6b166712ff \
|
||||
"https://github.com/openresty/luajit2/archive/v$LUAJIT_VERSION.tar.gz"
|
||||
fi
|
||||
|
||||
|
@ -266,7 +266,7 @@ get_src 8d39c6b23f941a2d11571daaccc04e69539a3fcbcc50a631837560d5861a7b96 \
|
|||
get_src 4c1933434572226942c65b2f2b26c8a536ab76aa771a3c7f6c2629faa764976b \
|
||||
"https://github.com/leev/ngx_http_geoip2_module/archive/$GEOIP2_VERSION.tar.gz"
|
||||
|
||||
get_src 5d16e623d17d4f42cc64ea9cfb69ca960d313e12f5d828f785dd227cc483fcbd \
|
||||
get_src deb4ab1ffb9f3d962c4b4a2c4bdff692b86a209e3835ae71ebdf3b97189e40a9 \
|
||||
"https://github.com/openresty/lua-resty-upload/archive/v$LUA_RESTY_UPLOAD_VERSION.tar.gz"
|
||||
|
||||
get_src bdbf271003d95aa91cab0a92f24dca129e99b33f79c13ebfcdbbcbb558129491 \
|
||||
|
@ -279,20 +279,20 @@ if [[ ${ARCH} == "s390x" ]]; then
|
|||
get_src 8f5f76d2689a3f6b0782f0a009c56a65e4c7a4382be86422c9b3549fe95b0dc4 \
|
||||
"https://github.com/openresty/lua-resty-core/archive/v$LUA_RESTY_CORE.tar.gz"
|
||||
else
|
||||
get_src efd6b51520429e64b1bcc10f477d370ebed1631c190f7e4dc270d959a743ad7d \
|
||||
get_src 39baab9e2b31cc48cecf896cea40ef6e80559054fd8a6e440cc804a858ea84d4 \
|
||||
"https://github.com/openresty/lua-resty-core/archive/v$LUA_RESTY_CORE.tar.gz"
|
||||
fi
|
||||
|
||||
get_src 0c551d6898f89f876e48730f9b55790d0ba07d5bc0aa6c76153277f63c19489f \
|
||||
get_src a77b9de160d81712f2f442e1de8b78a5a7ef0d08f13430ff619f79235db974d4 \
|
||||
"https://github.com/openresty/lua-cjson/archive/$LUA_CJSON_VERSION.tar.gz"
|
||||
|
||||
get_src 5ed48c36231e2622b001308622d46a0077525ac2f751e8cc0c9905914254baa4 \
|
||||
"https://github.com/cloudflare/lua-resty-cookie/archive/$LUA_RESTY_COOKIE_VERSION.tar.gz"
|
||||
|
||||
get_src e810ed124fe788b8e4aac2c8960dda1b9a6f8d0ca94ce162f28d3f4d877df8af \
|
||||
get_src 573184006b98ccee2594b0d134fa4d05e5d2afd5141cbad315051ccf7e9b6403 \
|
||||
"https://github.com/openresty/lua-resty-lrucache/archive/v$LUA_RESTY_CACHE.tar.gz"
|
||||
|
||||
get_src 2b4683f9abe73e18ca00345c65010c9056777970907a311d6e1699f753141de2 \
|
||||
get_src b4ddcd47db347e9adf5c1e1491a6279a6ae2a3aff3155ef77ea0a65c998a69c1 \
|
||||
"https://github.com/openresty/lua-resty-lock/archive/v$LUA_RESTY_LOCK.tar.gz"
|
||||
|
||||
get_src 70e9a01eb32ccade0d5116a25bcffde0445b94ad35035ce06b94ccd260ad1bf0 \
|
||||
|
@ -301,7 +301,7 @@ get_src 70e9a01eb32ccade0d5116a25bcffde0445b94ad35035ce06b94ccd260ad1bf0 \
|
|||
get_src 9fcb6db95bc37b6fce77d3b3dc740d593f9d90dce0369b405eb04844d56ac43f \
|
||||
"https://github.com/ledgetech/lua-resty-http/archive/$LUA_RESTY_HTTP.tar.gz"
|
||||
|
||||
get_src 42893da0e3de4ec180c9bf02f82608d78787290a70c5644b538f29d243147396 \
|
||||
get_src 02733575c4aed15f6cab662378e4b071c0a4a4d07940c4ef19a7319e9be943d4 \
|
||||
"https://github.com/openresty/lua-resty-memcached/archive/v$LUA_RESTY_MEMCACHED_VERSION.tar.gz"
|
||||
|
||||
get_src c15aed1a01c88a3a6387d9af67a957dff670357f5fdb4ee182beb44635eef3f1 \
|
||||
|
|
|
@ -8,8 +8,6 @@ steps:
|
|||
entrypoint: bash
|
||||
env:
|
||||
- DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
- SHORT_SHA=$SHORT_SHA
|
||||
- BASE_REF=$_PULL_BASE_REF
|
||||
- REGISTRY=gcr.io/k8s-staging-ingress-nginx
|
||||
# default cloudbuild has HOME=/builder/home and docker buildx is in /root/.docker/cli-plugins/docker-buildx
|
||||
# set the home to /root explicitly to if using docker buildx
|
||||
|
@ -19,6 +17,3 @@ steps:
|
|||
- |
|
||||
gcloud auth configure-docker \
|
||||
&& cd images/opentelemetry && make push
|
||||
substitutions:
|
||||
_GIT_TAG: "12345"
|
||||
_PULL_BASE_REF: "main"
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
# limitations under the License.
|
||||
|
||||
|
||||
FROM alpine:3.18.0 as base
|
||||
FROM alpine:3.18.2 as base
|
||||
|
||||
RUN mkdir -p /opt/third_party/install
|
||||
COPY . /opt/third_party/
|
||||
|
|
|
@ -43,7 +43,7 @@ image:
|
|||
--pull \
|
||||
--push \
|
||||
--build-arg BASE_IMAGE=${NGINX_BASE_IMAGE} \
|
||||
--build-arg GOLANG_VERSION=1.20.5 \
|
||||
--build-arg GOLANG_VERSION=1.20.6 \
|
||||
--build-arg ETCD_VERSION=3.4.3-0 \
|
||||
--build-arg K8S_RELEASE=v1.26.0 \
|
||||
--build-arg RESTY_CLI_VERSION=0.27 \
|
||||
|
@ -64,7 +64,7 @@ build: ensure-buildx
|
|||
--progress=${PROGRESS} \
|
||||
--pull \
|
||||
--build-arg BASE_IMAGE=${NGINX_BASE_IMAGE} \
|
||||
--build-arg GOLANG_VERSION=1.20.5 \
|
||||
--build-arg GOLANG_VERSION=1.20.6 \
|
||||
--build-arg ETCD_VERSION=3.4.3-0 \
|
||||
--build-arg K8S_RELEASE=v1.26.0 \
|
||||
--build-arg RESTY_CLI_VERSION=0.27 \
|
||||
|
|
|
@ -27,19 +27,44 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
serverAliasAnnotation = "server-alias"
|
||||
)
|
||||
|
||||
var aliasAnnotation = parser.Annotation{
|
||||
Group: "alias",
|
||||
Annotations: parser.AnnotationFields{
|
||||
serverAliasAnnotation: {
|
||||
Validator: parser.ValidateArrayOfServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh, // High as this allows regex chars
|
||||
Documentation: `this annotation can be used to define additional server
|
||||
aliases for this Ingress`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type alias struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new Alias annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return alias{r}
|
||||
return alias{
|
||||
r: r,
|
||||
annotationConfig: aliasAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
func (a alias) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an alias to the provided hosts
|
||||
func (a alias) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
val, err := parser.GetStringAnnotation("server-alias", ing)
|
||||
val, err := parser.GetStringAnnotation(serverAliasAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return []string{}, err
|
||||
}
|
||||
|
@ -61,3 +86,8 @@ func (a alias) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func (a alias) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, aliasAnnotation.Annotations)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
var annotation = parser.GetAnnotationWithPrefix("server-alias")
|
||||
var annotation = parser.GetAnnotationWithPrefix(serverAliasAnnotation)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
|
@ -36,16 +36,20 @@ func TestParse(t *testing.T) {
|
|||
}
|
||||
|
||||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
annotations map[string]string
|
||||
expected []string
|
||||
skipValidation bool
|
||||
wantErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}},
|
||||
{map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}},
|
||||
{map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}},
|
||||
{map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}},
|
||||
{map[string]string{annotation: ""}, []string{}},
|
||||
{map[string]string{}, []string{}},
|
||||
{nil, []string{}},
|
||||
{map[string]string{annotation: "a.com, b.com, , c.com"}, []string{"a.com", "b.com", "c.com"}, false, false},
|
||||
{map[string]string{annotation: "www.example.com"}, []string{"www.example.com"}, false, false},
|
||||
{map[string]string{annotation: "*.example.com,www.example.*"}, []string{"*.example.com", "www.example.*"}, false, false},
|
||||
{map[string]string{annotation: `~^www\d+\.example\.com$`}, []string{`~^www\d+\.example\.com$`}, false, false},
|
||||
{map[string]string{annotation: `www.xpto;lala`}, []string{}, false, true},
|
||||
{map[string]string{annotation: `www.xpto;lala`}, []string{"www.xpto;lala"}, true, false}, // When we skip validation no error should happen
|
||||
{map[string]string{annotation: ""}, []string{}, false, true},
|
||||
{map[string]string{}, []string{}, false, true},
|
||||
{nil, []string{}, false, true},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -58,7 +62,16 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
if testCase.skipValidation {
|
||||
parser.EnableAnnotationValidation = false
|
||||
}
|
||||
defer func() {
|
||||
parser.EnableAnnotationValidation = true
|
||||
}()
|
||||
result, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.wantErr {
|
||||
t.Errorf("ParseAliasAnnotation() annotation: %s, error = %v, wantErr %v", testCase.annotations, err, testCase.wantErr)
|
||||
}
|
||||
if !reflect.DeepEqual(result, testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
|
|
|
@ -45,8 +45,8 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/annotations/fastcgi"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/globalratelimit"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/http2pushpreload"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipallowlist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipdenylist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/ipwhitelist"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/loadbalancing"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/log"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/mirror"
|
||||
|
@ -111,7 +111,6 @@ type Ingress struct {
|
|||
UpstreamHashBy upstreamhashby.Config
|
||||
LoadBalancing string
|
||||
UpstreamVhost string
|
||||
Whitelist ipwhitelist.SourceRange
|
||||
Denylist ipdenylist.SourceRange
|
||||
XForwardedPrefix string
|
||||
SSLCipher sslcipher.Config
|
||||
|
@ -119,6 +118,7 @@ type Ingress struct {
|
|||
ModSecurity modsecurity.Config
|
||||
Mirror mirror.Config
|
||||
StreamSnippet string
|
||||
Allowlist ipallowlist.SourceRange
|
||||
}
|
||||
|
||||
// Extractor defines the annotation parsers to be used in the extraction of annotations
|
||||
|
@ -162,7 +162,7 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
"UpstreamHashBy": upstreamhashby.NewParser(cfg),
|
||||
"LoadBalancing": loadbalancing.NewParser(cfg),
|
||||
"UpstreamVhost": upstreamvhost.NewParser(cfg),
|
||||
"Whitelist": ipwhitelist.NewParser(cfg),
|
||||
"Allowlist": ipallowlist.NewParser(cfg),
|
||||
"Denylist": ipdenylist.NewParser(cfg),
|
||||
"XForwardedPrefix": xforwardedprefix.NewParser(cfg),
|
||||
"SSLCipher": sslcipher.NewParser(cfg),
|
||||
|
@ -176,16 +176,23 @@ func NewAnnotationExtractor(cfg resolver.Resolver) Extractor {
|
|||
}
|
||||
|
||||
// Extract extracts the annotations from an Ingress
|
||||
func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
|
||||
func (e Extractor) Extract(ing *networking.Ingress) (*Ingress, error) {
|
||||
pia := &Ingress{
|
||||
ObjectMeta: ing.ObjectMeta,
|
||||
}
|
||||
|
||||
data := make(map[string]interface{})
|
||||
for name, annotationParser := range e.annotations {
|
||||
if err := annotationParser.Validate(ing.GetAnnotations()); err != nil {
|
||||
return nil, errors.NewRiskyAnnotations(name)
|
||||
}
|
||||
val, err := annotationParser.Parse(ing)
|
||||
klog.V(5).InfoS("Parsing Ingress annotation", "name", name, "ingress", klog.KObj(ing), "value", val)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.ErrorS(err, "ingress contains invalid annotation value")
|
||||
return nil, err
|
||||
}
|
||||
if errors.IsMissingAnnotations(err) {
|
||||
continue
|
||||
}
|
||||
|
@ -223,5 +230,5 @@ func (e Extractor) Extract(ing *networking.Ingress) *Ingress {
|
|||
klog.ErrorS(err, "unexpected error merging extracted annotations")
|
||||
}
|
||||
|
||||
return pia
|
||||
return pia, nil
|
||||
}
|
||||
|
|
|
@ -134,8 +134,11 @@ func TestSSLPassthrough(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).SSLPassthrough
|
||||
if r != foo.er {
|
||||
r, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Errors should be null: %v", err)
|
||||
}
|
||||
if r.SSLPassthrough != foo.er {
|
||||
t.Errorf("Returned %v but expected %v", r, foo.er)
|
||||
}
|
||||
}
|
||||
|
@ -158,8 +161,11 @@ func TestUpstreamHashBy(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).UpstreamHashBy.UpstreamHashBy
|
||||
if r != foo.er {
|
||||
r, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
if r.UpstreamHashBy.UpstreamHashBy != foo.er {
|
||||
t.Errorf("Returned %v but expected %v", r, foo.er)
|
||||
}
|
||||
}
|
||||
|
@ -185,7 +191,11 @@ func TestAffinitySession(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).SessionAffinity
|
||||
rann, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
r := rann.SessionAffinity
|
||||
t.Logf("Testing pass %v %v", foo.affinitytype, foo.cookiename)
|
||||
|
||||
if r.Type != foo.affinitytype {
|
||||
|
@ -228,7 +238,11 @@ func TestCors(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).CorsConfig
|
||||
rann, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
r := rann.CorsConfig
|
||||
t.Logf("Testing pass %v %v %v %v %v", foo.corsenabled, foo.methods, foo.headers, foo.origin, foo.credentials)
|
||||
|
||||
if r.CorsEnabled != foo.corsenabled {
|
||||
|
@ -277,7 +291,11 @@ func TestCustomHTTPErrors(t *testing.T) {
|
|||
|
||||
for _, foo := range fooAnns {
|
||||
ing.SetAnnotations(foo.annotations)
|
||||
r := ec.Extract(ing).CustomHTTPErrors
|
||||
rann, err := ec.Extract(ing)
|
||||
if err != nil {
|
||||
t.Errorf("error should be null: %v", err)
|
||||
}
|
||||
r := rann.CustomHTTPErrors
|
||||
|
||||
// Check that expected codes were created
|
||||
for i := range foo.er {
|
||||
|
|
|
@ -32,13 +32,56 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/file"
|
||||
)
|
||||
|
||||
const (
|
||||
authSecretTypeAnnotation = "auth-secret-type" //#nosec G101
|
||||
authRealmAnnotation = "auth-realm"
|
||||
authTypeAnnotation = "auth-type"
|
||||
// This should be exported as it is imported by other packages
|
||||
AuthSecretAnnotation = "auth-secret" //#nosec G101
|
||||
)
|
||||
|
||||
var (
|
||||
authTypeRegex = regexp.MustCompile(`basic|digest`)
|
||||
authTypeRegex = regexp.MustCompile(`basic|digest`)
|
||||
authSecretTypeRegex = regexp.MustCompile(`auth-file|auth-map`)
|
||||
|
||||
// AuthDirectory default directory used to store files
|
||||
// to authenticate request
|
||||
AuthDirectory = "/etc/ingress-controller/auth"
|
||||
)
|
||||
|
||||
var AuthSecretConfig = parser.AnnotationConfig{
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation defines the name of the Secret that contains the usernames and passwords which are granted access to the paths defined in the Ingress rules. `,
|
||||
}
|
||||
|
||||
var authSecretAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
AuthSecretAnnotation: AuthSecretConfig,
|
||||
authSecretTypeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*authSecretTypeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation what is the format of auth-secret value. Can be "auth-file" that defines the content of an htpasswd file, or "auth-map" where each key
|
||||
is a user and each value is the password.`,
|
||||
},
|
||||
authRealmAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.CharsWithSpace, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation defines the realm (message) that should be shown to user when authentication is requested.`,
|
||||
},
|
||||
authTypeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*authTypeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the basic authentication type. Should be "basic" or "digest"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
const (
|
||||
fileAuth = "auth-file"
|
||||
mapAuth = "auth-map"
|
||||
|
@ -85,13 +128,18 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
}
|
||||
|
||||
type auth struct {
|
||||
r resolver.Resolver
|
||||
authDirectory string
|
||||
r resolver.Resolver
|
||||
authDirectory string
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication annotation parser
|
||||
func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotation {
|
||||
return auth{r, authDirectory}
|
||||
return auth{
|
||||
r: r,
|
||||
authDirectory: authDirectory,
|
||||
annotationConfig: authSecretAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -99,7 +147,7 @@ func NewParser(authDirectory string, r resolver.Resolver) parser.IngressAnnotati
|
|||
// and generated an htpasswd compatible file to be used as source
|
||||
// during the authentication process
|
||||
func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
at, err := parser.GetStringAnnotation("auth-type", ing)
|
||||
at, err := parser.GetStringAnnotation(authTypeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -109,12 +157,15 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
|
||||
var secretType string
|
||||
secretType, err = parser.GetStringAnnotation("auth-secret-type", ing)
|
||||
secretType, err = parser.GetStringAnnotation(authSecretTypeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
secretType = fileAuth
|
||||
}
|
||||
|
||||
s, err := parser.GetStringAnnotation("auth-secret", ing)
|
||||
s, err := parser.GetStringAnnotation(AuthSecretAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("error reading secret name from annotation: %w", err),
|
||||
|
@ -131,6 +182,13 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
if sns == "" {
|
||||
sns = ing.Namespace
|
||||
}
|
||||
secCfg := a.r.GetSecurityConfiguration()
|
||||
// We don't accept different namespaces for secrets.
|
||||
if !secCfg.AllowCrossNamespaceResources && sns != ing.Namespace {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%v/%v", sns, sname)
|
||||
secret, err := a.r.GetSecret(name)
|
||||
|
@ -140,7 +198,10 @@ func (a auth) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
realm, _ := parser.GetStringAnnotation("auth-realm", ing)
|
||||
realm, err := parser.GetStringAnnotation(authRealmAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
passFilename := fmt.Sprintf("%v/%v-%v-%v.passwd", a.authDirectory, ing.GetNamespace(), ing.UID, secret.UID)
|
||||
|
||||
|
@ -210,3 +271,12 @@ func dumpSecretAuthMap(filename string, secret *api.Secret) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a auth) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a auth) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, authSecretAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@ import (
|
|||
api "k8s.io/api/core/v1"
|
||||
networking "k8s.io/api/networking/v1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
|
@ -79,13 +80,18 @@ type mockSecret struct {
|
|||
}
|
||||
|
||||
func (m mockSecret) GetSecret(name string) (*api.Secret, error) {
|
||||
if name != "default/demo-secret" {
|
||||
if name != "default/demo-secret" && name != "otherns/demo-secret" {
|
||||
return nil, fmt.Errorf("there is no secret with name %v", name)
|
||||
}
|
||||
|
||||
ns, _, err := cache.SplitMetaNamespaceKey(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &api.Secret{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Namespace: api.NamespaceDefault,
|
||||
Namespace: ns,
|
||||
Name: "demo-secret",
|
||||
},
|
||||
Data: map[string][]byte{"auth": []byte("foo:$apr1$OFG3Xybp$ckL0FHDAkoXYIlH9.cysT0")},
|
||||
|
@ -106,13 +112,91 @@ func TestIngressAuthBadAuthType(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "invalid"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "invalid"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.NewLocationDenied("invalid authentication type")
|
||||
expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-type")
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidRealm(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "something weird ; location trying to { break }"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.NewValidationError("nginx.ingress.kubernetes.io/auth-realm")
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidDifferentNamespace(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.LocationDenied{
|
||||
Reason: errors.New("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidDifferentNamespaceAllowed(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "otherns/demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
r := mockSecret{}
|
||||
r.AllowCrossNamespace = true
|
||||
_, err := NewParser(dir, r).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("not expecting an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressInvalidSecretName(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret;xpto"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
expected := ing_errors.LocationDenied{
|
||||
Reason: errors.New("error reading secret name from annotation: annotation nginx.ingress.kubernetes.io/auth-secret contains invalid value"),
|
||||
}
|
||||
_, err := NewParser(dir, &mockSecret{}).Parse(ing)
|
||||
if err.Error() != expected.Error() {
|
||||
t.Errorf("expected '%v' but got '%v'", expected, err)
|
||||
|
@ -123,7 +207,7 @@ func TestInvalidIngressAuthNoSecret(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
@ -142,9 +226,9 @@ func TestIngressAuth(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
@ -173,9 +257,9 @@ func TestIngressAuthWithoutSecret(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret")] = "invalid-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "invalid-secret"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
@ -191,10 +275,10 @@ func TestIngressAuthInvalidSecretKey(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("auth-type")] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-secret-type")] = "invalid-type"
|
||||
data[parser.GetAnnotationWithPrefix("auth-realm")] = "-realm-"
|
||||
data[parser.GetAnnotationWithPrefix(authTypeAnnotation)] = "basic"
|
||||
data[parser.GetAnnotationWithPrefix(AuthSecretAnnotation)] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(authSecretTypeAnnotation)] = "invalid-type"
|
||||
data[parser.GetAnnotationWithPrefix(authRealmAnnotation)] = "-realm-"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, dir, _ := dummySecretContent(t)
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
|
@ -31,6 +32,118 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
authReqURLAnnotation = "auth-url"
|
||||
authReqMethodAnnotation = "auth-method"
|
||||
authReqSigninAnnotation = "auth-signin"
|
||||
authReqSigninRedirParamAnnotation = "auth-signin-redirect-param"
|
||||
authReqSnippetAnnotation = "auth-snippet"
|
||||
authReqCacheKeyAnnotation = "auth-cache-key"
|
||||
authReqKeepaliveAnnotation = "auth-keepalive"
|
||||
authReqKeepaliveRequestsAnnotation = "auth-keepalive-requests"
|
||||
authReqKeepaliveTimeout = "auth-keepalive-timeout"
|
||||
authReqCacheDuration = "auth-cache-duration"
|
||||
authReqResponseHeadersAnnotation = "auth-response-headers"
|
||||
authReqProxySetHeadersAnnotation = "auth-proxy-set-headers"
|
||||
authReqRequestRedirectAnnotation = "auth-request-redirect"
|
||||
authReqAlwaysSetCookieAnnotation = "auth-always-set-cookie"
|
||||
|
||||
// This should be exported as it is imported by other packages
|
||||
AuthSecretAnnotation = "auth-secret"
|
||||
)
|
||||
|
||||
var authReqAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
authReqURLAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to indicate the URL where the HTTP request should be sent`,
|
||||
},
|
||||
authReqMethodAnnotation: {
|
||||
Validator: parser.ValidateRegex(*methodsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows to specify the HTTP method to use`,
|
||||
},
|
||||
authReqSigninAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLWithNginxVariableRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to specify the location of the error page`,
|
||||
},
|
||||
authReqSigninRedirParamAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the URL parameter in the error page which should contain the original URL for a failed signin request`,
|
||||
},
|
||||
authReqSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskCritical,
|
||||
Documentation: `This annotation allows to specify a custom snippet to use with external authentication`,
|
||||
},
|
||||
authReqCacheKeyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation enables caching for auth requests.`,
|
||||
},
|
||||
authReqKeepaliveAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation specifies the maximum number of keepalive connections to auth-url. Only takes effect when no variables are used in the host part of the URL`,
|
||||
},
|
||||
authReqKeepaliveRequestsAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the maximum number of requests that can be served through one keepalive connection`,
|
||||
},
|
||||
authReqKeepaliveTimeout: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation specifies a duration in seconds which an idle keepalive connection to an upstream server will stay open`,
|
||||
},
|
||||
authReqCacheDuration: {
|
||||
Validator: parser.ValidateRegex(*parser.ExtendedCharsRegex, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify a caching time for auth responses based on their response codes, e.g. 200 202 30m`,
|
||||
},
|
||||
authReqResponseHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the headers to pass to backend once authentication request completes. They should be separated by comma.`,
|
||||
},
|
||||
authReqProxySetHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the name of a ConfigMap that specifies headers to pass to the authentication service.
|
||||
Only ConfigMaps on the same namespace are allowed`,
|
||||
},
|
||||
authReqRequestRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the X-Auth-Request-Redirect header value`,
|
||||
},
|
||||
authReqAlwaysSetCookieAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables setting a cookie returned by auth request.
|
||||
By default, the cookie will be set only if an upstream reports with the code 200, 201, 204, 206, 301, 302, 303, 304, 307, or 308`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns external authentication configuration for an Ingress rule
|
||||
type Config struct {
|
||||
URL string `json:"url"`
|
||||
|
@ -121,7 +234,7 @@ func (e1 *Config) Equal(e2 *Config) bool {
|
|||
}
|
||||
|
||||
var (
|
||||
methods = []string{"GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "CONNECT", "OPTIONS", "TRACE"}
|
||||
methodsRegex = regexp.MustCompile("(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)")
|
||||
headerRegexp = regexp.MustCompile(`^[a-zA-Z\d\-_]+$`)
|
||||
statusCodeRegex = regexp.MustCompile(`^[\d]{3}$`)
|
||||
durationRegex = regexp.MustCompile(`^[\d]+(ms|s|m|h|d|w|M|y)$`) // see http://nginx.org/en/docs/syntax.html
|
||||
|
@ -129,16 +242,7 @@ var (
|
|||
|
||||
// ValidMethod checks is the provided string a valid HTTP method
|
||||
func ValidMethod(method string) bool {
|
||||
if len(method) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, m := range methods {
|
||||
if method == m {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return methodsRegex.MatchString(method)
|
||||
}
|
||||
|
||||
// ValidHeader checks is the provided string satisfies the header's name regex
|
||||
|
@ -173,19 +277,23 @@ func ValidCacheDuration(duration string) bool {
|
|||
}
|
||||
|
||||
type authReq struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return authReq{r}
|
||||
return authReq{
|
||||
r: r,
|
||||
annotationConfig: authReqAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to use an Config URL as source for authentication
|
||||
func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
// Required Parameters
|
||||
urlString, err := parser.GetStringAnnotation("auth-url", ing)
|
||||
urlString, err := parser.GetStringAnnotation(authReqURLAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -195,33 +303,44 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return nil, ing_errors.LocationDenied{Reason: fmt.Errorf("could not parse auth-url annotation: %v", err)}
|
||||
}
|
||||
|
||||
authMethod, _ := parser.GetStringAnnotation("auth-method", ing)
|
||||
if len(authMethod) != 0 && !ValidMethod(authMethod) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
authMethod, err := parser.GetStringAnnotation(authReqMethodAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return nil, ing_errors.NewLocationDenied("invalid HTTP method")
|
||||
}
|
||||
}
|
||||
|
||||
// Optional Parameters
|
||||
signIn, err := parser.GetStringAnnotation("auth-signin", ing)
|
||||
signIn, err := parser.GetStringAnnotation(authReqSigninAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqSigninAnnotation, err)
|
||||
}
|
||||
klog.V(3).InfoS("auth-signin annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
signInRedirectParam, err := parser.GetStringAnnotation("auth-signin-redirect-param", ing)
|
||||
signInRedirectParam, err := parser.GetStringAnnotation(authReqSigninRedirParamAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqSigninRedirParamAnnotation, err)
|
||||
}
|
||||
klog.V(3).Infof("auth-signin-redirect-param annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
authSnippet, err := parser.GetStringAnnotation("auth-snippet", ing)
|
||||
authSnippet, err := parser.GetStringAnnotation(authReqSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-snippet annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
authCacheKey, err := parser.GetStringAnnotation("auth-cache-key", ing)
|
||||
authCacheKey, err := parser.GetStringAnnotation(authReqCacheKeyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
klog.Warningf("%s value is invalid: %s", authReqCacheKeyAnnotation, err)
|
||||
}
|
||||
klog.V(3).InfoS("auth-cache-key annotation is undefined and will not be set")
|
||||
}
|
||||
|
||||
keepaliveConnections, err := parser.GetIntAnnotation("auth-keepalive", ing)
|
||||
keepaliveConnections, err := parser.GetIntAnnotation(authReqKeepaliveAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive annotation is undefined and will be set to its default value")
|
||||
keepaliveConnections = defaultKeepaliveConnections
|
||||
|
@ -238,9 +357,9 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
keepaliveRequests, err := parser.GetIntAnnotation("auth-keepalive-requests", ing)
|
||||
keepaliveRequests, err := parser.GetIntAnnotation(authReqKeepaliveRequestsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined and will be set to its default value")
|
||||
klog.V(3).InfoS("auth-keepalive-requests annotation is undefined or invalid and will be set to its default value")
|
||||
keepaliveRequests = defaultKeepaliveRequests
|
||||
}
|
||||
if keepaliveRequests <= 0 {
|
||||
|
@ -248,7 +367,7 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
keepaliveConnections = 0
|
||||
}
|
||||
|
||||
keepaliveTimeout, err := parser.GetIntAnnotation("auth-keepalive-timeout", ing)
|
||||
keepaliveTimeout, err := parser.GetIntAnnotation(authReqKeepaliveTimeout, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-keepalive-timeout annotation is undefined and will be set to its default value")
|
||||
keepaliveTimeout = defaultKeepaliveTimeout
|
||||
|
@ -258,14 +377,20 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
keepaliveConnections = 0
|
||||
}
|
||||
|
||||
durstr, _ := parser.GetStringAnnotation("auth-cache-duration", ing)
|
||||
durstr, err := parser.GetStringAnnotation(authReqCacheDuration, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s contains invalid value", authReqCacheDuration)
|
||||
}
|
||||
authCacheDuration, err := ParseStringToCacheDurations(durstr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
responseHeaders := []string{}
|
||||
hstr, _ := parser.GetStringAnnotation("auth-response-headers", ing)
|
||||
hstr, err := parser.GetStringAnnotation(authReqResponseHeadersAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, ing_errors.NewLocationDenied("validation error")
|
||||
}
|
||||
if len(hstr) != 0 {
|
||||
harr := strings.Split(hstr, ",")
|
||||
for _, header := range harr {
|
||||
|
@ -279,9 +404,28 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
proxySetHeaderMap, err := parser.GetStringAnnotation("auth-proxy-set-headers", ing)
|
||||
proxySetHeaderMap, err := parser.GetStringAnnotation(authReqProxySetHeadersAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set")
|
||||
klog.V(3).InfoS("auth-set-proxy-headers annotation is undefined and will not be set", "err", err)
|
||||
}
|
||||
|
||||
cns, _, err := cache.SplitMetaNamespaceKey(proxySetHeaderMap)
|
||||
if err != nil {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("error reading configmap name %s from annotation: %w", proxySetHeaderMap, err),
|
||||
}
|
||||
}
|
||||
|
||||
if cns == "" {
|
||||
cns = ing.Namespace
|
||||
}
|
||||
|
||||
secCfg := a.r.GetSecurityConfiguration()
|
||||
// We don't accept different namespaces for secrets.
|
||||
if !secCfg.AllowCrossNamespaceResources && cns != ing.Namespace {
|
||||
return nil, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("cross namespace usage of secrets is not allowed"),
|
||||
}
|
||||
}
|
||||
|
||||
var proxySetHeaders map[string]string
|
||||
|
@ -301,9 +445,15 @@ func (a authReq) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
proxySetHeaders = proxySetHeadersMapContents.Data
|
||||
}
|
||||
|
||||
requestRedirect, _ := parser.GetStringAnnotation("auth-request-redirect", ing)
|
||||
requestRedirect, err := parser.GetStringAnnotation(authReqRequestRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s is invalid: %w", authReqRequestRedirectAnnotation, err)
|
||||
}
|
||||
|
||||
alwaysSetCookie, _ := parser.GetBoolAnnotation("auth-always-set-cookie", ing)
|
||||
alwaysSetCookie, err := parser.GetBoolAnnotation(authReqAlwaysSetCookieAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && ing_errors.IsValidationError(err) {
|
||||
return nil, fmt.Errorf("%s is invalid: %w", authReqAlwaysSetCookieAnnotation, err)
|
||||
}
|
||||
|
||||
return &Config{
|
||||
URL: urlString,
|
||||
|
@ -348,3 +498,12 @@ func ParseStringToCacheDurations(input string) ([]string, error) {
|
|||
}
|
||||
return authCacheDuration, nil
|
||||
}
|
||||
|
||||
func (a authReq) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a authReq) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, authReqAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -192,11 +192,13 @@ func TestHeaderAnnotations(t *testing.T) {
|
|||
i, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Error("expected error but retuned nil")
|
||||
t.Errorf("%v expected error but retuned nil", test.title)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Errorf("no error was expected but %v happened in %s", err, test.title)
|
||||
}
|
||||
u, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("%v: expected an External type", test.title)
|
||||
|
|
|
@ -23,23 +23,52 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableGlobalAuthAnnotation = "enable-global-auth"
|
||||
)
|
||||
|
||||
var globalAuthAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableGlobalAuthAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Defines if the global external authentication should be enabled.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type authReqGlobal struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new authentication request annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return authReqGlobal{r}
|
||||
return authReqGlobal{
|
||||
r: r,
|
||||
annotationConfig: globalAuthAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to enable or disable global external authentication
|
||||
func (a authReqGlobal) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
|
||||
enableGlobalAuth, err := parser.GetBoolAnnotation("enable-global-auth", ing)
|
||||
enableGlobalAuth, err := parser.GetBoolAnnotation(enableGlobalAuthAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
enableGlobalAuth = true
|
||||
}
|
||||
|
||||
return enableGlobalAuth, nil
|
||||
}
|
||||
|
||||
func (a authReqGlobal) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a authReqGlobal) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, globalAuthAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -32,13 +32,64 @@ import (
|
|||
const (
|
||||
defaultAuthTLSDepth = 1
|
||||
defaultAuthVerifyClient = "on"
|
||||
|
||||
annotationAuthTLSSecret = "auth-tls-secret" //#nosec G101
|
||||
annotationAuthTLSVerifyClient = "auth-tls-verify-client"
|
||||
annotationAuthTLSVerifyDepth = "auth-tls-verify-depth"
|
||||
annotationAuthTLSErrorPage = "auth-tls-error-page"
|
||||
annotationAuthTLSPassCertToUpstream = "auth-tls-pass-certificate-to-upstream" //#nosec G101
|
||||
annotationAuthTLSMatchCN = "auth-tls-match-cn"
|
||||
)
|
||||
|
||||
var (
|
||||
regexChars = regexp.QuoteMeta(`()|=`)
|
||||
authVerifyClientRegex = regexp.MustCompile(`on|off|optional|optional_no_ca`)
|
||||
commonNameRegex = regexp.MustCompile(`CN=`)
|
||||
commonNameRegex = regexp.MustCompile(`^CN=[/\-.\_\~a-zA-Z0-9` + regexChars + `]*$`)
|
||||
redirectRegex = regexp.MustCompile(`^((https?://)?[A-Za-z0-9\-\.]*(:[0-9]+)?/[A-Za-z0-9\-\.]*)?$`)
|
||||
)
|
||||
|
||||
var authTLSAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
annotationAuthTLSSecret: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation defines the secret that contains the certificate chain of allowed certs`,
|
||||
},
|
||||
annotationAuthTLSVerifyClient: {
|
||||
Validator: parser.ValidateRegex(*authVerifyClientRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium as it allows a subset of chars
|
||||
Documentation: `This annotation enables verification of client certificates. Can be "on", "off", "optional" or "optional_no_ca"`,
|
||||
},
|
||||
annotationAuthTLSVerifyDepth: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines validation depth between the provided client certificate and the Certification Authority chain.`,
|
||||
},
|
||||
annotationAuthTLSErrorPage: {
|
||||
Validator: parser.ValidateRegex(*redirectRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation defines the URL/Page that user should be redirected in case of a Certificate Authentication Error`,
|
||||
},
|
||||
annotationAuthTLSPassCertToUpstream: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the received certificates should be passed or not to the upstream server in the header "ssl-client-cert"`,
|
||||
},
|
||||
annotationAuthTLSMatchCN: {
|
||||
Validator: parser.ValidateRegex(*commonNameRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation adds a sanity check for the CN of the client certificate that is sent over using a string / regex starting with "CN="`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
// and the configured ValidationDepth
|
||||
type Config struct {
|
||||
|
@ -80,11 +131,15 @@ func (assl1 *Config) Equal(assl2 *Config) bool {
|
|||
|
||||
// NewParser creates a new TLS authentication annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return authTLS{resolver}
|
||||
return authTLS{
|
||||
r: resolver,
|
||||
annotationConfig: authTLSAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
type authTLS struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -93,15 +148,23 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
tlsauthsecret, err := parser.GetStringAnnotation("auth-tls-secret", ing)
|
||||
tlsauthsecret, err := parser.GetStringAnnotation(annotationAuthTLSSecret, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, err
|
||||
}
|
||||
|
||||
_, _, err = k8s.ParseNameNS(tlsauthsecret)
|
||||
ns, _, err := k8s.ParseNameNS(tlsauthsecret)
|
||||
if err != nil {
|
||||
return &Config{}, ing_errors.NewLocationDenied(err.Error())
|
||||
}
|
||||
if ns == "" {
|
||||
ns = ing.Namespace
|
||||
}
|
||||
secCfg := a.r.GetSecurityConfiguration()
|
||||
// We don't accept different namespaces for secrets.
|
||||
if !secCfg.AllowCrossNamespaceResources && ns != ing.Namespace {
|
||||
return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
}
|
||||
|
||||
authCert, err := a.r.GetAuthCertificate(tlsauthsecret)
|
||||
if err != nil {
|
||||
|
@ -110,30 +173,50 @@ func (a authTLS) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
config.AuthSSLCert = *authCert
|
||||
|
||||
config.VerifyClient, err = parser.GetStringAnnotation("auth-tls-verify-client", ing)
|
||||
config.VerifyClient, err = parser.GetStringAnnotation(annotationAuthTLSVerifyClient, ing, a.annotationConfig.Annotations)
|
||||
// We can set a default value here in case of validation error
|
||||
if err != nil || !authVerifyClientRegex.MatchString(config.VerifyClient) {
|
||||
config.VerifyClient = defaultAuthVerifyClient
|
||||
}
|
||||
|
||||
config.ValidationDepth, err = parser.GetIntAnnotation("auth-tls-verify-depth", ing)
|
||||
config.ValidationDepth, err = parser.GetIntAnnotation(annotationAuthTLSVerifyDepth, ing, a.annotationConfig.Annotations)
|
||||
// We can set a default value here in case of validation error
|
||||
if err != nil || config.ValidationDepth == 0 {
|
||||
config.ValidationDepth = defaultAuthTLSDepth
|
||||
}
|
||||
|
||||
config.ErrorPage, err = parser.GetStringAnnotation("auth-tls-error-page", ing)
|
||||
config.ErrorPage, err = parser.GetStringAnnotation(annotationAuthTLSErrorPage, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.ErrorPage = ""
|
||||
}
|
||||
|
||||
config.PassCertToUpstream, err = parser.GetBoolAnnotation("auth-tls-pass-certificate-to-upstream", ing)
|
||||
config.PassCertToUpstream, err = parser.GetBoolAnnotation(annotationAuthTLSPassCertToUpstream, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.PassCertToUpstream = false
|
||||
}
|
||||
|
||||
config.MatchCN, err = parser.GetStringAnnotation("auth-tls-match-cn", ing)
|
||||
if err != nil || !commonNameRegex.MatchString(config.MatchCN) {
|
||||
config.MatchCN, err = parser.GetStringAnnotation(annotationAuthTLSMatchCN, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return &Config{}, err
|
||||
}
|
||||
config.MatchCN = ""
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a authTLS) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a authTLS) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, authTLSAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -132,11 +132,11 @@ func TestAnnotations(t *testing.T) {
|
|||
t.Errorf("expected empty string, but got %v", u.MatchCN)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "off"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "2"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-error-page")] = "ok.com/error"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "CN=hello-app"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "off"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "2"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSErrorPage)] = "ok.com/error"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "CN=(hello-app|ok|goodbye)"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -165,8 +165,8 @@ func TestAnnotations(t *testing.T) {
|
|||
if u.PassCertToUpstream != true {
|
||||
t.Errorf("expected %v but got %v", true, u.PassCertToUpstream)
|
||||
}
|
||||
if u.MatchCN != "CN=hello-app" {
|
||||
t.Errorf("expected %v but got %v", "CN=hello-app", u.MatchCN)
|
||||
if u.MatchCN != "CN=(hello-app|ok|goodbye)" {
|
||||
t.Errorf("expected %v but got %v", "CN=(hello-app|ok|goodbye)", u.MatchCN)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -182,15 +182,24 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Invalid NameSpace
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress but got nil")
|
||||
}
|
||||
|
||||
// Invalid Cross NameSpace
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "nondefault/demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
expErr := errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
if err.Error() != expErr.Error() {
|
||||
t.Errorf("received error is different from cross namespace error: %s Expected %s", err, expErr)
|
||||
}
|
||||
|
||||
// Invalid Auth Certificate
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/invalid-demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/invalid-demo-secret"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
|
@ -198,11 +207,38 @@ func TestInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Invalid optional Annotations
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-client")] = "w00t"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-verify-depth")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-pass-certificate-to-upstream")] = "nahh"
|
||||
data[parser.GetAnnotationWithPrefix("auth-tls-match-cn")] = "<script>nope</script>"
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSSecret)] = "default/demo-secret"
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyClient)] = "w00t"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Error should be nil and verify client should be defaulted")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSVerifyDepth)] = "abcd"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("Error should be nil and verify depth should be defaulted")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream)] = "nahh"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress but got nil")
|
||||
}
|
||||
delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSPassCertToUpstream))
|
||||
|
||||
data[parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN)] = "<script>nope</script>"
|
||||
ing.SetAnnotations(data)
|
||||
_, err = NewParser(fakeSecret).Parse(ing)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error with ingress CN but got nil")
|
||||
}
|
||||
delete(data, parser.GetAnnotationWithPrefix(annotationAuthTLSMatchCN))
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(fakeSecret).Parse(ing)
|
||||
|
|
|
@ -17,49 +17,72 @@ limitations under the License.
|
|||
package backendprotocol
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
// HTTP protocol
|
||||
const HTTP = "HTTP"
|
||||
|
||||
var (
|
||||
validProtocols = regexp.MustCompile(`^(AUTO_HTTP|HTTP|HTTPS|GRPC|GRPCS|FCGI)$`)
|
||||
validProtocols = []string{"auto_http", "http", "https", "grpc", "grpcs", "fcgi"}
|
||||
)
|
||||
|
||||
const (
|
||||
http = "HTTP"
|
||||
backendProtocolAnnotation = "backend-protocol"
|
||||
)
|
||||
|
||||
var backendProtocolConfig = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
backendProtocolAnnotation: {
|
||||
Validator: parser.ValidateOptions(validProtocols, false, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `this annotation can be used to define which protocol should
|
||||
be used to communicate with backends`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type backendProtocol struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new backend protocol annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return backendProtocol{r}
|
||||
return backendProtocol{
|
||||
r: r,
|
||||
annotationConfig: backendProtocolConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (a backendProtocol) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to indicate the backend protocol.
|
||||
func (a backendProtocol) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
if ing.GetAnnotations() == nil {
|
||||
return HTTP, nil
|
||||
return http, nil
|
||||
}
|
||||
|
||||
proto, err := parser.GetStringAnnotation("backend-protocol", ing)
|
||||
proto, err := parser.GetStringAnnotation(backendProtocolAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return HTTP, nil
|
||||
}
|
||||
|
||||
proto = strings.TrimSpace(strings.ToUpper(proto))
|
||||
if !validProtocols.MatchString(proto) {
|
||||
klog.Warningf("Protocol %v is not a valid value for the backend-protocol annotation. Using HTTP as protocol", proto)
|
||||
return HTTP, nil
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("validation error %s. Using HTTP as protocol", err)
|
||||
}
|
||||
return http, nil
|
||||
}
|
||||
|
||||
return proto, nil
|
||||
}
|
||||
|
||||
func (a backendProtocol) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, backendProtocolConfig.Annotations)
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ func TestParseInvalidAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
// Test invalid annotation set
|
||||
data[parser.GetAnnotationWithPrefix("backend-protocol")] = "INVALID"
|
||||
data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = "INVALID"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err = NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -97,7 +97,7 @@ func TestParseAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("backend-protocol")] = "HTTPS"
|
||||
data[parser.GetAnnotationWithPrefix(backendProtocolAnnotation)] = " HTTPS "
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -18,14 +18,82 @@ package canary
|
|||
|
||||
import (
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
canaryAnnotation = "canary"
|
||||
canaryWeightAnnotation = "canary-weight"
|
||||
canaryWeightTotalAnnotation = "canary-weight-total"
|
||||
canaryByHeaderAnnotation = "canary-by-header"
|
||||
canaryByHeaderValueAnnotation = "canary-by-header-value"
|
||||
canaryByHeaderPatternAnnotation = "canary-by-header-pattern"
|
||||
canaryByCookieAnnotation = "canary-by-cookie"
|
||||
)
|
||||
|
||||
var CanaryAnnotations = parser.Annotation{
|
||||
Group: "canary",
|
||||
Annotations: parser.AnnotationFields{
|
||||
canaryAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables the Ingress spec to act as an alternative service for requests to route to depending on the rules applied`,
|
||||
},
|
||||
canaryWeightAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the integer based (0 - ) percent of random requests that should be routed to the service specified in the canary Ingress`,
|
||||
},
|
||||
canaryWeightTotalAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation The total weight of traffic. If unspecified, it defaults to 100`,
|
||||
},
|
||||
canaryByHeaderAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the header that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress.
|
||||
When the request header is set to 'always', it will be routed to the canary. When the header is set to 'never', it will never be routed to the canary.
|
||||
For any other value, the header will be ignored and the request compared against the other canary rules by precedence`,
|
||||
},
|
||||
canaryByHeaderValueAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the header value to match for notifying the Ingress to route the request to the service specified in the Canary Ingress.
|
||||
When the request header is set to this value, it will be routed to the canary. For any other header value, the header will be ignored and the request compared against the other canary rules by precedence.
|
||||
This annotation has to be used together with 'canary-by-header'. The annotation is an extension of the 'canary-by-header' to allow customizing the header value instead of using hardcoded values.
|
||||
It doesn't have any effect if the 'canary-by-header' annotation is not defined`,
|
||||
},
|
||||
canaryByHeaderPatternAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.IsValidRegex, false),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation works the same way as canary-by-header-value except it does PCRE Regex matching.
|
||||
Note that when 'canary-by-header-value' is set this annotation will be ignored.
|
||||
When the given Regex causes error during request processing, the request will be considered as not matching.`,
|
||||
},
|
||||
canaryByCookieAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the cookie that should be used for notifying the Ingress to route the request to the service specified in the Canary Ingress.
|
||||
When the cookie is set to 'always', it will be routed to the canary. When the cookie is set to 'never', it will never be routed to the canary`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type canary struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config returns the configuration rules for setting up the Canary
|
||||
|
@ -41,7 +109,10 @@ type Config struct {
|
|||
|
||||
// NewParser parses the ingress for canary related annotations
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return canary{r}
|
||||
return canary{
|
||||
r: r,
|
||||
annotationConfig: CanaryAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -50,45 +121,75 @@ func (c canary) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
config := &Config{}
|
||||
var err error
|
||||
|
||||
config.Enabled, err = parser.GetBoolAnnotation("canary", ing)
|
||||
config.Enabled, err = parser.GetBoolAnnotation(canaryAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to 'false'", canaryAnnotation)
|
||||
}
|
||||
config.Enabled = false
|
||||
}
|
||||
|
||||
config.Weight, err = parser.GetIntAnnotation("canary-weight", ing)
|
||||
config.Weight, err = parser.GetIntAnnotation(canaryWeightAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to '0'", canaryWeightAnnotation)
|
||||
}
|
||||
config.Weight = 0
|
||||
}
|
||||
|
||||
config.WeightTotal, err = parser.GetIntAnnotation("canary-weight-total", ing)
|
||||
config.WeightTotal, err = parser.GetIntAnnotation(canaryWeightTotalAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to '100'", canaryWeightTotalAnnotation)
|
||||
}
|
||||
config.WeightTotal = 100
|
||||
}
|
||||
|
||||
config.Header, err = parser.GetStringAnnotation("canary-by-header", ing)
|
||||
config.Header, err = parser.GetStringAnnotation(canaryByHeaderAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderAnnotation)
|
||||
}
|
||||
config.Header = ""
|
||||
}
|
||||
|
||||
config.HeaderValue, err = parser.GetStringAnnotation("canary-by-header-value", ing)
|
||||
config.HeaderValue, err = parser.GetStringAnnotation(canaryByHeaderValueAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderValueAnnotation)
|
||||
}
|
||||
config.HeaderValue = ""
|
||||
}
|
||||
|
||||
config.HeaderPattern, err = parser.GetStringAnnotation("canary-by-header-pattern", ing)
|
||||
config.HeaderPattern, err = parser.GetStringAnnotation(canaryByHeaderPatternAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByHeaderPatternAnnotation)
|
||||
}
|
||||
config.HeaderPattern = ""
|
||||
}
|
||||
|
||||
config.Cookie, err = parser.GetStringAnnotation("canary-by-cookie", ing)
|
||||
config.Cookie, err = parser.GetStringAnnotation(canaryByCookieAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%s is invalid, defaulting to ''", canaryByCookieAnnotation)
|
||||
}
|
||||
config.Cookie = ""
|
||||
}
|
||||
|
||||
if !config.Enabled && (config.Weight > 0 || len(config.Header) > 0 || len(config.HeaderValue) > 0 || len(config.Cookie) > 0 ||
|
||||
len(config.HeaderPattern) > 0) {
|
||||
return nil, errors.NewInvalidAnnotationConfiguration("canary", "configured but not enabled")
|
||||
return nil, errors.NewInvalidAnnotationConfiguration(canaryAnnotation, "configured but not enabled")
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c canary) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a canary) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, CanaryAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -23,17 +23,49 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
clientBodyBufferSizeAnnotation = "client-body-buffer-size"
|
||||
)
|
||||
|
||||
var clientBodyBufferSizeConfig = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
clientBodyBufferSizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Sets buffer size for reading client request body per location.
|
||||
In case the request body is larger than the buffer, the whole body or only its part is written to a temporary file.
|
||||
By default, buffer size is equal to two memory pages. This is 8K on x86, other 32-bit platforms, and x86-64.
|
||||
It is usually 16K on other 64-bit platforms. This annotation is applied to each location provided in the ingress rule.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type clientBodyBufferSize struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new clientBodyBufferSize annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return clientBodyBufferSize{r}
|
||||
return clientBodyBufferSize{
|
||||
r: r,
|
||||
annotationConfig: clientBodyBufferSizeConfig,
|
||||
}
|
||||
}
|
||||
|
||||
func (cbbs clientBodyBufferSize) GetDocumentation() parser.AnnotationFields {
|
||||
return cbbs.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add an client-body-buffer-size to the provided locations
|
||||
func (cbbs clientBodyBufferSize) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("client-body-buffer-size", ing)
|
||||
return parser.GetStringAnnotation(clientBodyBufferSizeAnnotation, ing, cbbs.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a clientBodyBufferSize) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, clientBodyBufferSizeConfig.Annotations)
|
||||
}
|
||||
|
|
|
@ -39,6 +39,9 @@ func TestParse(t *testing.T) {
|
|||
}{
|
||||
{map[string]string{annotation: "8k"}, "8k"},
|
||||
{map[string]string{annotation: "16k"}, "16k"},
|
||||
{map[string]string{annotation: "10000"}, "10000"},
|
||||
{map[string]string{annotation: "16R"}, ""},
|
||||
{map[string]string{annotation: "16kkk"}, ""},
|
||||
{map[string]string{annotation: ""}, ""},
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
|
|
|
@ -17,12 +17,34 @@ limitations under the License.
|
|||
package connection
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
connectionProxyHeaderAnnotation = "connection-proxy-header"
|
||||
)
|
||||
|
||||
var (
|
||||
validConnectionHeaderValue = regexp.MustCompile(`^(close|keep-alive)$`)
|
||||
)
|
||||
|
||||
var connectionHeadersAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
connectionProxyHeaderAnnotation: {
|
||||
Validator: parser.ValidateRegex(*validConnectionHeaderValue, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting a specific value for "proxy_set_header Connection" directive. Right now it is restricted to "close" or "keep-alive"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns the connection header configuration for an Ingress rule
|
||||
type Config struct {
|
||||
Header string `json:"header"`
|
||||
|
@ -30,18 +52,22 @@ type Config struct {
|
|||
}
|
||||
|
||||
type connection struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new port in redirect annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return connection{r}
|
||||
return connection{
|
||||
r: r,
|
||||
annotationConfig: connectionHeadersAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the connection header should be overridden.
|
||||
func (a connection) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
cp, err := parser.GetStringAnnotation("connection-proxy-header", ing)
|
||||
cp, err := parser.GetStringAnnotation(connectionProxyHeaderAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{
|
||||
Enabled: false,
|
||||
|
@ -70,3 +96,12 @@ func (r1 *Config) Equal(r2 *Config) bool {
|
|||
|
||||
return true
|
||||
}
|
||||
|
||||
func (a connection) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a connection) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, connectionHeadersAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -37,10 +37,12 @@ func TestParse(t *testing.T) {
|
|||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected *Config
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}},
|
||||
{map[string]string{}, &Config{Enabled: false}},
|
||||
{nil, &Config{Enabled: false}},
|
||||
{map[string]string{annotation: "keep-alive"}, &Config{Enabled: true, Header: "keep-alive"}, false},
|
||||
{map[string]string{annotation: "not-allowed-value"}, &Config{Enabled: false}, true},
|
||||
{map[string]string{}, &Config{Enabled: false}, true},
|
||||
{nil, &Config{Enabled: false}, true},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -53,11 +55,17 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
i, _ := ap.Parse(ing)
|
||||
p, _ := i.(*Config)
|
||||
|
||||
i, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.expectErr {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
p, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Config type")
|
||||
}
|
||||
if !p.Equal(testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, p, testCase.annotations)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
|
@ -38,20 +39,87 @@ var (
|
|||
// Regex are defined here to prevent information leak, if user tries to set anything not valid
|
||||
// that could cause the Response to contain some internal value/variable (like returning $pid, $upstream_addr, etc)
|
||||
// Origin must contain a http/s Origin (including or not the port) or the value '*'
|
||||
// This Regex is composed of the following:
|
||||
// * Sets a group that can be (https?://)?*?.something.com:port?
|
||||
// * Allows this to be repeated as much as possible, and separated by comma
|
||||
// Otherwise it should be '*'
|
||||
corsOriginRegexValidator = regexp.MustCompile(`^((((https?://)?(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?,?)+)|\*)?$`)
|
||||
// corsOriginRegex defines the regex for validation inside Parse
|
||||
corsOriginRegex = regexp.MustCompile(`^(https?://(\*\.)?[A-Za-z0-9\-\.]*(:[0-9]+)?|\*)?$`)
|
||||
// Method must contain valid methods list (PUT, GET, POST, BLA)
|
||||
// May contain or not spaces between each verb
|
||||
corsMethodsRegex = regexp.MustCompile(`^([A-Za-z]+,?\s?)+$`)
|
||||
// Headers must contain valid values only (X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
corsHeadersRegex = regexp.MustCompile(`^([A-Za-z0-9\-\_]+,?\s?)+$`)
|
||||
// Expose Headers must contain valid values only (*, X-HEADER12, X-ABC)
|
||||
// May contain or not spaces between each Header
|
||||
corsExposeHeadersRegex = regexp.MustCompile(`^(([A-Za-z0-9\-\_]+|\*),?\s?)+$`)
|
||||
)
|
||||
|
||||
const (
|
||||
corsEnableAnnotation = "enable-cors"
|
||||
corsAllowOriginAnnotation = "cors-allow-origin"
|
||||
corsAllowHeadersAnnotation = "cors-allow-headers"
|
||||
corsAllowMethodsAnnotation = "cors-allow-methods"
|
||||
corsAllowCredentialsAnnotation = "cors-allow-credentials" //#nosec G101
|
||||
corsExposeHeadersAnnotation = "cors-expose-headers"
|
||||
corsMaxAgeAnnotation = "cors-max-age"
|
||||
)
|
||||
|
||||
var corsAnnotation = parser.Annotation{
|
||||
Group: "cors",
|
||||
Annotations: parser.AnnotationFields{
|
||||
corsEnableAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables Cross-Origin Resource Sharing (CORS) in an Ingress rule`,
|
||||
},
|
||||
corsAllowOriginAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsOriginRegexValidator, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls what's the accepted Origin for CORS.
|
||||
This is a multi-valued field, separated by ','. It must follow this format: http(s)://origin-site.com or http(s)://origin-site.com:port
|
||||
It also supports single level wildcard subdomains and follows this format: http(s)://*.foo.bar, http(s)://*.bar.foo:8080 or http(s)://*.abc.bar.foo:9000`,
|
||||
},
|
||||
corsAllowHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.HeadersVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which headers are accepted.
|
||||
This is a multi-valued field, separated by ',' and accepts letters, numbers, _ and -`,
|
||||
},
|
||||
corsAllowMethodsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsMethodsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which methods are accepted.
|
||||
This is a multi-valued field, separated by ',' and accepts only letters (upper and lower case)`,
|
||||
},
|
||||
corsAllowCredentialsAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation controls if credentials can be passed during CORS operations.`,
|
||||
},
|
||||
corsExposeHeadersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*corsExposeHeadersRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation controls which headers are exposed to response.
|
||||
This is a multi-valued field, separated by ',' and accepts letters, numbers, _, - and *.`,
|
||||
},
|
||||
corsMaxAgeAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation controls how long, in seconds, preflight requests can be cached.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type cors struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the Cors configuration to be used in the Ingress
|
||||
|
@ -67,7 +135,10 @@ type Config struct {
|
|||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return cors{r}
|
||||
return cors{
|
||||
r: r,
|
||||
annotationConfig: corsAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
// Equal tests for equality between two External types
|
||||
|
@ -116,13 +187,16 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.CorsEnabled, err = parser.GetBoolAnnotation("enable-cors", ing)
|
||||
config.CorsEnabled, err = parser.GetBoolAnnotation(corsEnableAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("enable-cors is invalid, defaulting to 'false'")
|
||||
}
|
||||
config.CorsEnabled = false
|
||||
}
|
||||
|
||||
config.CorsAllowOrigin = []string{}
|
||||
unparsedOrigins, err := parser.GetStringAnnotation("cors-allow-origin", ing)
|
||||
unparsedOrigins, err := parser.GetStringAnnotation(corsAllowOriginAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err == nil {
|
||||
origins := strings.Split(unparsedOrigins, ",")
|
||||
for _, origin := range origins {
|
||||
|
@ -140,33 +214,53 @@ func (c cors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
klog.Infof("Current config.corsAllowOrigin %v", config.CorsAllowOrigin)
|
||||
}
|
||||
} else {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-allow-origin is invalid, defaulting to '*'")
|
||||
}
|
||||
config.CorsAllowOrigin = []string{"*"}
|
||||
}
|
||||
|
||||
config.CorsAllowHeaders, err = parser.GetStringAnnotation("cors-allow-headers", ing)
|
||||
if err != nil || !corsHeadersRegex.MatchString(config.CorsAllowHeaders) {
|
||||
config.CorsAllowHeaders, err = parser.GetStringAnnotation(corsAllowHeadersAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !parser.HeadersVariable.MatchString(config.CorsAllowHeaders) {
|
||||
config.CorsAllowHeaders = defaultCorsHeaders
|
||||
}
|
||||
|
||||
config.CorsAllowMethods, err = parser.GetStringAnnotation("cors-allow-methods", ing)
|
||||
config.CorsAllowMethods, err = parser.GetStringAnnotation(corsAllowMethodsAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !corsMethodsRegex.MatchString(config.CorsAllowMethods) {
|
||||
config.CorsAllowMethods = defaultCorsMethods
|
||||
}
|
||||
|
||||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation("cors-allow-credentials", ing)
|
||||
config.CorsAllowCredentials, err = parser.GetBoolAnnotation(corsAllowCredentialsAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-allow-credentials is invalid, defaulting to 'true'")
|
||||
}
|
||||
}
|
||||
config.CorsAllowCredentials = true
|
||||
}
|
||||
|
||||
config.CorsExposeHeaders, err = parser.GetStringAnnotation("cors-expose-headers", ing)
|
||||
config.CorsExposeHeaders, err = parser.GetStringAnnotation(corsExposeHeadersAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil || !corsExposeHeadersRegex.MatchString(config.CorsExposeHeaders) {
|
||||
config.CorsExposeHeaders = ""
|
||||
}
|
||||
|
||||
config.CorsMaxAge, err = parser.GetIntAnnotation("cors-max-age", ing)
|
||||
config.CorsMaxAge, err = parser.GetIntAnnotation(corsMaxAgeAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("cors-max-age is invalid, defaulting to %d", defaultCorsMaxAge)
|
||||
}
|
||||
config.CorsMaxAge = defaultCorsMaxAge
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (c cors) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a cors) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, corsAnnotation.Annotations)
|
||||
}
|
||||
|
|
|
@ -75,13 +75,13 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
data := map[string]string{}
|
||||
|
||||
// Valid
|
||||
data[parser.GetAnnotationWithPrefix("enable-cors")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "https://origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "*, X-CustomResponseHeader"
|
||||
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "600"
|
||||
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "DNT,X-CustomHeader, Keep-Alive,User-Agent"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "https://origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "*, X-CustomResponseHeader"
|
||||
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "600"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -95,31 +95,31 @@ func TestIngressCorsConfigValid(t *testing.T) {
|
|||
}
|
||||
|
||||
if !nginxCors.CorsEnabled {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("enable-cors")], nginxCors.CorsEnabled)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)], nginxCors.CorsEnabled)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowCredentials {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-credentials")], nginxCors.CorsAllowCredentials)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)], nginxCors.CorsAllowCredentials)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowHeaders != "DNT,X-CustomHeader, Keep-Alive,User-Agent" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-headers")], nginxCors.CorsAllowHeaders)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)], nginxCors.CorsAllowHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowMethods != "GET, PATCH" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-methods")], nginxCors.CorsAllowMethods)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)], nginxCors.CorsAllowMethods)
|
||||
}
|
||||
|
||||
if nginxCors.CorsAllowOrigin[0] != "https://origin123.test.com:4443" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-allow-origin")], nginxCors.CorsAllowOrigin)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)], nginxCors.CorsAllowOrigin)
|
||||
}
|
||||
|
||||
if nginxCors.CorsExposeHeaders != "*, X-CustomResponseHeader" {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-expose-headers")], nginxCors.CorsExposeHeaders)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)], nginxCors.CorsExposeHeaders)
|
||||
}
|
||||
|
||||
if nginxCors.CorsMaxAge != 600 {
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix("cors-max-age")], nginxCors.CorsMaxAge)
|
||||
t.Errorf("expected %v but returned %v", data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)], nginxCors.CorsMaxAge)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -129,13 +129,13 @@ func TestIngressCorsConfigInvalid(t *testing.T) {
|
|||
data := map[string]string{}
|
||||
|
||||
// Valid
|
||||
data[parser.GetAnnotationWithPrefix("enable-cors")] = "yes"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-headers")] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-credentials")] = "no"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-methods")] = "GET, PATCH, $nginx"
|
||||
data[parser.GetAnnotationWithPrefix("cors-allow-origin")] = "origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix("cors-expose-headers")] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix("cors-max-age")] = "abcd"
|
||||
data[parser.GetAnnotationWithPrefix(corsEnableAnnotation)] = "yes"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowHeadersAnnotation)] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowCredentialsAnnotation)] = "no"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowMethodsAnnotation)] = "GET, PATCH, $nginx"
|
||||
data[parser.GetAnnotationWithPrefix(corsAllowOriginAnnotation)] = "origin123.test.com:4443"
|
||||
data[parser.GetAnnotationWithPrefix(corsExposeHeadersAnnotation)] = "@alright, #ingress"
|
||||
data[parser.GetAnnotationWithPrefix(corsMaxAgeAnnotation)] = "abcd"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
corst, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package customhttperrors
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
|
@ -26,19 +27,46 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
customHTTPErrorsAnnotation = "custom-http-errors"
|
||||
)
|
||||
|
||||
var (
|
||||
// We accept anything between 400 and 599, on a comma separated.
|
||||
arrayOfHTTPErrors = regexp.MustCompile(`^(?:[4,5][0-9][0-9],?)*$`)
|
||||
)
|
||||
|
||||
var customHTTPErrorsAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
customHTTPErrorsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*arrayOfHTTPErrors, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `If a default backend annotation is specified on the ingress, the errors code specified on this annotation
|
||||
will be routed to that annotation's default backend service. Otherwise they will be routed to the global default backend.
|
||||
A comma-separated list of error codes is accepted (anything between 400 and 599, like 403, 503)`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type customhttperrors struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new custom http errors annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return customhttperrors{r}
|
||||
return customhttperrors{
|
||||
r: r,
|
||||
annotationConfig: customHTTPErrorsAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress to use
|
||||
// custom http errors
|
||||
func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
c, err := parser.GetStringAnnotation("custom-http-errors", ing)
|
||||
c, err := parser.GetStringAnnotation(customHTTPErrorsAnnotation, ing, e.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -55,3 +83,12 @@ func (e customhttperrors) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return codes, nil
|
||||
}
|
||||
|
||||
func (e customhttperrors) GetDocumentation() parser.AnnotationFields {
|
||||
return e.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a customhttperrors) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, customHTTPErrorsAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -25,19 +25,40 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBackendAnnotation = "default-backend"
|
||||
)
|
||||
|
||||
var defaultBackendAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
defaultBackendAnnotation: {
|
||||
Validator: parser.ValidateServiceName,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This service will be used to handle the response when the configured service in the Ingress rule does not have any active endpoints.
|
||||
It will also be used to handle the error responses if both this annotation and the custom-http-errors annotation are set.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type backend struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new default backend annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return backend{r}
|
||||
return backend{
|
||||
r: r,
|
||||
annotationConfig: defaultBackendAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress to use
|
||||
// a custom default backend
|
||||
func (db backend) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
s, err := parser.GetStringAnnotation("default-backend", ing)
|
||||
s, err := parser.GetStringAnnotation(defaultBackendAnnotation, ing, db.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -50,3 +71,12 @@ func (db backend) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return svc, nil
|
||||
}
|
||||
|
||||
func (db backend) GetDocumentation() parser.AnnotationFields {
|
||||
return db.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a backend) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, defaultBackendAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -91,21 +91,51 @@ func (m mockService) GetService(name string) (*api.Service, error) {
|
|||
func TestAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("default-backend")] = "demo-service"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
fakeService := &mockService{}
|
||||
i, err := NewParser(fakeService).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error %v", err)
|
||||
tests := map[string]struct {
|
||||
expectErr bool
|
||||
serviceName string
|
||||
}{
|
||||
"valid name": {
|
||||
serviceName: "demo-service",
|
||||
expectErr: false,
|
||||
},
|
||||
"not in backend": {
|
||||
serviceName: "demo1-service",
|
||||
expectErr: true,
|
||||
},
|
||||
"invalid dns name": {
|
||||
serviceName: "demo-service.something.tld",
|
||||
expectErr: true,
|
||||
},
|
||||
"invalid name": {
|
||||
serviceName: "something/xpto",
|
||||
expectErr: true,
|
||||
},
|
||||
"invalid characters": {
|
||||
serviceName: "something;xpto",
|
||||
expectErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
svc, ok := i.(*api.Service)
|
||||
if !ok {
|
||||
t.Errorf("expected *api.Service but got %v", svc)
|
||||
}
|
||||
if svc.Name != "demo-service" {
|
||||
t.Errorf("expected %v but got %v", "demo-service", svc.Name)
|
||||
for _, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(defaultBackendAnnotation)] = test.serviceName
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
fakeService := &mockService{}
|
||||
i, err := NewParser(fakeService).Parse(ing)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
|
||||
if !test.expectErr {
|
||||
svc, ok := i.(*api.Service)
|
||||
if !ok {
|
||||
t.Errorf("expected *api.Service but got %v", svc)
|
||||
}
|
||||
if svc.Name != test.serviceName {
|
||||
t.Errorf("expected %v but got %v", test.serviceName, svc.Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,17 +19,49 @@ package fastcgi
|
|||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
fastCGIIndexAnnotation = "fastcgi-index"
|
||||
fastCGIParamsAnnotation = "fastcgi-params-configmap"
|
||||
)
|
||||
|
||||
var (
|
||||
// fast-cgi valid parameters is just a single file name (like index.php)
|
||||
regexValidIndexAnnotationAndKey = regexp.MustCompile(`^[A-Za-z0-9\.\-\_]+$`)
|
||||
)
|
||||
|
||||
var fastCGIAnnotations = parser.Annotation{
|
||||
Group: "fastcgi",
|
||||
Annotations: parser.AnnotationFields{
|
||||
fastCGIIndexAnnotation: {
|
||||
Validator: parser.ValidateRegex(*regexValidIndexAnnotationAndKey, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation can be used to specify an index file`,
|
||||
},
|
||||
fastCGIParamsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation can be used to specify a ConfigMap containing the fastcgi parameters as a key/value.
|
||||
Only ConfigMaps on the same namespace of ingress can be used. They key and value from ConfigMap are validated for unauthorized characters.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type fastcgi struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config describes the per location fastcgi config
|
||||
|
@ -57,7 +89,10 @@ func (l1 *Config) Equal(l2 *Config) bool {
|
|||
|
||||
// NewParser creates a new fastcgiConfig protocol annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return fastcgi{r}
|
||||
return fastcgi{
|
||||
r: r,
|
||||
annotationConfig: fastCGIAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -70,14 +105,21 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return fcgiConfig, nil
|
||||
}
|
||||
|
||||
index, err := parser.GetStringAnnotation("fastcgi-index", ing)
|
||||
index, err := parser.GetStringAnnotation(fastCGIIndexAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return fcgiConfig, err
|
||||
}
|
||||
index = ""
|
||||
}
|
||||
|
||||
fcgiConfig.Index = index
|
||||
|
||||
cm, err := parser.GetStringAnnotation("fastcgi-params-configmap", ing)
|
||||
cm, err := parser.GetStringAnnotation(fastCGIParamsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if ing_errors.IsValidationError(err) {
|
||||
return fcgiConfig, err
|
||||
}
|
||||
return fcgiConfig, nil
|
||||
}
|
||||
|
||||
|
@ -87,8 +129,10 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
Reason: fmt.Errorf("error reading configmap name from annotation: %w", err),
|
||||
}
|
||||
}
|
||||
secCfg := a.r.GetSecurityConfiguration()
|
||||
|
||||
if cmns != "" && cmns != ing.Namespace {
|
||||
// We don't accept different namespaces for secrets.
|
||||
if cmns != "" && !secCfg.AllowCrossNamespaceResources && cmns != ing.Namespace {
|
||||
return fcgiConfig, fmt.Errorf("different namespace is not supported on fast_cgi param configmap")
|
||||
}
|
||||
|
||||
|
@ -100,7 +144,24 @@ func (a fastcgi) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
for k, v := range cmap.Data {
|
||||
if !regexValidIndexAnnotationAndKey.MatchString(k) || !parser.NGINXVariable.MatchString(v) {
|
||||
klog.ErrorS(fmt.Errorf("fcgi contains invalid key or value"), "fcgi annotation error", "configmap", cmap.Name, "namespace", cmap.Namespace, "key", k, "value", v)
|
||||
return fcgiConfig, ing_errors.NewValidationError(fastCGIParamsAnnotation)
|
||||
}
|
||||
}
|
||||
|
||||
fcgiConfig.Index = index
|
||||
fcgiConfig.Params = cmap.Data
|
||||
|
||||
return fcgiConfig, nil
|
||||
}
|
||||
|
||||
func (a fastcgi) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a fastcgi) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, fastCGIAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ package fastcgi
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
|
@ -49,10 +50,16 @@ func buildIngress() *networking.Ingress {
|
|||
|
||||
type mockConfigMap struct {
|
||||
resolver.Mock
|
||||
extraConfigMap map[string]map[string]string
|
||||
}
|
||||
|
||||
func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) {
|
||||
if name != "default/demo-configmap" && name != "otherns/demo-configmap" {
|
||||
if m.extraConfigMap == nil {
|
||||
m.extraConfigMap = make(map[string]map[string]string)
|
||||
}
|
||||
cmdata, ok := m.extraConfigMap[name]
|
||||
|
||||
if name != "default/demo-configmap" && name != "otherns/demo-configmap" && !ok {
|
||||
return nil, fmt.Errorf("there is no configmap with name %v", name)
|
||||
}
|
||||
|
||||
|
@ -61,12 +68,17 @@ func (m mockConfigMap) GetConfigMap(name string) (*api.ConfigMap, error) {
|
|||
return nil, fmt.Errorf("invalid configmap name")
|
||||
}
|
||||
|
||||
data := map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"}
|
||||
if ok {
|
||||
data = cmdata
|
||||
}
|
||||
|
||||
return &api.ConfigMap{
|
||||
ObjectMeta: meta_v1.ObjectMeta{
|
||||
Namespace: cmns,
|
||||
Name: cmn,
|
||||
},
|
||||
Data: map[string]string{"REDIRECT_STATUS": "200", "SERVER_NAME": "$server_name"},
|
||||
Data: data,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -283,3 +295,111 @@ func TestConfigEquality(t *testing.T) {
|
|||
t.Errorf("config4 should be equal to config")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_fastcgi_Parse(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
index string
|
||||
configmapname string
|
||||
configmap map[string]string
|
||||
want interface{}
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "valid configuration",
|
||||
index: "indexxpto-92123.php",
|
||||
configmapname: "default/fcgiconfig",
|
||||
configmap: map[string]string{
|
||||
"REQUEST_METHOD": "$request_method",
|
||||
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
|
||||
},
|
||||
want: Config{
|
||||
Index: "indexxpto-92123.php",
|
||||
Params: map[string]string{
|
||||
"REQUEST_METHOD": "$request_method",
|
||||
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "invalid index name",
|
||||
index: "indexxpto-92123$xx.php",
|
||||
configmapname: "default/fcgiconfig",
|
||||
configmap: map[string]string{
|
||||
"REQUEST_METHOD": "$request_method",
|
||||
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
|
||||
},
|
||||
want: Config{},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid configmap namespace",
|
||||
index: "indexxpto-92123.php",
|
||||
configmapname: "otherns/fcgiconfig",
|
||||
configmap: map[string]string{
|
||||
"REQUEST_METHOD": "$request_method",
|
||||
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
|
||||
},
|
||||
want: Config{Index: "indexxpto-92123.php"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid configmap namespace name",
|
||||
index: "indexxpto-92123.php",
|
||||
configmapname: "otherns/fcgicon;{fig",
|
||||
configmap: map[string]string{
|
||||
"REQUEST_METHOD": "$request_method",
|
||||
"SCRIPT_FILENAME": "$document_root$fastcgi_script_name",
|
||||
},
|
||||
want: Config{Index: "indexxpto-92123.php"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid configmap values key",
|
||||
index: "indexxpto-92123.php",
|
||||
configmapname: "default/fcgiconfig",
|
||||
configmap: map[string]string{
|
||||
"REQUEST_METHOD$XPTO": "$request_method",
|
||||
},
|
||||
want: Config{Index: "indexxpto-92123.php"},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "invalid configmap values val",
|
||||
index: "indexxpto-92123.php",
|
||||
configmapname: "default/fcgiconfig",
|
||||
configmap: map[string]string{
|
||||
"REQUEST_METHOD_XPTO": "$request_method{test};a",
|
||||
},
|
||||
want: Config{Index: "indexxpto-92123.php"},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("fastcgi-index")] = tt.index
|
||||
data[parser.GetAnnotationWithPrefix("fastcgi-params-configmap")] = tt.configmapname
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
m := &mockConfigMap{
|
||||
extraConfigMap: map[string]map[string]string{
|
||||
tt.configmapname: tt.configmap,
|
||||
},
|
||||
}
|
||||
|
||||
got, err := NewParser(m).Parse(ing)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("fastcgi.Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !reflect.DeepEqual(got, tt.want) {
|
||||
t.Errorf("fastcgi.Parse() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,8 +22,10 @@ import (
|
|||
"time"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
|
@ -32,6 +34,46 @@ import (
|
|||
|
||||
const defaultKey = "$remote_addr"
|
||||
|
||||
const (
|
||||
globalRateLimitAnnotation = "global-rate-limit"
|
||||
globalRateLimitWindowAnnotation = "global-rate-limit-window"
|
||||
globalRateLimitKeyAnnotation = "global-rate-limit-key"
|
||||
globalRateLimitIgnoredCidrsAnnotation = "global-rate-limit-ignored-cidrs"
|
||||
)
|
||||
|
||||
var globalRateLimitAnnotationConfig = parser.Annotation{
|
||||
Group: "ratelimit",
|
||||
Annotations: parser.AnnotationFields{
|
||||
globalRateLimitAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation configures maximum allowed number of requests per window`,
|
||||
},
|
||||
globalRateLimitWindowAnnotation: {
|
||||
Validator: parser.ValidateDuration,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Configures a time window (i.e 1m) that the limit is applied`,
|
||||
},
|
||||
globalRateLimitKeyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation Configures a key for counting the samples. Defaults to $remote_addr.
|
||||
You can also combine multiple NGINX variables here, like ${remote_addr}-${http_x_api_client} which would mean the limit will be applied to
|
||||
requests coming from the same API client (indicated by X-API-Client HTTP request header) with the same source IP address`,
|
||||
},
|
||||
globalRateLimitIgnoredCidrsAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines a comma separated list of IPs and CIDRs to match client IP against.
|
||||
When there's a match request is not considered for rate limiting.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config encapsulates all global rate limit attributes
|
||||
type Config struct {
|
||||
Namespace string `json:"namespace"`
|
||||
|
@ -63,12 +105,16 @@ func (l *Config) Equal(r *Config) bool {
|
|||
}
|
||||
|
||||
type globalratelimit struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new globalratelimit annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return globalratelimit{r}
|
||||
return globalratelimit{
|
||||
r: r,
|
||||
annotationConfig: globalRateLimitAnnotationConfig,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse extracts globalratelimit annotations from the given ingress
|
||||
|
@ -76,8 +122,16 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
|||
func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
config := &Config{}
|
||||
|
||||
limit, _ := parser.GetIntAnnotation("global-rate-limit", ing)
|
||||
rawWindowSize, _ := parser.GetStringAnnotation("global-rate-limit-window", ing)
|
||||
limit, err := parser.GetIntAnnotation(globalRateLimitAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsInvalidContent(err) {
|
||||
return nil, err
|
||||
}
|
||||
rawWindowSize, err := parser.GetStringAnnotation(globalRateLimitWindowAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return config, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: %w", err),
|
||||
}
|
||||
}
|
||||
|
||||
if limit == 0 || len(rawWindowSize) == 0 {
|
||||
return config, nil
|
||||
|
@ -90,12 +144,18 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
key, _ := parser.GetStringAnnotation("global-rate-limit-key", ing)
|
||||
key, err := parser.GetStringAnnotation(globalRateLimitKeyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.Warningf("invalid %s, defaulting to %s", globalRateLimitKeyAnnotation, defaultKey)
|
||||
}
|
||||
if len(key) == 0 {
|
||||
key = defaultKey
|
||||
}
|
||||
|
||||
rawIgnoredCIDRs, _ := parser.GetStringAnnotation("global-rate-limit-ignored-cidrs", ing)
|
||||
rawIgnoredCIDRs, err := parser.GetStringAnnotation(globalRateLimitIgnoredCidrsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsInvalidContent(err) {
|
||||
return nil, err
|
||||
}
|
||||
ignoredCIDRs, err := net.ParseCIDRs(rawIgnoredCIDRs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -109,3 +169,12 @@ func (a globalratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a globalratelimit) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a globalratelimit) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, globalRateLimitAnnotationConfig.Annotations)
|
||||
}
|
||||
|
|
|
@ -149,6 +149,22 @@ func TestGlobalRateLimiting(t *testing.T) {
|
|||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"global-rate-limit-complex-key",
|
||||
map[string]string{
|
||||
annRateLimit: "100",
|
||||
annRateLimitWindow: "2m",
|
||||
annRateLimitKey: "${http_x_api_user}${otherinfo}",
|
||||
},
|
||||
&Config{
|
||||
Namespace: expectedUID,
|
||||
Limit: 100,
|
||||
WindowSize: 120,
|
||||
Key: "${http_x_api_user}${otherinfo}",
|
||||
IgnoredCIDRs: make([]string, 0),
|
||||
},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
"incorrect duration for window",
|
||||
map[string]string{
|
||||
|
@ -157,8 +173,8 @@ func TestGlobalRateLimiting(t *testing.T) {
|
|||
annRateLimitKey: "$http_x_api_user",
|
||||
},
|
||||
&Config{},
|
||||
ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: time: unknown unit \"mb\" in duration \"2mb\""),
|
||||
ing_errors.ValidationError{
|
||||
Reason: fmt.Errorf("failed to parse 'global-rate-limit-window' value: annotation nginx.ingress.kubernetes.io/global-rate-limit-window contains invalid value"),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
@ -168,7 +184,7 @@ func TestGlobalRateLimiting(t *testing.T) {
|
|||
|
||||
i, actualErr := NewParser(mockBackend{}).Parse(ing)
|
||||
if (testCase.expectedErr == nil || actualErr == nil) && testCase.expectedErr != actualErr {
|
||||
t.Errorf("expected error 'nil' but got '%v'", actualErr)
|
||||
t.Errorf("%s expected error '%v' but got '%v'", testCase.title, testCase.expectedErr, actualErr)
|
||||
} else if testCase.expectedErr != nil && actualErr != nil &&
|
||||
testCase.expectedErr.Error() != actualErr.Error() {
|
||||
t.Errorf("expected error '%v' but got '%v'", testCase.expectedErr, actualErr)
|
||||
|
|
|
@ -23,17 +23,46 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
http2PushPreloadAnnotation = "http2-push-preload"
|
||||
)
|
||||
|
||||
var http2PushPreloadAnnotations = parser.Annotation{
|
||||
Group: "http2",
|
||||
Annotations: parser.AnnotationFields{
|
||||
http2PushPreloadAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Enables automatic conversion of preload links specified in the “Link” response header fields into push requests`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type http2PushPreload struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new http2PushPreload annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return http2PushPreload{r}
|
||||
return http2PushPreload{
|
||||
r: r,
|
||||
annotationConfig: http2PushPreloadAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to add http2 push preload to the server
|
||||
func (h2pp http2PushPreload) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetBoolAnnotation("http2-push-preload", ing)
|
||||
return parser.GetBoolAnnotation(http2PushPreloadAnnotation, ing, h2pp.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (h2pp http2PushPreload) GetDocumentation() parser.AnnotationFields {
|
||||
return h2pp.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a http2PushPreload) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, http2PushPreloadAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -23,11 +23,12 @@ import (
|
|||
networking "k8s.io/api/networking/v1"
|
||||
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("http2-push-preload")
|
||||
annotation := parser.GetAnnotationWithPrefix(http2PushPreloadAnnotation)
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
|
@ -36,12 +37,14 @@ func TestParse(t *testing.T) {
|
|||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected bool
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotation: "true"}, true},
|
||||
{map[string]string{annotation: "1"}, true},
|
||||
{map[string]string{annotation: ""}, false},
|
||||
{map[string]string{}, false},
|
||||
{nil, false},
|
||||
{map[string]string{annotation: "true"}, true, false},
|
||||
{map[string]string{annotation: "1"}, true, false},
|
||||
{map[string]string{annotation: "xpto"}, false, true},
|
||||
{map[string]string{annotation: ""}, false, false},
|
||||
{map[string]string{}, false, false},
|
||||
{nil, false, false},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -54,7 +57,10 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if ((err != nil) != testCase.expectErr) && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
if result != testCase.expected {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipwhitelist
|
||||
package ipallowlist
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -30,6 +30,24 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
ipWhitelistAnnotation = "whitelist-source-range"
|
||||
ipAllowlistAnnotation = "allowlist-source-range"
|
||||
)
|
||||
|
||||
var allowlistAnnotations = parser.Annotation{
|
||||
Group: "acl",
|
||||
Annotations: parser.AnnotationFields{
|
||||
ipAllowlistAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access
|
||||
Documentation: `This annotation allows setting a list of IPs and networks allowed to access this Location`,
|
||||
AnnotationAliases: []string{ipWhitelistAnnotation},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// SourceRange returns the CIDR
|
||||
type SourceRange struct {
|
||||
CIDR []string `json:"cidr,omitempty"`
|
||||
|
@ -47,36 +65,47 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
|
|||
return sets.StringElementsMatch(sr1.CIDR, sr2.CIDR)
|
||||
}
|
||||
|
||||
type ipwhitelist struct {
|
||||
r resolver.Resolver
|
||||
type ipallowlist struct {
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new whitelist annotation parser
|
||||
// NewParser creates a new ipallowlist annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return ipwhitelist{r}
|
||||
return ipallowlist{
|
||||
r: r,
|
||||
annotationConfig: allowlistAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to limit access to certain client addresses or networks.
|
||||
// Multiple ranges can specified using commas as separator
|
||||
// e.g. `18.0.0.0/8,56.0.0.0/8`
|
||||
func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
func (a ipallowlist) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
defBackend := a.r.GetDefaultBackend()
|
||||
|
||||
defaultWhitelistSourceRange := make([]string, len(defBackend.WhitelistSourceRange))
|
||||
copy(defaultWhitelistSourceRange, defBackend.WhitelistSourceRange)
|
||||
sort.Strings(defaultWhitelistSourceRange)
|
||||
defaultAllowlistSourceRange := make([]string, len(defBackend.WhitelistSourceRange))
|
||||
copy(defaultAllowlistSourceRange, defBackend.WhitelistSourceRange)
|
||||
sort.Strings(defaultAllowlistSourceRange)
|
||||
|
||||
val, err := parser.GetStringAnnotation("whitelist-source-range", ing)
|
||||
val, err := parser.GetStringAnnotation(ipAllowlistAnnotation, ing, a.annotationConfig.Annotations)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultWhitelistSourceRange}, nil
|
||||
if err != nil {
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultAllowlistSourceRange}, nil
|
||||
}
|
||||
|
||||
return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
values := strings.Split(val, ",")
|
||||
ipnets, ips, err := net.ParseIPNets(values...)
|
||||
if err != nil && len(ips) == 0 {
|
||||
return &SourceRange{CIDR: defaultWhitelistSourceRange}, ing_errors.LocationDenied{
|
||||
return &SourceRange{CIDR: defaultAllowlistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: fmt.Errorf("the annotation does not contain a valid IP address or network: %w", err),
|
||||
}
|
||||
}
|
||||
|
@ -93,3 +122,12 @@ func (a ipwhitelist) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return &SourceRange{cidrs}, nil
|
||||
}
|
||||
|
||||
func (a ipallowlist) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a ipallowlist) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, allowlistAnnotations.Annotations)
|
||||
}
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
package ipwhitelist
|
||||
package ipallowlist
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
@ -86,12 +86,12 @@ func TestParseAnnotations(t *testing.T) {
|
|||
"test parse a invalid net": {
|
||||
net: "ww",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -102,16 +102,16 @@ func TestParseAnnotations(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(&resolver.Mock{})
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("%s expected error: %t got error: %t err value: %s. %+v", testName, test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
|
@ -137,7 +137,7 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
|||
}
|
||||
}
|
||||
|
||||
// Test that when we have a whitelist set on the Backend that is used when we
|
||||
// Test that when we have a allowlist set on the Backend that is used when we
|
||||
// don't have the annotation
|
||||
func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
@ -158,12 +158,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
"test parse a invalid net": {
|
||||
net: "ww",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/allowlist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/allowlist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -174,16 +174,67 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("whitelist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipAllowlistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(mockBackend)
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
sr, ok := i.(*SourceRange)
|
||||
if !ok {
|
||||
t.Errorf("%v:expected a SourceRange type", testName)
|
||||
}
|
||||
if !strsEquals(sr.CIDR, test.expectCidr) {
|
||||
t.Errorf("%v:expected %v CIDR but %v returned", testName, test.expectCidr, sr.CIDR)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when we have a whitelist set on the Backend that is used when we
|
||||
// don't have the annotation
|
||||
func TestLegacyAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
mockBackend := mockBackend{}
|
||||
|
||||
tests := map[string]struct {
|
||||
net string
|
||||
expectCidr []string
|
||||
expectErr bool
|
||||
errOut string
|
||||
}{
|
||||
"test parse a valid net": {
|
||||
net: "10.0.0.0/24",
|
||||
expectCidr: []string{"10.0.0.0/24"},
|
||||
expectErr: false,
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
expectCidr: []string{"1.1.1.1/32", "2.2.2.2/32", "3.3.3.0/24"},
|
||||
expectErr: false,
|
||||
},
|
||||
}
|
||||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(ipWhitelistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(mockBackend)
|
||||
i, err := p.Parse(ing)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
|
@ -30,6 +30,22 @@ import (
|
|||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
)
|
||||
|
||||
const (
|
||||
ipDenylistAnnotation = "denylist-source-range"
|
||||
)
|
||||
|
||||
var denylistAnnotations = parser.Annotation{
|
||||
Group: "acl",
|
||||
Annotations: parser.AnnotationFields{
|
||||
ipDenylistAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Failure on parsing this may cause undesired access
|
||||
Documentation: `This annotation allows setting a list of IPs and networks that should be blocked to access this Location`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// SourceRange returns the CIDR
|
||||
type SourceRange struct {
|
||||
CIDR []string `json:"cidr,omitempty"`
|
||||
|
@ -48,12 +64,16 @@ func (sr1 *SourceRange) Equal(sr2 *SourceRange) bool {
|
|||
}
|
||||
|
||||
type ipdenylist struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new denylist annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return ipdenylist{r}
|
||||
return ipdenylist{
|
||||
r: r,
|
||||
annotationConfig: denylistAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -67,10 +87,16 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
copy(defaultDenylistSourceRange, defBackend.DenylistSourceRange)
|
||||
sort.Strings(defaultDenylistSourceRange)
|
||||
|
||||
val, err := parser.GetStringAnnotation("denylist-source-range", ing)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultDenylistSourceRange}, nil
|
||||
val, err := parser.GetStringAnnotation(ipDenylistAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if err == ing_errors.ErrMissingAnnotations {
|
||||
return &SourceRange{CIDR: defaultDenylistSourceRange}, nil
|
||||
}
|
||||
|
||||
return &SourceRange{CIDR: defaultDenylistSourceRange}, ing_errors.LocationDenied{
|
||||
Reason: err,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
values := strings.Split(val, ",")
|
||||
|
@ -93,3 +119,12 @@ func (a ipdenylist) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return &SourceRange{cidrs}, nil
|
||||
}
|
||||
|
||||
func (a ipdenylist) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a ipdenylist) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, denylistAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -86,17 +86,17 @@ func TestParseAnnotations(t *testing.T) {
|
|||
"test parse a invalid net": {
|
||||
net: "ww",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse a malicious escaped string": {
|
||||
net: `10.0.0.0/8"rm /tmp",11.0.0.0/8`,
|
||||
expectErr: true,
|
||||
errOut: `the annotation does not contain a valid IP address or network: invalid CIDR address: 10.0.0.0/8"rm /tmp"`,
|
||||
errOut: `annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value`,
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -107,16 +107,16 @@ func TestParseAnnotations(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(&resolver.Mock{})
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
|
@ -163,12 +163,12 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
"test parse a invalid net": {
|
||||
net: "ww",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ww",
|
||||
errOut: "annotation nginx.ingress.kubernetes.io/denylist-source-range contains invalid value",
|
||||
},
|
||||
"test parse a empty net": {
|
||||
net: "",
|
||||
expectErr: true,
|
||||
errOut: "the annotation does not contain a valid IP address or network: invalid CIDR address: ",
|
||||
errOut: "the annotation nginx.ingress.kubernetes.io/denylist-source-range does not contain a valid value ()",
|
||||
},
|
||||
"test parse multiple valid cidr": {
|
||||
net: "2.2.2.2/32,1.1.1.1/32,3.3.3.0/24",
|
||||
|
@ -179,16 +179,16 @@ func TestParseAnnotationsWithDefaultConfig(t *testing.T) {
|
|||
|
||||
for testName, test := range tests {
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("denylist-source-range")] = test.net
|
||||
data[parser.GetAnnotationWithPrefix(ipDenylistAnnotation)] = test.net
|
||||
ing.SetAnnotations(data)
|
||||
p := NewParser(mockBackend)
|
||||
i, err := p.Parse(ing)
|
||||
if err != nil && !test.expectErr {
|
||||
t.Errorf("%v:unexpected error: %v", testName, err)
|
||||
if (err != nil) != test.expectErr {
|
||||
t.Errorf("expected error: %t got error: %t err value: %s. %+v", test.expectErr, err != nil, err, i)
|
||||
}
|
||||
if test.expectErr {
|
||||
if test.expectErr && err != nil {
|
||||
if err.Error() != test.errOut {
|
||||
t.Errorf("%v:expected error: %v but %v return", testName, test.errOut, err.Error())
|
||||
t.Errorf("expected error %s but got %s", test.errOut, err)
|
||||
}
|
||||
}
|
||||
if !test.expectErr {
|
||||
|
|
|
@ -23,18 +23,52 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
type loadbalancing struct {
|
||||
r resolver.Resolver
|
||||
// LB Alghorithms are defined in https://github.com/kubernetes/ingress-nginx/blob/d3e75b056f77be54e01bdb18675f1bb46caece31/rootfs/etc/nginx/lua/balancer.lua#L28
|
||||
|
||||
const (
|
||||
loadBalanceAlghoritmAnnotation = "load-balance"
|
||||
)
|
||||
|
||||
var loadBalanceAlghoritms = []string{"round_robin", "chash", "chashsubset", "sticky_balanced", "sticky_persistent", "ewma"}
|
||||
|
||||
var loadBalanceAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
loadBalanceAlghoritmAnnotation: {
|
||||
Validator: parser.ValidateOptions(loadBalanceAlghoritms, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the load balancing alghorithm that should be used. If none is specified, defaults to
|
||||
the default configured by Ingress admin, otherwise to round_robin`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
type loadbalancing struct {
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new Load Balancer annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return loadbalancing{r}
|
||||
return loadbalancing{
|
||||
r: r,
|
||||
annotationConfig: loadBalanceAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to indicate if the location/s contains a fragment of
|
||||
// configuration to be included inside the paths of the rules
|
||||
func (a loadbalancing) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("load-balance", ing)
|
||||
return parser.GetStringAnnotation(loadBalanceAlghoritmAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a loadbalancing) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a loadbalancing) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, loadBalanceAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -38,7 +38,8 @@ func TestParse(t *testing.T) {
|
|||
annotations map[string]string
|
||||
expected string
|
||||
}{
|
||||
{map[string]string{annotation: "ip_hash"}, "ip_hash"},
|
||||
{map[string]string{annotation: "ewma"}, "ewma"},
|
||||
{map[string]string{annotation: "ip_hash"}, ""}, // This is invalid and should not return anything
|
||||
{map[string]string{}, ""},
|
||||
{nil, ""},
|
||||
}
|
||||
|
|
|
@ -23,8 +23,32 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableAccessLogAnnotation = "enable-access-log"
|
||||
enableRewriteLogAnnotation = "enable-rewrite-log"
|
||||
)
|
||||
|
||||
var logAnnotations = parser.Annotation{
|
||||
Group: "log",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableAccessLogAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This configuration setting allows you to control if this location should generate an access_log`,
|
||||
},
|
||||
enableRewriteLogAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This configuration setting allows you to control if this location should generate logs from the rewrite feature usage`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type log struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the configuration to be used in the Ingress
|
||||
|
@ -48,7 +72,10 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
|
||||
// NewParser creates a new log annotations parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return log{r}
|
||||
return log{
|
||||
r: r,
|
||||
annotationConfig: logAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -57,15 +84,24 @@ func (l log) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.Access, err = parser.GetBoolAnnotation("enable-access-log", ing)
|
||||
config.Access, err = parser.GetBoolAnnotation(enableAccessLogAnnotation, ing, l.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Access = true
|
||||
}
|
||||
|
||||
config.Rewrite, err = parser.GetBoolAnnotation("enable-rewrite-log", ing)
|
||||
config.Rewrite, err = parser.GetBoolAnnotation(enableRewriteLogAnnotation, ing, l.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Rewrite = false
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (l log) GetDocumentation() parser.AnnotationFields {
|
||||
return l.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a log) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, logAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func TestIngressAccessLogConfig(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-access-log")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(enableAccessLogAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -91,7 +91,7 @@ func TestIngressRewriteLogConfig(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-rewrite-log")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -104,3 +104,21 @@ func TestIngressRewriteLogConfig(t *testing.T) {
|
|||
t.Errorf("expected rewrite log to be enabled but it is disabled")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidBoolConfig(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(enableRewriteLogAnnotation)] = "blo"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
log, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
nginxLogs, ok := log.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Config type")
|
||||
}
|
||||
|
||||
if !nginxLogs.Access {
|
||||
t.Errorf("expected access log to be enabled due to invalid config, but it is disabled")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,13 +18,50 @@ package mirror
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
mirrorRequestBodyAnnotation = "mirror-request-body"
|
||||
mirrorTargetAnnotation = "mirror-target"
|
||||
mirrorHostAnnotation = "mirror-host"
|
||||
)
|
||||
|
||||
var (
|
||||
OnOffRegex = regexp.MustCompile(`^(on|off)$`)
|
||||
)
|
||||
|
||||
var mirrorAnnotation = parser.Annotation{
|
||||
Group: "mirror",
|
||||
Annotations: parser.AnnotationFields{
|
||||
mirrorRequestBodyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*OnOffRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the request-body should be sent to the mirror backend. Can be 'on' or 'off'`,
|
||||
},
|
||||
mirrorTargetAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation enables a request to be mirrored to a mirror backend.`,
|
||||
},
|
||||
mirrorHostAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation defines if a specific Host header should be set for mirrored request.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns the mirror to use in a given location
|
||||
type Config struct {
|
||||
Source string `json:"source"`
|
||||
|
@ -63,12 +100,16 @@ func (m1 *Config) Equal(m2 *Config) bool {
|
|||
}
|
||||
|
||||
type mirror struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new mirror configuration annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return mirror{r}
|
||||
return mirror{
|
||||
r: r,
|
||||
annotationConfig: mirrorAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -79,19 +120,29 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
|
||||
var err error
|
||||
config.RequestBody, err = parser.GetStringAnnotation("mirror-request-body", ing)
|
||||
config.RequestBody, err = parser.GetStringAnnotation(mirrorRequestBodyAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil || config.RequestBody != "off" {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("annotation %s contains invalid value", mirrorRequestBodyAnnotation)
|
||||
}
|
||||
config.RequestBody = "on"
|
||||
}
|
||||
|
||||
config.Target, err = parser.GetStringAnnotation("mirror-target", ing)
|
||||
config.Target, err = parser.GetStringAnnotation(mirrorTargetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Target = ""
|
||||
config.Source = ""
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorTargetAnnotation)
|
||||
} else {
|
||||
config.Target = ""
|
||||
config.Source = ""
|
||||
}
|
||||
}
|
||||
|
||||
config.Host, err = parser.GetStringAnnotation("mirror-host", ing)
|
||||
config.Host, err = parser.GetStringAnnotation(mirrorHostAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("annotation %s contains invalid value, defaulting", mirrorHostAnnotation)
|
||||
}
|
||||
if config.Target != "" {
|
||||
target := strings.Split(config.Target, "$")
|
||||
|
||||
|
@ -106,3 +157,12 @@ func (a mirror) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a mirror) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a mirror) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, mirrorAnnotation.Annotations)
|
||||
}
|
||||
|
|
|
@ -94,13 +94,13 @@ func TestParse(t *testing.T) {
|
|||
Source: ngxURI,
|
||||
RequestBody: "on",
|
||||
Target: "http://some.test.env.com",
|
||||
Host: "someInvalidParm.%^&*()_=!@#'\"",
|
||||
Host: "some.test.env.com",
|
||||
}},
|
||||
{map[string]string{backendURL: "http://some.test.env.com", host: "_sbrubles-i\"@xpto:12345"}, &Config{
|
||||
Source: ngxURI,
|
||||
RequestBody: "on",
|
||||
Target: "http://some.test.env.com",
|
||||
Host: "_sbrubles-i\"@xpto:12345",
|
||||
Host: "some.test.env.com",
|
||||
}},
|
||||
}
|
||||
|
||||
|
@ -115,9 +115,12 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf(err.Error())
|
||||
}
|
||||
if !reflect.DeepEqual(result, testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
t.Errorf("expected %+v but returned %+v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,48 @@ package modsecurity
|
|||
import (
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
modsecEnableAnnotation = "enable-modsecurity"
|
||||
modsecEnableOwaspCoreAnnotation = "enable-owasp-core-rules"
|
||||
modesecTransactionIdAnnotation = "modsecurity-transaction-id"
|
||||
modsecSnippetAnnotation = "modsecurity-snippet"
|
||||
)
|
||||
|
||||
var modsecurityAnnotation = parser.Annotation{
|
||||
Group: "modsecurity",
|
||||
Annotations: parser.AnnotationFields{
|
||||
modsecEnableAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables ModSecurity`,
|
||||
},
|
||||
modsecEnableOwaspCoreAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables the OWASP Core Rule Set`,
|
||||
},
|
||||
modesecTransactionIdAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.NGINXVariable, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation enables passing an NGINX variable to ModSecurity.`,
|
||||
},
|
||||
modsecSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskCritical,
|
||||
Documentation: `This annotation enables adding a specific snippet configuration for ModSecurity`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains ModSecurity Configuration items
|
||||
type Config struct {
|
||||
Enable bool `json:"enable-modsecurity"`
|
||||
|
@ -60,11 +99,15 @@ func (modsec1 *Config) Equal(modsec2 *Config) bool {
|
|||
|
||||
// NewParser creates a new ModSecurity annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return modSecurity{resolver}
|
||||
return modSecurity{
|
||||
r: resolver,
|
||||
annotationConfig: modsecurityAnnotation,
|
||||
}
|
||||
}
|
||||
|
||||
type modSecurity struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -74,26 +117,44 @@ func (a modSecurity) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
config := &Config{}
|
||||
|
||||
config.EnableSet = true
|
||||
config.Enable, err = parser.GetBoolAnnotation("enable-modsecurity", ing)
|
||||
config.Enable, err = parser.GetBoolAnnotation(modsecEnableAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsInvalidContent(err) {
|
||||
klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableAnnotation)
|
||||
}
|
||||
config.Enable = false
|
||||
config.EnableSet = false
|
||||
}
|
||||
|
||||
config.OWASPRules, err = parser.GetBoolAnnotation("enable-owasp-core-rules", ing)
|
||||
config.OWASPRules, err = parser.GetBoolAnnotation(modsecEnableOwaspCoreAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsInvalidContent(err) {
|
||||
klog.Warningf("annotation %s contains invalid directive, defaulting to false", modsecEnableOwaspCoreAnnotation)
|
||||
}
|
||||
config.OWASPRules = false
|
||||
}
|
||||
|
||||
config.TransactionID, err = parser.GetStringAnnotation("modsecurity-transaction-id", ing)
|
||||
config.TransactionID, err = parser.GetStringAnnotation(modesecTransactionIdAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsInvalidContent(err) {
|
||||
klog.Warningf("annotation %s contains invalid directive, defaulting", modesecTransactionIdAnnotation)
|
||||
}
|
||||
config.TransactionID = ""
|
||||
}
|
||||
|
||||
config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing)
|
||||
config.Snippet, err = parser.GetStringAnnotation("modsecurity-snippet", ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.Snippet = ""
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a modSecurity) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a modSecurity) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, modsecurityAnnotation.Annotations)
|
||||
}
|
||||
|
|
|
@ -17,14 +17,51 @@ limitations under the License.
|
|||
package opentelemetry
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableOpenTelemetryAnnotation = "enable-opentelemetry"
|
||||
otelTrustSpanAnnotation = "opentelemetry-trust-incoming-span"
|
||||
otelOperationNameAnnotation = "opentelemetry-operation-name"
|
||||
)
|
||||
|
||||
var regexOperationName = regexp.MustCompile(`^[A-Za-z0-9_\-]*$`)
|
||||
|
||||
var otelAnnotations = parser.Annotation{
|
||||
Group: "opentelemetry",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableOpenTelemetryAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if Open Telemetry collector should be enable for this location. OpenTelemetry should
|
||||
already be configured by Ingress administrator`,
|
||||
},
|
||||
otelTrustSpanAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`,
|
||||
},
|
||||
otelOperationNameAnnotation: {
|
||||
Validator: parser.ValidateRegex(*regexOperationName, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines what operation name should be added to the span`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type opentelemetry struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the configuration to be used in the Ingress
|
||||
|
@ -64,13 +101,16 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return opentelemetry{r}
|
||||
return opentelemetry{
|
||||
r: r,
|
||||
annotationConfig: otelAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations to look for opentelemetry configurations
|
||||
func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
cfg := Config{}
|
||||
enabled, err := parser.GetBoolAnnotation("enable-opentelemetry", ing)
|
||||
enabled, err := parser.GetBoolAnnotation(enableOpenTelemetryAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &cfg, nil
|
||||
}
|
||||
|
@ -80,10 +120,13 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return &cfg, nil
|
||||
}
|
||||
|
||||
trustEnabled, err := parser.GetBoolAnnotation("opentelemetry-trust-incoming-span", ing)
|
||||
trustEnabled, err := parser.GetBoolAnnotation(otelTrustSpanAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing)
|
||||
operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
cfg.OperationName = operationName
|
||||
|
@ -92,10 +135,22 @@ func (c opentelemetry) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
cfg.TrustSet = true
|
||||
cfg.TrustEnabled = trustEnabled
|
||||
operationName, err := parser.GetStringAnnotation("opentelemetry-operation-name", ing)
|
||||
operationName, err := parser.GetStringAnnotation(otelOperationNameAnnotation, ing, c.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
cfg.OperationName = operationName
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func (c opentelemetry) GetDocumentation() parser.AnnotationFields {
|
||||
return c.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a opentelemetry) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, otelAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func TestIngressAnnotationOpentelemetrySetTrue(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -100,7 +100,7 @@ func TestIngressAnnotationOpentelemetrySetFalse(t *testing.T) {
|
|||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -123,12 +123,15 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) {
|
|||
|
||||
data := map[string]string{}
|
||||
opName := "foo-op"
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentelemetry")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("opentelemetry-trust-incoming-span")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("opentelemetry-operation-name")] = opName
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(otelTrustSpanAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
val, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
openTelemetry, ok := val.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Config type")
|
||||
|
@ -155,6 +158,21 @@ func TestIngressAnnotationOpentelemetryTrustSetTrue(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIngressAnnotationOpentelemetryWithBadOpName(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
opName := "fooxpto_123$la;"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpenTelemetryAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(otelOperationNameAnnotation)] = opName
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
if err == nil {
|
||||
t.Fatalf("This operation should return an error but no error was returned")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIngressAnnotationOpentelemetryUnset(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
|
|
|
@ -23,8 +23,33 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
enableOpentracingAnnotation = "enable-opentracing"
|
||||
opentracingTrustSpanAnnotation = "opentracing-trust-incoming-span"
|
||||
)
|
||||
|
||||
var opentracingAnnotations = parser.Annotation{
|
||||
Group: "opentracing",
|
||||
Annotations: parser.AnnotationFields{
|
||||
enableOpentracingAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if Opentracing collector should be enable for this location. Opentracing should
|
||||
already be configured by Ingress administrator`,
|
||||
},
|
||||
opentracingTrustSpanAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables using spans from incoming requests as parent for created ones`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type opentracing struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the configuration to be used in the Ingress
|
||||
|
@ -58,19 +83,31 @@ func (bd1 *Config) Equal(bd2 *Config) bool {
|
|||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return opentracing{r}
|
||||
return opentracing{
|
||||
r: r,
|
||||
annotationConfig: opentracingAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
func (s opentracing) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
enabled, err := parser.GetBoolAnnotation("enable-opentracing", ing)
|
||||
enabled, err := parser.GetBoolAnnotation(enableOpentracingAnnotation, ing, s.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, nil
|
||||
}
|
||||
|
||||
trustSpan, err := parser.GetBoolAnnotation("opentracing-trust-incoming-span", ing)
|
||||
trustSpan, err := parser.GetBoolAnnotation(opentracingTrustSpanAnnotation, ing, s.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{Set: true, Enabled: enabled}, nil
|
||||
}
|
||||
|
||||
return &Config{Set: true, Enabled: enabled, TrustSet: true, TrustEnabled: trustSpan}, nil
|
||||
}
|
||||
|
||||
func (s opentracing) GetDocumentation() parser.AnnotationFields {
|
||||
return s.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a opentracing) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, opentracingAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -73,7 +73,7 @@ func TestIngressAnnotationOpentracingSetTrue(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -92,7 +92,7 @@ func TestIngressAnnotationOpentracingSetFalse(t *testing.T) {
|
|||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -110,8 +110,8 @@ func TestIngressAnnotationOpentracingTrustSetTrue(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("enable-opentracing")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("opentracing-trust-incoming-span")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(enableOpentracingAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(opentracingTrustSpanAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -29,20 +29,79 @@ import (
|
|||
)
|
||||
|
||||
// DefaultAnnotationsPrefix defines the common prefix used in the nginx ingress controller
|
||||
const DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
const (
|
||||
DefaultAnnotationsPrefix = "nginx.ingress.kubernetes.io"
|
||||
DefaultEnableAnnotationValidation = true
|
||||
)
|
||||
|
||||
var (
|
||||
// AnnotationsPrefix is the mutable attribute that the controller explicitly refers to
|
||||
AnnotationsPrefix = DefaultAnnotationsPrefix
|
||||
// Enable is the mutable attribute for enabling or disabling the validation functions
|
||||
EnableAnnotationValidation = DefaultEnableAnnotationValidation
|
||||
)
|
||||
|
||||
// AnnotationGroup defines the group that this annotation may belong
|
||||
// eg.: Security, Snippets, Rewrite, etc
|
||||
type AnnotationGroup string
|
||||
|
||||
// AnnotationScope defines which scope this annotation applies. May be to the whole
|
||||
// ingress, per location, etc
|
||||
type AnnotationScope string
|
||||
|
||||
var (
|
||||
AnnotationScopeLocation AnnotationScope = "location"
|
||||
AnnotationScopeIngress AnnotationScope = "ingress"
|
||||
)
|
||||
|
||||
// AnnotationRisk is a subset of risk that an annotation may represent.
|
||||
// Based on the Risk, the admin will be able to allow or disallow users to set it
|
||||
// on their ingress objects
|
||||
type AnnotationRisk int
|
||||
|
||||
type AnnotationFields map[string]AnnotationConfig
|
||||
|
||||
// AnnotationConfig defines the configuration that a single annotation field
|
||||
// has, with the Validator and the documentation of this field.
|
||||
type AnnotationConfig struct {
|
||||
// Validator defines a function to validate the annotation value
|
||||
Validator AnnotationValidator
|
||||
// Documentation defines a user facing documentation for this annotation. This
|
||||
// field will be used to auto generate documentations
|
||||
Documentation string
|
||||
// Risk defines a risk of this annotation being exposed to the user. Annotations
|
||||
// with bool fields, or to set timeout are usually low risk. Annotations that allows
|
||||
// string input without a limited set of options may represent a high risk
|
||||
Risk AnnotationRisk
|
||||
|
||||
// Scope defines which scope this annotation applies, may be to location, to an Ingress object, etc
|
||||
Scope AnnotationScope
|
||||
|
||||
// AnnotationAliases defines other names this annotation may have.
|
||||
AnnotationAliases []string
|
||||
}
|
||||
|
||||
// Annotation defines an annotation feature an Ingress may have.
|
||||
// It should contain the internal resolver, and all the annotations
|
||||
// with configs and Validators that should be used for each Annotation
|
||||
type Annotation struct {
|
||||
// Annotations contains all the annotations that belong to this feature
|
||||
Annotations AnnotationFields
|
||||
// Group defines which annotation group this feature belongs to
|
||||
Group AnnotationGroup
|
||||
}
|
||||
|
||||
// IngressAnnotation has a method to parse annotations located in Ingress
|
||||
type IngressAnnotation interface {
|
||||
Parse(ing *networking.Ingress) (interface{}, error)
|
||||
GetDocumentation() AnnotationFields
|
||||
Validate(anns map[string]string) error
|
||||
}
|
||||
|
||||
type ingAnnotations map[string]string
|
||||
|
||||
// TODO: We already parse all of this on checkAnnotation and can just do a parse over the
|
||||
// value
|
||||
func (a ingAnnotations) parseBool(name string) (bool, error) {
|
||||
val, ok := a[name]
|
||||
if ok {
|
||||
|
@ -92,21 +151,9 @@ func (a ingAnnotations) parseFloat32(name string) (float32, error) {
|
|||
return 0, errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
func checkAnnotation(name string, ing *networking.Ingress) error {
|
||||
if ing == nil || len(ing.GetAnnotations()) == 0 {
|
||||
return errors.ErrMissingAnnotations
|
||||
}
|
||||
if name == "" {
|
||||
return errors.ErrInvalidAnnotationName
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetBoolAnnotation extracts a boolean from an Ingress annotation
|
||||
func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetBoolAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (bool, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
@ -114,9 +161,8 @@ func GetBoolAnnotation(name string, ing *networking.Ingress) (bool, error) {
|
|||
}
|
||||
|
||||
// GetStringAnnotation extracts a string from an Ingress annotation
|
||||
func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetStringAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
@ -125,9 +171,8 @@ func GetStringAnnotation(name string, ing *networking.Ingress) (string, error) {
|
|||
}
|
||||
|
||||
// GetIntAnnotation extracts an int from an Ingress annotation
|
||||
func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetIntAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (int, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -135,9 +180,8 @@ func GetIntAnnotation(name string, ing *networking.Ingress) (int, error) {
|
|||
}
|
||||
|
||||
// GetFloatAnnotation extracts a float32 from an Ingress annotation
|
||||
func GetFloatAnnotation(name string, ing *networking.Ingress) (float32, error) {
|
||||
v := GetAnnotationWithPrefix(name)
|
||||
err := checkAnnotation(v, ing)
|
||||
func GetFloatAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (float32, error) {
|
||||
v, err := checkAnnotation(name, ing, fields)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
@ -149,6 +193,23 @@ func GetAnnotationWithPrefix(suffix string) string {
|
|||
return fmt.Sprintf("%v/%v", AnnotationsPrefix, suffix)
|
||||
}
|
||||
|
||||
func TrimAnnotationPrefix(annotation string) string {
|
||||
return strings.TrimPrefix(annotation, AnnotationsPrefix+"/")
|
||||
}
|
||||
|
||||
func StringRiskToRisk(risk string) AnnotationRisk {
|
||||
switch strings.ToLower(risk) {
|
||||
case "critical":
|
||||
return AnnotationRiskCritical
|
||||
case "high":
|
||||
return AnnotationRiskHigh
|
||||
case "medium":
|
||||
return AnnotationRiskMedium
|
||||
default:
|
||||
return AnnotationRiskLow
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeString(input string) string {
|
||||
trimmedContent := []string{}
|
||||
for _, line := range strings.Split(input, "\n") {
|
||||
|
|
|
@ -38,7 +38,7 @@ func buildIngress() *networking.Ingress {
|
|||
func TestGetBoolAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetBoolAnnotation("", nil)
|
||||
_, err := GetBoolAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
@ -59,8 +59,8 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
|
||||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
u, err := GetBoolAnnotation(test.field, ing)
|
||||
ing.SetAnnotations(data)
|
||||
u, err := GetBoolAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
@ -68,7 +68,7 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
continue
|
||||
}
|
||||
if u != test.exp {
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned", test.name, test.exp, u)
|
||||
t.Errorf("%v: expected \"%v\" but \"%v\" was returned, %+v", test.name, test.exp, u, ing)
|
||||
}
|
||||
|
||||
delete(data, test.field)
|
||||
|
@ -78,7 +78,7 @@ func TestGetBoolAnnotation(t *testing.T) {
|
|||
func TestGetStringAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetStringAnnotation("", nil)
|
||||
_, err := GetStringAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but none returned")
|
||||
}
|
||||
|
@ -109,7 +109,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetStringAnnotation(test.field, ing)
|
||||
s, err := GetStringAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but none returned", test.name)
|
||||
|
@ -133,7 +133,7 @@ rewrite (?i)/arcgis/services/Utilities/Geometry/GeometryServer(.*)$ /arcgis/serv
|
|||
func TestGetFloatAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetFloatAnnotation("", nil)
|
||||
_, err := GetFloatAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
@ -156,7 +156,7 @@ func TestGetFloatAnnotation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetFloatAnnotation(test.field, ing)
|
||||
s, err := GetFloatAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
@ -174,7 +174,7 @@ func TestGetFloatAnnotation(t *testing.T) {
|
|||
func TestGetIntAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
_, err := GetIntAnnotation("", nil)
|
||||
_, err := GetIntAnnotation("", nil, nil)
|
||||
if err == nil {
|
||||
t.Errorf("expected error but retuned nil")
|
||||
}
|
||||
|
@ -196,7 +196,7 @@ func TestGetIntAnnotation(t *testing.T) {
|
|||
for _, test := range tests {
|
||||
data[GetAnnotationWithPrefix(test.field)] = test.value
|
||||
|
||||
s, err := GetIntAnnotation(test.field, ing)
|
||||
s, err := GetIntAnnotation(test.field, ing, nil)
|
||||
if test.expErr {
|
||||
if err == nil {
|
||||
t.Errorf("%v: expected error but retuned nil", test.name)
|
||||
|
@ -224,6 +224,7 @@ func TestStringToURL(t *testing.T) {
|
|||
}{
|
||||
{"empty", "", "url scheme is empty", nil, true},
|
||||
{"no scheme", "bar", "url scheme is empty", nil, true},
|
||||
{"invalid parse", "://lala.com", "://lala.com is not a valid URL: parse \"://lala.com\": missing protocol scheme", nil, true},
|
||||
{"invalid host", "http://", "url host is empty", nil, true},
|
||||
{"invalid host (multiple dots)", "http://foo..bar.com", "invalid url host", nil, true},
|
||||
{"valid URL", validURL, "", validParsedURL, false},
|
||||
|
|
239
internal/ingress/annotations/parser/validators.go
Normal file
239
internal/ingress/annotations/parser/validators.go
Normal file
|
@ -0,0 +1,239 @@
|
|||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
machineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
type AnnotationValidator func(string) error
|
||||
|
||||
const (
|
||||
AnnotationRiskLow AnnotationRisk = iota
|
||||
AnnotationRiskMedium
|
||||
AnnotationRiskHigh
|
||||
AnnotationRiskCritical
|
||||
)
|
||||
|
||||
var (
|
||||
alphaNumericChars = `\-\.\_\~a-zA-Z0-9\/:`
|
||||
extendedAlphaNumeric = alphaNumericChars + ", "
|
||||
regexEnabledChars = regexp.QuoteMeta(`^$[](){}*+?|&=\`)
|
||||
urlEnabledChars = regexp.QuoteMeta(`:?&=`)
|
||||
)
|
||||
|
||||
// IsValidRegex checks if the tested string can be used as a regex, but without any weird character.
|
||||
// It includes regex characters for paths that may contain regexes
|
||||
var IsValidRegex = regexp.MustCompile("^[/" + alphaNumericChars + regexEnabledChars + "]*$")
|
||||
|
||||
// SizeRegex validates sizes understood by NGINX, like 1000, 100k, 1000M
|
||||
var SizeRegex = regexp.MustCompile("^(?i)[0-9]+[bkmg]?$")
|
||||
|
||||
// URLRegex is used to validate a URL but with only a specific set of characters:
|
||||
// It is alphanumericChar + ":", "?", "&"
|
||||
// A valid URL would be proto://something.com:port/something?arg=param
|
||||
var (
|
||||
// URLIsValidRegex is used on full URLs, containing query strings (:, ? and &)
|
||||
URLIsValidRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "]*$")
|
||||
// BasicChars is alphanumeric and ".", "-", "_", "~" and ":", usually used on simple host:port/path composition.
|
||||
// This combination can also be used on fields that may contain characters like / (as ns/name)
|
||||
BasicCharsRegex = regexp.MustCompile("^[/" + alphaNumericChars + "]*$")
|
||||
// ExtendedChars is alphanumeric and ".", "-", "_", "~" and ":" plus "," and spaces, usually used on simple host:port/path composition
|
||||
ExtendedCharsRegex = regexp.MustCompile("^[/" + extendedAlphaNumeric + "]*$")
|
||||
// CharsWithSpace is like basic chars, but includes the space character
|
||||
CharsWithSpace = regexp.MustCompile("^[/" + alphaNumericChars + " ]*$")
|
||||
// NGINXVariable allows entries with alphanumeric characters, -, _ and the special "$"
|
||||
NGINXVariable = regexp.MustCompile(`^[A-Za-z0-9\-\_\$\{\}]*$`)
|
||||
// RegexPathWithCapture allows entries that SHOULD start with "/" and may contain alphanumeric + capture
|
||||
// character for regex based paths, like /something/$1/anything/$2
|
||||
RegexPathWithCapture = regexp.MustCompile(`^/[` + alphaNumericChars + `\/\$]*$`)
|
||||
// HeadersVariable defines a regex that allows headers separated by comma
|
||||
HeadersVariable = regexp.MustCompile(`^[A-Za-z0-9-_, ]*$`)
|
||||
// URLWithNginxVariableRegex defines a url that can contain nginx variables.
|
||||
// It is a risky operation
|
||||
URLWithNginxVariableRegex = regexp.MustCompile("^[" + alphaNumericChars + urlEnabledChars + "$]*$")
|
||||
)
|
||||
|
||||
// ValidateArrayOfServerName validates if all fields on a Server name annotation are
|
||||
// regexes. They can be *.something*, ~^www\d+\.example\.com$ but not fancy character
|
||||
func ValidateArrayOfServerName(value string) error {
|
||||
for _, fqdn := range strings.Split(value, ",") {
|
||||
if err := ValidateServerName(fqdn); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateServerName validates if the passed value is an acceptable server name. The server name
|
||||
// can contain regex characters, as those are accepted values on nginx configuration
|
||||
func ValidateServerName(value string) error {
|
||||
value = strings.TrimSpace(value)
|
||||
if !IsValidRegex.MatchString(value) {
|
||||
return fmt.Errorf("value %s is invalid server name", value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateRegex receives a regex as an argument and uses it to validate
|
||||
// the value of the field.
|
||||
// Annotation can define if the spaces should be trimmed before validating the value
|
||||
func ValidateRegex(regex regexp.Regexp, removeSpace bool) AnnotationValidator {
|
||||
return func(s string) error {
|
||||
if removeSpace {
|
||||
s = strings.ReplaceAll(s, " ", "")
|
||||
}
|
||||
if !regex.MatchString(s) {
|
||||
return fmt.Errorf("value %s is invalid", s)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateOptions receives an array of valid options that can be the value of annotation.
|
||||
// If no valid option is found, it will return an error
|
||||
func ValidateOptions(options []string, caseSensitive bool, trimSpace bool) AnnotationValidator {
|
||||
return func(s string) error {
|
||||
if trimSpace {
|
||||
s = strings.TrimSpace(s)
|
||||
}
|
||||
if !caseSensitive {
|
||||
s = strings.ToLower(s)
|
||||
}
|
||||
for _, option := range options {
|
||||
if s == option {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf("value does not match any valid option")
|
||||
}
|
||||
}
|
||||
|
||||
// ValidateBool validates if the specified value is a bool
|
||||
func ValidateBool(value string) error {
|
||||
_, err := strconv.ParseBool(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateInt validates if the specified value is an integer
|
||||
func ValidateInt(value string) error {
|
||||
_, err := strconv.Atoi(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateCIDRs validates if the specified value is an array of IPs and CIDRs
|
||||
func ValidateCIDRs(value string) error {
|
||||
_, err := net.ParseCIDRs(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateDuration validates if the specified value is a valid time
|
||||
func ValidateDuration(value string) error {
|
||||
_, err := time.ParseDuration(value)
|
||||
return err
|
||||
}
|
||||
|
||||
// ValidateNull always return null values and should not be widely used.
|
||||
// It is used on the "snippet" annotations, as it is up to the admin to allow its
|
||||
// usage, knowing it can be critical!
|
||||
func ValidateNull(value string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateServiceName validates if a provided service name is a valid string
|
||||
func ValidateServiceName(value string) error {
|
||||
errs := machineryvalidation.NameIsDNS1035Label(value, false)
|
||||
if len(errs) != 0 {
|
||||
return fmt.Errorf("annotation does not contain a valid service name: %+v", errs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkAnnotations will check each annotation for:
|
||||
// 1 - Does it contain the internal validation and docs config?
|
||||
// 2 - Does the ingress contains annotations? (validate null pointers)
|
||||
// 3 - Does it contains a validator? Should it contain a validator (not containing is a bug!)
|
||||
// 4 - Does the annotation contain aliases? So we should use if the alias is defined an the annotation not.
|
||||
// 4 - Runs the validator on the value
|
||||
// It will return the full annotation name if all is fine
|
||||
func checkAnnotation(name string, ing *networking.Ingress, fields AnnotationFields) (string, error) {
|
||||
var validateFunc AnnotationValidator
|
||||
if fields != nil {
|
||||
config, ok := fields[name]
|
||||
if !ok {
|
||||
return "", fmt.Errorf("annotation does not contain a valid internal configuration, this is an Ingress Controller issue! Please raise an issue on github.com/kubernetes/ingress-nginx")
|
||||
}
|
||||
validateFunc = config.Validator
|
||||
}
|
||||
|
||||
if ing == nil || len(ing.GetAnnotations()) == 0 {
|
||||
return "", ing_errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
annotationFullName := GetAnnotationWithPrefix(name)
|
||||
if annotationFullName == "" {
|
||||
return "", ing_errors.ErrInvalidAnnotationName
|
||||
}
|
||||
|
||||
annotationValue := ing.GetAnnotations()[annotationFullName]
|
||||
if fields != nil {
|
||||
if validateFunc == nil {
|
||||
return "", fmt.Errorf("annotation does not contain a validator. This is an ingress-controller bug. Please open an issue")
|
||||
}
|
||||
if annotationValue == "" {
|
||||
for _, annotationAlias := range fields[name].AnnotationAliases {
|
||||
tempAnnotationFullName := GetAnnotationWithPrefix(annotationAlias)
|
||||
if aliasVal := ing.GetAnnotations()[tempAnnotationFullName]; aliasVal != "" {
|
||||
annotationValue = aliasVal
|
||||
annotationFullName = tempAnnotationFullName
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
// We don't run validation against empty values
|
||||
if EnableAnnotationValidation && annotationValue != "" {
|
||||
if err := validateFunc(annotationValue); err != nil {
|
||||
klog.Warningf("validation error on ingress %s/%s: annotation %s contains invalid value %s", ing.GetNamespace(), ing.GetName(), name, annotationValue)
|
||||
return "", ing_errors.NewValidationError(annotationFullName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return annotationFullName, nil
|
||||
}
|
||||
|
||||
func CheckAnnotationRisk(annotations map[string]string, maxrisk AnnotationRisk, config AnnotationFields) error {
|
||||
var err error
|
||||
for annotation := range annotations {
|
||||
annPure := TrimAnnotationPrefix(annotation)
|
||||
if cfg, ok := config[annPure]; ok && cfg.Risk > maxrisk {
|
||||
err = errors.Join(err, fmt.Errorf("annotation %s is too risky for environment", annotation))
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
310
internal/ingress/annotations/parser/validators_test.go
Normal file
310
internal/ingress/annotations/parser/validators_test.go
Normal file
|
@ -0,0 +1,310 @@
|
|||
/*
|
||||
Copyright 2023 The Kubernetes Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestValidateArrayOfServerName(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "should accept common name",
|
||||
value: "something.com,anything.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should accept wildcard name",
|
||||
value: "*.something.com,otherthing.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with spaces between array and some regexes",
|
||||
value: `~^www\d+\.example\.com$,something.com`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with regexes",
|
||||
value: `http://some.test.env.com:2121/$someparam=1&$someotherparam=2`,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should allow names with wildcard in middle common name",
|
||||
value: "*.so*mething.com,bla.com",
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "should deny names with weird characters",
|
||||
value: "something.com,lolo;xpto.com,nothing.com",
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidateArrayOfServerName(tt.value); (err != nil) != tt.wantErr {
|
||||
t.Errorf("ValidateArrayOfServerName() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_checkAnnotation(t *testing.T) {
|
||||
type args struct {
|
||||
name string
|
||||
ing *networking.Ingress
|
||||
fields AnnotationFields
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want string
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "null ingress should error",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-random-annotation",
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "not having a validator for a specific annotation is a bug",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"otherannotation": AnnotationConfig{
|
||||
Validator: func(value string) error { return nil },
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotationconfig found and no validation func defined on annotation is a bug",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-invalid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-invalid-annotation": AnnotationConfig{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no annotation can turn into a null pointer and should fail",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-invalid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-invalid-annotation": AnnotationConfig{},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "no AnnotationField config should bypass validations",
|
||||
want: GetAnnotationWithPrefix("some-valid-annotation"),
|
||||
args: args{
|
||||
name: "some-valid-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-valid-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "annotation with invalid value should fail",
|
||||
want: "",
|
||||
args: args{
|
||||
name: "some-new-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-new-annotation"): "xpto1",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-new-annotation": AnnotationConfig{
|
||||
Validator: func(value string) error {
|
||||
if value != "xpto" {
|
||||
return fmt.Errorf("this is an error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "annotation with valid value should pass",
|
||||
want: GetAnnotationWithPrefix("some-other-annotation"),
|
||||
args: args{
|
||||
name: "some-other-annotation",
|
||||
ing: &networking.Ingress{
|
||||
ObjectMeta: v1.ObjectMeta{
|
||||
Annotations: map[string]string{
|
||||
GetAnnotationWithPrefix("some-other-annotation"): "xpto",
|
||||
},
|
||||
},
|
||||
},
|
||||
fields: AnnotationFields{
|
||||
"some-other-annotation": AnnotationConfig{
|
||||
Validator: func(value string) error {
|
||||
if value != "xpto" {
|
||||
return fmt.Errorf("this is an error")
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := checkAnnotation(tt.args.name, tt.args.ing, tt.args.fields)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("checkAnnotation() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if got != tt.want {
|
||||
t.Errorf("checkAnnotation() = %v, want %v", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckAnnotationRisk(t *testing.T) {
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
annotations map[string]string
|
||||
maxrisk AnnotationRisk
|
||||
config AnnotationFields
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "high risk should not be accepted with maximum medium",
|
||||
maxrisk: AnnotationRiskMedium,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskHigh,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskMedium,
|
||||
},
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "high risk should be accepted with maximum critical",
|
||||
maxrisk: AnnotationRiskCritical,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskHigh,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskMedium,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "low risk should be accepted with maximum low",
|
||||
maxrisk: AnnotationRiskLow,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskLow,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskLow,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "critical risk should be accepted with maximum critical",
|
||||
maxrisk: AnnotationRiskCritical,
|
||||
annotations: map[string]string{
|
||||
"nginx.ingress.kubernetes.io/bla": "blo",
|
||||
"nginx.ingress.kubernetes.io/bli": "bl3",
|
||||
},
|
||||
config: AnnotationFields{
|
||||
"bla": {
|
||||
Risk: AnnotationRiskCritical,
|
||||
},
|
||||
"bli": {
|
||||
Risk: AnnotationRiskCritical,
|
||||
},
|
||||
},
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := CheckAnnotationRisk(tt.annotations, tt.maxrisk, tt.config); (err != nil) != tt.wantErr {
|
||||
t.Errorf("CheckAnnotationRisk() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -23,22 +23,51 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
portsInRedirectAnnotation = "use-port-in-redirects"
|
||||
)
|
||||
|
||||
var portsInRedirectAnnotations = parser.Annotation{
|
||||
Group: "redirect",
|
||||
Annotations: parser.AnnotationFields{
|
||||
portsInRedirectAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Enables or disables specifying the port in absolute redirects issued by nginx.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type portInRedirect struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new port in redirect annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return portInRedirect{r}
|
||||
return portInRedirect{
|
||||
r: r,
|
||||
annotationConfig: portsInRedirectAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
// rule used to indicate if the redirects must
|
||||
func (a portInRedirect) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
up, err := parser.GetBoolAnnotation("use-port-in-redirects", ing)
|
||||
up, err := parser.GetBoolAnnotation(portsInRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return a.r.GetDefaultBackend().UsePortInRedirects, nil
|
||||
}
|
||||
|
||||
return up, nil
|
||||
}
|
||||
|
||||
func (a portInRedirect) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a portInRedirect) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, portsInRedirectAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ limitations under the License.
|
|||
package portinredirect
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
api "k8s.io/api/core/v1"
|
||||
|
@ -84,23 +83,24 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
|||
func TestPortInRedirect(t *testing.T) {
|
||||
tests := []struct {
|
||||
title string
|
||||
usePort *bool
|
||||
usePort string
|
||||
def bool
|
||||
exp bool
|
||||
}{
|
||||
{"false - default false", newFalse(), false, false},
|
||||
{"false - default true", newFalse(), true, false},
|
||||
{"no annotation - default false", nil, false, false},
|
||||
{"no annotation - default true", nil, true, true},
|
||||
{"true - default true", newTrue(), true, true},
|
||||
{"false - default false", "false", false, false},
|
||||
{"false - default true", "false", true, false},
|
||||
{"no annotation - default false", "", false, false},
|
||||
{"no annotation - default false", "not-a-bool", false, false},
|
||||
{"no annotation - default true", "", true, true},
|
||||
{"true - default true", "true", true, true},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
if test.usePort != nil {
|
||||
data[parser.GetAnnotationWithPrefix("use-port-in-redirects")] = fmt.Sprintf("%v", *test.usePort)
|
||||
if test.usePort != "" {
|
||||
data[parser.GetAnnotationWithPrefix(portsInRedirectAnnotation)] = test.usePort
|
||||
}
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -118,13 +118,3 @@ func TestPortInRedirect(t *testing.T) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func newTrue() *bool {
|
||||
b := true
|
||||
return &b
|
||||
}
|
||||
|
||||
func newFalse() *bool {
|
||||
b := false
|
||||
return &b
|
||||
}
|
||||
|
|
|
@ -17,12 +17,150 @@ limitations under the License.
|
|||
package proxy
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
proxyConnectTimeoutAnnotation = "proxy-connect-timeout"
|
||||
proxySendTimeoutAnnotation = "proxy-send-timeout"
|
||||
proxyReadTimeoutAnnotation = "proxy-read-timeout"
|
||||
proxyBuffersNumberAnnotation = "proxy-buffers-number"
|
||||
proxyBufferSizeAnnotation = "proxy-buffer-size"
|
||||
proxyCookiePathAnnotation = "proxy-cookie-path"
|
||||
proxyCookieDomainAnnotation = "proxy-cookie-domain"
|
||||
proxyBodySizeAnnotation = "proxy-body-size"
|
||||
proxyNextUpstreamAnnotation = "proxy-next-upstream"
|
||||
proxyNextUpstreamTimeoutAnnotation = "proxy-next-upstream-timeout"
|
||||
proxyNextUpstreamTriesAnnotation = "proxy-next-upstream-tries"
|
||||
proxyRequestBufferingAnnotation = "proxy-request-buffering"
|
||||
proxyRedirectFromAnnotation = "proxy-redirect-from"
|
||||
proxyRedirectToAnnotation = "proxy-redirect-to"
|
||||
proxyBufferingAnnotation = "proxy-buffering"
|
||||
proxyHTTPVersionAnnotation = "proxy-http-version"
|
||||
proxyMaxTempFileSizeAnnotation = "proxy-max-temp-file-size"
|
||||
)
|
||||
|
||||
var (
|
||||
validUpstreamAnnotation = regexp.MustCompile(`^((error|timeout|invalid_header|http_500|http_502|http_503|http_504|http_403|http_404|http_429|non_idempotent|off)\s?)+$`)
|
||||
)
|
||||
|
||||
var proxyAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
proxyConnectTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the timeout in seconds of the connect operation to the backend.`,
|
||||
},
|
||||
proxySendTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the timeout in seconds of the send operation to the backend.`,
|
||||
},
|
||||
proxyReadTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation allows setting the timeout in seconds of the read operation to the backend.`,
|
||||
},
|
||||
proxyBuffersNumberAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation sets the number of the buffers in proxy_buffers used for reading the first part of the response received from the proxied server.
|
||||
By default proxy buffers number is set as 4`,
|
||||
},
|
||||
proxyBufferSizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation sets the size of the buffer proxy_buffer_size used for reading the first part of the response received from the proxied server.
|
||||
By default proxy buffer size is set as "4k".`,
|
||||
},
|
||||
proxyCookiePathAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets a text that should be changed in the path attribute of the "Set-Cookie" header fields of a proxied server response.`,
|
||||
},
|
||||
proxyCookieDomainAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation ets a text that should be changed in the domain attribute of the "Set-Cookie" header fields of a proxied server response.`,
|
||||
},
|
||||
proxyBodySizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows setting the maximum allowed size of a client request body.`,
|
||||
},
|
||||
proxyNextUpstreamAnnotation: {
|
||||
Validator: parser.ValidateRegex(*validUpstreamAnnotation, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines when the next upstream should be used.
|
||||
This annotation reflect the directive https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_next_upstream
|
||||
and only the allowed values on upstream are allowed here.`,
|
||||
},
|
||||
proxyNextUpstreamTimeoutAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation limits the time during which a request can be passed to the next server`,
|
||||
},
|
||||
proxyNextUpstreamTriesAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation limits the number of possible tries for passing a request to the next server`,
|
||||
},
|
||||
proxyRequestBufferingAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"on", "off"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables buffering of a client request body.`,
|
||||
},
|
||||
proxyRedirectFromAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`,
|
||||
},
|
||||
proxyRedirectToAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `The annotations proxy-redirect-from and proxy-redirect-to will set the first and second parameters of NGINX's proxy_redirect directive respectively`,
|
||||
},
|
||||
proxyBufferingAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"on", "off"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables buffering of responses from the proxied server. It can be "on" or "off"`,
|
||||
},
|
||||
proxyHTTPVersionAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"1.0", "1.1"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotations sets the HTTP protocol version for proxying. Can be "1.0" or "1.1".`,
|
||||
},
|
||||
proxyMaxTempFileSizeAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.SizeRegex, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the maximum size of a temporary file when buffering responses.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config returns the proxy timeout to use in the upstream server/s
|
||||
type Config struct {
|
||||
BodySize string `json:"bodySize"`
|
||||
|
@ -109,12 +247,15 @@ func (l1 *Config) Equal(l2 *Config) bool {
|
|||
}
|
||||
|
||||
type proxy struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new reverse proxy configuration annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return proxy{r}
|
||||
return proxy{r: r,
|
||||
annotationConfig: proxyAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -125,90 +266,99 @@ func (a proxy) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
var err error
|
||||
|
||||
config.ConnectTimeout, err = parser.GetIntAnnotation("proxy-connect-timeout", ing)
|
||||
config.ConnectTimeout, err = parser.GetIntAnnotation(proxyConnectTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ConnectTimeout = defBackend.ProxyConnectTimeout
|
||||
}
|
||||
|
||||
config.SendTimeout, err = parser.GetIntAnnotation("proxy-send-timeout", ing)
|
||||
config.SendTimeout, err = parser.GetIntAnnotation(proxySendTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.SendTimeout = defBackend.ProxySendTimeout
|
||||
}
|
||||
|
||||
config.ReadTimeout, err = parser.GetIntAnnotation("proxy-read-timeout", ing)
|
||||
config.ReadTimeout, err = parser.GetIntAnnotation(proxyReadTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ReadTimeout = defBackend.ProxyReadTimeout
|
||||
}
|
||||
|
||||
config.BuffersNumber, err = parser.GetIntAnnotation("proxy-buffers-number", ing)
|
||||
config.BuffersNumber, err = parser.GetIntAnnotation(proxyBuffersNumberAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.BuffersNumber = defBackend.ProxyBuffersNumber
|
||||
}
|
||||
|
||||
config.BufferSize, err = parser.GetStringAnnotation("proxy-buffer-size", ing)
|
||||
config.BufferSize, err = parser.GetStringAnnotation(proxyBufferSizeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.BufferSize = defBackend.ProxyBufferSize
|
||||
}
|
||||
|
||||
config.CookiePath, err = parser.GetStringAnnotation("proxy-cookie-path", ing)
|
||||
config.CookiePath, err = parser.GetStringAnnotation(proxyCookiePathAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.CookiePath = defBackend.ProxyCookiePath
|
||||
}
|
||||
|
||||
config.CookieDomain, err = parser.GetStringAnnotation("proxy-cookie-domain", ing)
|
||||
config.CookieDomain, err = parser.GetStringAnnotation(proxyCookieDomainAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.CookieDomain = defBackend.ProxyCookieDomain
|
||||
}
|
||||
|
||||
config.BodySize, err = parser.GetStringAnnotation("proxy-body-size", ing)
|
||||
config.BodySize, err = parser.GetStringAnnotation(proxyBodySizeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.BodySize = defBackend.ProxyBodySize
|
||||
}
|
||||
|
||||
config.NextUpstream, err = parser.GetStringAnnotation("proxy-next-upstream", ing)
|
||||
config.NextUpstream, err = parser.GetStringAnnotation(proxyNextUpstreamAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.NextUpstream = defBackend.ProxyNextUpstream
|
||||
}
|
||||
|
||||
config.NextUpstreamTimeout, err = parser.GetIntAnnotation("proxy-next-upstream-timeout", ing)
|
||||
config.NextUpstreamTimeout, err = parser.GetIntAnnotation(proxyNextUpstreamTimeoutAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.NextUpstreamTimeout = defBackend.ProxyNextUpstreamTimeout
|
||||
}
|
||||
|
||||
config.NextUpstreamTries, err = parser.GetIntAnnotation("proxy-next-upstream-tries", ing)
|
||||
config.NextUpstreamTries, err = parser.GetIntAnnotation(proxyNextUpstreamTriesAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.NextUpstreamTries = defBackend.ProxyNextUpstreamTries
|
||||
}
|
||||
|
||||
config.RequestBuffering, err = parser.GetStringAnnotation("proxy-request-buffering", ing)
|
||||
config.RequestBuffering, err = parser.GetStringAnnotation(proxyRequestBufferingAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.RequestBuffering = defBackend.ProxyRequestBuffering
|
||||
}
|
||||
|
||||
config.ProxyRedirectFrom, err = parser.GetStringAnnotation("proxy-redirect-from", ing)
|
||||
config.ProxyRedirectFrom, err = parser.GetStringAnnotation(proxyRedirectFromAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyRedirectFrom = defBackend.ProxyRedirectFrom
|
||||
}
|
||||
|
||||
config.ProxyRedirectTo, err = parser.GetStringAnnotation("proxy-redirect-to", ing)
|
||||
config.ProxyRedirectTo, err = parser.GetStringAnnotation(proxyRedirectToAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyRedirectTo = defBackend.ProxyRedirectTo
|
||||
}
|
||||
|
||||
config.ProxyBuffering, err = parser.GetStringAnnotation("proxy-buffering", ing)
|
||||
config.ProxyBuffering, err = parser.GetStringAnnotation(proxyBufferingAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyBuffering = defBackend.ProxyBuffering
|
||||
}
|
||||
|
||||
config.ProxyHTTPVersion, err = parser.GetStringAnnotation("proxy-http-version", ing)
|
||||
config.ProxyHTTPVersion, err = parser.GetStringAnnotation(proxyHTTPVersionAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyHTTPVersion = defBackend.ProxyHTTPVersion
|
||||
}
|
||||
|
||||
config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation("proxy-max-temp-file-size", ing)
|
||||
config.ProxyMaxTempFileSize, err = parser.GetStringAnnotation(proxyMaxTempFileSizeAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.ProxyMaxTempFileSize = defBackend.ProxyMaxTempFileSize
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a proxy) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a proxy) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, proxyAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -161,6 +161,74 @@ func TestProxy(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestProxyComplex(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("proxy-connect-timeout")] = "1"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-send-timeout")] = "2"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-read-timeout")] = "3"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-buffers-number")] = "8"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-buffer-size")] = "1k"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-body-size")] = "2k"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-next-upstream")] = "error http_502"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-next-upstream-timeout")] = "5"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-next-upstream-tries")] = "3"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-request-buffering")] = "off"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-buffering")] = "on"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-http-version")] = "1.0"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-max-temp-file-size")] = "128k"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error parsing a valid")
|
||||
}
|
||||
p, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Fatalf("expected a Config type")
|
||||
}
|
||||
if p.ConnectTimeout != 1 {
|
||||
t.Errorf("expected 1 as connect-timeout but returned %v", p.ConnectTimeout)
|
||||
}
|
||||
if p.SendTimeout != 2 {
|
||||
t.Errorf("expected 2 as send-timeout but returned %v", p.SendTimeout)
|
||||
}
|
||||
if p.ReadTimeout != 3 {
|
||||
t.Errorf("expected 3 as read-timeout but returned %v", p.ReadTimeout)
|
||||
}
|
||||
if p.BuffersNumber != 8 {
|
||||
t.Errorf("expected 8 as proxy-buffers-number but returned %v", p.BuffersNumber)
|
||||
}
|
||||
if p.BufferSize != "1k" {
|
||||
t.Errorf("expected 1k as buffer-size but returned %v", p.BufferSize)
|
||||
}
|
||||
if p.BodySize != "2k" {
|
||||
t.Errorf("expected 2k as body-size but returned %v", p.BodySize)
|
||||
}
|
||||
if p.NextUpstream != "error http_502" {
|
||||
t.Errorf("expected off as next-upstream but returned %v", p.NextUpstream)
|
||||
}
|
||||
if p.NextUpstreamTimeout != 5 {
|
||||
t.Errorf("expected 5 as next-upstream-timeout but returned %v", p.NextUpstreamTimeout)
|
||||
}
|
||||
if p.NextUpstreamTries != 3 {
|
||||
t.Errorf("expected 3 as next-upstream-tries but returned %v", p.NextUpstreamTries)
|
||||
}
|
||||
if p.RequestBuffering != "off" {
|
||||
t.Errorf("expected off as request-buffering but returned %v", p.RequestBuffering)
|
||||
}
|
||||
if p.ProxyBuffering != "on" {
|
||||
t.Errorf("expected on as proxy-buffering but returned %v", p.ProxyBuffering)
|
||||
}
|
||||
if p.ProxyHTTPVersion != "1.0" {
|
||||
t.Errorf("expected 1.0 as proxy-http-version but returned %v", p.ProxyHTTPVersion)
|
||||
}
|
||||
if p.ProxyMaxTempFileSize != "128k" {
|
||||
t.Errorf("expected 128k as proxy-max-temp-file-size but returned %v", p.ProxyMaxTempFileSize)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProxyWithNoAnnotation(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
|
|
|
@ -24,9 +24,11 @@ import (
|
|||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
ing_errors "k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/k8s"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -39,9 +41,73 @@ const (
|
|||
|
||||
var (
|
||||
proxySSLOnOffRegex = regexp.MustCompile(`^(on|off)$`)
|
||||
proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3)$`)
|
||||
proxySSLProtocolRegex = regexp.MustCompile(`^(SSLv2|SSLv3|TLSv1|TLSv1\.1|TLSv1\.2|TLSv1\.3| )*$`)
|
||||
proxySSLCiphersRegex = regexp.MustCompile(`^[A-Za-z0-9\+\:\_\-\!]*$`)
|
||||
)
|
||||
|
||||
const (
|
||||
proxySSLSecretAnnotation = "proxy-ssl-secret"
|
||||
proxySSLCiphersAnnotation = "proxy-ssl-ciphers"
|
||||
proxySSLProtocolsAnnotation = "proxy-ssl-protocols"
|
||||
proxySSLNameAnnotation = "proxy-ssl-name"
|
||||
proxySSLVerifyAnnotation = "proxy-ssl-verify"
|
||||
proxySSLVerifyDepthAnnotation = "proxy-ssl-verify-depth"
|
||||
proxySSLServerNameAnnotation = "proxy-ssl-server-name"
|
||||
)
|
||||
|
||||
var proxySSLAnnotation = parser.Annotation{
|
||||
Group: "proxy",
|
||||
Annotations: parser.AnnotationFields{
|
||||
proxySSLSecretAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation specifies a Secret with the certificate tls.crt, key tls.key in PEM format used for authentication to a proxied HTTPS server.
|
||||
It should also contain trusted CA certificates ca.crt in PEM format used to verify the certificate of the proxied HTTPS server.
|
||||
This annotation expects the Secret name in the form "namespace/secretName"
|
||||
Just secrets on the same namespace of the ingress can be used.`,
|
||||
},
|
||||
proxySSLCiphersAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLCiphersRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation Specifies the enabled ciphers for requests to a proxied HTTPS server.
|
||||
The ciphers are specified in the format understood by the OpenSSL library.`,
|
||||
},
|
||||
proxySSLProtocolsAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLProtocolRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables the specified protocols for requests to a proxied HTTPS server.`,
|
||||
},
|
||||
proxySSLNameAnnotation: {
|
||||
Validator: parser.ValidateServerName,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskHigh,
|
||||
Documentation: `This annotation allows to set proxy_ssl_name. This allows overriding the server name used to verify the certificate of the proxied HTTPS server.
|
||||
This value is also passed through SNI when a connection is established to the proxied HTTPS server.`,
|
||||
},
|
||||
proxySSLVerifyAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables or disables verification of the proxied HTTPS server certificate. (default: off)`,
|
||||
},
|
||||
proxySSLVerifyDepthAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation Sets the verification depth in the proxied HTTPS server certificates chain. (default: 1).`,
|
||||
},
|
||||
proxySSLServerNameAnnotation: {
|
||||
Validator: parser.ValidateRegex(*proxySSLOnOffRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables passing of the server name through TLS Server Name Indication extension (SNI, RFC 6066) when establishing a connection with the proxied HTTPS server.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config contains the AuthSSLCert used for mutual authentication
|
||||
// and the configured VerifyDepth
|
||||
type Config struct {
|
||||
|
@ -85,11 +151,14 @@ func (pssl1 *Config) Equal(pssl2 *Config) bool {
|
|||
|
||||
// NewParser creates a new TLS authentication annotation parser
|
||||
func NewParser(resolver resolver.Resolver) parser.IngressAnnotation {
|
||||
return proxySSL{resolver}
|
||||
return proxySSL{
|
||||
r: resolver,
|
||||
annotationConfig: proxySSLAnnotation}
|
||||
}
|
||||
|
||||
type proxySSL struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
func sortProtocols(protocols string) string {
|
||||
|
@ -120,16 +189,22 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
proxysslsecret, err := parser.GetStringAnnotation("proxy-ssl-secret", ing)
|
||||
proxysslsecret, err := parser.GetStringAnnotation(proxySSLSecretAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
return &Config{}, err
|
||||
}
|
||||
|
||||
_, _, err = k8s.ParseNameNS(proxysslsecret)
|
||||
ns, _, err := k8s.ParseNameNS(proxysslsecret)
|
||||
if err != nil {
|
||||
return &Config{}, ing_errors.NewLocationDenied(err.Error())
|
||||
}
|
||||
|
||||
secCfg := p.r.GetSecurityConfiguration()
|
||||
// We don't accept different namespaces for secrets.
|
||||
if !secCfg.AllowCrossNamespaceResources && ns != ing.Namespace {
|
||||
return &Config{}, ing_errors.NewLocationDenied("cross namespace secrets are not supported")
|
||||
}
|
||||
|
||||
proxyCert, err := p.r.GetAuthCertificate(proxysslsecret)
|
||||
if err != nil {
|
||||
e := fmt.Errorf("error obtaining certificate: %w", err)
|
||||
|
@ -137,37 +212,55 @@ func (p proxySSL) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
config.AuthSSLCert = *proxyCert
|
||||
|
||||
config.Ciphers, err = parser.GetStringAnnotation("proxy-ssl-ciphers", ing)
|
||||
config.Ciphers, err = parser.GetStringAnnotation(proxySSLCiphersAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("invalid value passed to proxy-ssl-ciphers, defaulting to %s", defaultProxySSLCiphers)
|
||||
}
|
||||
config.Ciphers = defaultProxySSLCiphers
|
||||
}
|
||||
|
||||
config.Protocols, err = parser.GetStringAnnotation("proxy-ssl-protocols", ing)
|
||||
config.Protocols, err = parser.GetStringAnnotation(proxySSLProtocolsAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("invalid value passed to proxy-ssl-protocols, defaulting to %s", defaultProxySSLProtocols)
|
||||
}
|
||||
config.Protocols = defaultProxySSLProtocols
|
||||
} else {
|
||||
config.Protocols = sortProtocols(config.Protocols)
|
||||
}
|
||||
|
||||
config.ProxySSLName, err = parser.GetStringAnnotation("proxy-ssl-name", ing)
|
||||
config.ProxySSLName, err = parser.GetStringAnnotation(proxySSLNameAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("invalid value passed to proxy-ssl-name, defaulting to empty")
|
||||
}
|
||||
config.ProxySSLName = ""
|
||||
}
|
||||
|
||||
config.Verify, err = parser.GetStringAnnotation("proxy-ssl-verify", ing)
|
||||
config.Verify, err = parser.GetStringAnnotation(proxySSLVerifyAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil || !proxySSLOnOffRegex.MatchString(config.Verify) {
|
||||
config.Verify = defaultProxySSLVerify
|
||||
}
|
||||
|
||||
config.VerifyDepth, err = parser.GetIntAnnotation("proxy-ssl-verify-depth", ing)
|
||||
config.VerifyDepth, err = parser.GetIntAnnotation(proxySSLVerifyDepthAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil || config.VerifyDepth == 0 {
|
||||
config.VerifyDepth = defaultProxySSLVerifyDepth
|
||||
}
|
||||
|
||||
config.ProxySSLServerName, err = parser.GetStringAnnotation("proxy-ssl-server-name", ing)
|
||||
config.ProxySSLServerName, err = parser.GetStringAnnotation(proxySSLServerNameAnnotation, ing, p.annotationConfig.Annotations)
|
||||
if err != nil || !proxySSLOnOffRegex.MatchString(config.ProxySSLServerName) {
|
||||
config.ProxySSLServerName = defaultProxySSLServerName
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (p proxySSL) GetDocumentation() parser.AnnotationFields {
|
||||
return p.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a proxySSL) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, proxySSLAnnotation.Annotations)
|
||||
}
|
||||
|
|
|
@ -93,7 +93,7 @@ func TestAnnotations(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
data := map[string]string{}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-secret")] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix(proxySSLSecretAnnotation)] = "default/demo-secret"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-ciphers")] = "HIGH:-SHA"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-name")] = "$host"
|
||||
data[parser.GetAnnotationWithPrefix("proxy-ssl-protocols")] = "TLSv1.3 SSLv2 TLSv1 TLSv1.2"
|
||||
|
|
|
@ -24,6 +24,7 @@ import (
|
|||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
"k8s.io/ingress-nginx/internal/net"
|
||||
"k8s.io/ingress-nginx/pkg/util/sets"
|
||||
|
@ -58,7 +59,7 @@ type Config struct {
|
|||
|
||||
ID string `json:"id"`
|
||||
|
||||
Whitelist []string `json:"whitelist"`
|
||||
Allowlist []string `json:"allowlist"`
|
||||
}
|
||||
|
||||
// Equal tests for equality between two RateLimit types
|
||||
|
@ -90,11 +91,11 @@ func (rt1 *Config) Equal(rt2 *Config) bool {
|
|||
if rt1.Name != rt2.Name {
|
||||
return false
|
||||
}
|
||||
if len(rt1.Whitelist) != len(rt2.Whitelist) {
|
||||
if len(rt1.Allowlist) != len(rt2.Allowlist) {
|
||||
return false
|
||||
}
|
||||
|
||||
return sets.StringElementsMatch(rt1.Whitelist, rt2.Whitelist)
|
||||
return sets.StringElementsMatch(rt1.Allowlist, rt2.Allowlist)
|
||||
}
|
||||
|
||||
// Zone returns information about the NGINX rate limit (limit_req_zone)
|
||||
|
@ -131,43 +132,121 @@ func (z1 *Zone) Equal(z2 *Zone) bool {
|
|||
return true
|
||||
}
|
||||
|
||||
const (
|
||||
limitRateAnnotation = "limit-rate"
|
||||
limitRateAfterAnnotation = "limit-rate-after"
|
||||
limitRateRPMAnnotation = "limit-rpm"
|
||||
limitRateRPSAnnotation = "limit-rps"
|
||||
limitRateConnectionsAnnotation = "limit-connections"
|
||||
limitRateBurstMultiplierAnnotation = "limit-burst-multiplier"
|
||||
limitWhitelistAnnotation = "limit-whitelist" // This annotation is an alias for limit-allowlist
|
||||
limitAllowlistAnnotation = "limit-allowlist"
|
||||
)
|
||||
|
||||
var rateLimitAnnotations = parser.Annotation{
|
||||
Group: "rate-limit",
|
||||
Annotations: parser.AnnotationFields{
|
||||
limitRateAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Limits the rate of response transmission to a client. The rate is specified in bytes per second.
|
||||
The zero value disables rate limiting. The limit is set per a request, and so if a client simultaneously opens two connections, the overall rate will be twice as much as the specified limit.
|
||||
References: https://nginx.org/en/docs/http/ngx_http_core_module.html#limit_rate`,
|
||||
},
|
||||
limitRateAfterAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Sets the initial amount after which the further transmission of a response to a client will be rate limited.`,
|
||||
},
|
||||
limitRateRPMAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Requests per minute that will be allowed.`,
|
||||
},
|
||||
limitRateRPSAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Requests per second that will be allowed.`,
|
||||
},
|
||||
limitRateConnectionsAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Number of connections that will be allowed`,
|
||||
},
|
||||
limitRateBurstMultiplierAnnotation: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `Burst multiplier for a limit-rate enabled location.`,
|
||||
},
|
||||
limitAllowlistAnnotation: {
|
||||
Validator: parser.ValidateCIDRs,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `List of CIDR/IP addresses that will not be rate-limited.`,
|
||||
AnnotationAliases: []string{limitWhitelistAnnotation},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type ratelimit struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new ratelimit annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return ratelimit{r}
|
||||
return ratelimit{
|
||||
r: r,
|
||||
annotationConfig: rateLimitAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
// rule used to rewrite the defined paths
|
||||
func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
defBackend := a.r.GetDefaultBackend()
|
||||
lr, err := parser.GetIntAnnotation("limit-rate", ing)
|
||||
lr, err := parser.GetIntAnnotation(limitRateAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
lr = defBackend.LimitRate
|
||||
}
|
||||
lra, err := parser.GetIntAnnotation("limit-rate-after", ing)
|
||||
lra, err := parser.GetIntAnnotation(limitRateAfterAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
lra = defBackend.LimitRateAfter
|
||||
}
|
||||
|
||||
rpm, _ := parser.GetIntAnnotation("limit-rpm", ing)
|
||||
rps, _ := parser.GetIntAnnotation("limit-rps", ing)
|
||||
conn, _ := parser.GetIntAnnotation("limit-connections", ing)
|
||||
burstMultiplier, err := parser.GetIntAnnotation("limit-burst-multiplier", ing)
|
||||
rpm, err := parser.GetIntAnnotation(limitRateRPMAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
rps, err := parser.GetIntAnnotation(limitRateRPSAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
conn, err := parser.GetIntAnnotation(limitRateConnectionsAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
burstMultiplier, err := parser.GetIntAnnotation(limitRateBurstMultiplierAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
burstMultiplier = defBurst
|
||||
}
|
||||
|
||||
val, _ := parser.GetStringAnnotation("limit-whitelist", ing)
|
||||
|
||||
cidrs, err := net.ParseCIDRs(val)
|
||||
if err != nil {
|
||||
val, err := parser.GetStringAnnotation(limitAllowlistAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil && errors.IsValidationError(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cidrs, errCidr := net.ParseCIDRs(val)
|
||||
if errCidr != nil {
|
||||
return nil, errCidr
|
||||
}
|
||||
|
||||
if rpm == 0 && rps == 0 && conn == 0 {
|
||||
return &Config{
|
||||
Connections: Zone{},
|
||||
|
@ -203,7 +282,7 @@ func (a ratelimit) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
LimitRateAfter: lra,
|
||||
Name: zoneName,
|
||||
ID: encode(zoneName),
|
||||
Whitelist: cidrs,
|
||||
Allowlist: cidrs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -211,3 +290,12 @@ func encode(s string) string {
|
|||
str := base64.URLEncoding.EncodeToString([]byte(s))
|
||||
return strings.Replace(str, "=", "", -1)
|
||||
}
|
||||
|
||||
func (a ratelimit) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a ratelimit) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, rateLimitAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ import (
|
|||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/defaults"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
|
@ -85,8 +86,8 @@ func (m mockBackend) GetDefaultBackend() defaults.Backend {
|
|||
func TestWithoutAnnotations(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Error("unexpected error with ingress without annotations")
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
t.Errorf("unexpected error with ingress without annotations: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,22 +95,22 @@ func TestRateLimiting(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("limit-connections")] = "0"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rps")] = "0"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "0"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "0"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "0"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "0"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
_, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error with invalid limits (0)")
|
||||
t.Errorf("unexpected error with invalid limits (0): %s", err)
|
||||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("limit-connections")] = "5"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rps")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -147,12 +148,12 @@ func TestRateLimiting(t *testing.T) {
|
|||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("limit-connections")] = "5"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rps")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rpm")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate-after")] = "100"
|
||||
data[parser.GetAnnotationWithPrefix("limit-rate")] = "10"
|
||||
data[parser.GetAnnotationWithPrefix("limit-burst-multiplier")] = "3"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPSAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateRPMAnnotation)] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAfterAnnotation)] = "100"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateAnnotation)] = "10"
|
||||
data[parser.GetAnnotationWithPrefix(limitRateBurstMultiplierAnnotation)] = "3"
|
||||
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
@ -189,3 +190,61 @@ func TestRateLimiting(t *testing.T) {
|
|||
t.Errorf("expected 10 in limit by limitrate but %v was returned", rateLimit.LimitRate)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAnnotationCIDR(t *testing.T) {
|
||||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.5, 192.168.50.32/24"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
rateLimit, ok := i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if len(rateLimit.Allowlist) != 2 {
|
||||
t.Errorf("expected 2 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
|
||||
}
|
||||
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err = NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
rateLimit, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if len(rateLimit.Allowlist) != 3 {
|
||||
t.Errorf("expected 3 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
|
||||
}
|
||||
|
||||
// Parent annotation surpasses any alias
|
||||
data = map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix(limitRateConnectionsAnnotation)] = "5"
|
||||
data[parser.GetAnnotationWithPrefix(limitWhitelistAnnotation)] = "192.168.0.5, 192.168.50.32/24, 10.10.10.1"
|
||||
data[parser.GetAnnotationWithPrefix(limitAllowlistAnnotation)] = "192.168.0.9"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err = NewParser(mockBackend{}).Parse(ing)
|
||||
if err != nil {
|
||||
t.Errorf("unexpected error: %v", err)
|
||||
}
|
||||
rateLimit, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a RateLimit type")
|
||||
}
|
||||
if len(rateLimit.Allowlist) != 1 {
|
||||
t.Errorf("expected 1 cidrs in limit by ip but %v was returned", len(rateLimit.Allowlist))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -37,13 +37,56 @@ type Config struct {
|
|||
FromToWWW bool `json:"fromToWWW"`
|
||||
}
|
||||
|
||||
const (
|
||||
fromToWWWRedirAnnotation = "from-to-www-redirect"
|
||||
temporalRedirectAnnotation = "temporal-redirect"
|
||||
permanentRedirectAnnotation = "permanent-redirect"
|
||||
permanentRedirectAnnotationCode = "permanent-redirect-code"
|
||||
)
|
||||
|
||||
var redirectAnnotations = parser.Annotation{
|
||||
Group: "redirect",
|
||||
Annotations: parser.AnnotationFields{
|
||||
fromToWWWRedirAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `In some scenarios is required to redirect from www.domain.com to domain.com or vice versa. To enable this feature use this annotation.`,
|
||||
},
|
||||
temporalRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated
|
||||
Documentation: `This annotation allows you to return a temporal redirect (Return Code 302) instead of sending data to the upstream.
|
||||
For example setting this annotation to https://www.google.com would redirect everything to Google with a Return Code of 302 (Moved Temporarily).`,
|
||||
},
|
||||
permanentRedirectAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium, // Medium, as it allows arbitrary URLs that needs to be validated
|
||||
Documentation: `This annotation allows to return a permanent redirect (Return Code 301) instead of sending data to the upstream.
|
||||
For example setting this annotation https://www.google.com would redirect everything to Google with a code 301`,
|
||||
},
|
||||
permanentRedirectAnnotationCode: {
|
||||
Validator: parser.ValidateInt,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
|
||||
Documentation: `This annotation allows you to modify the status code used for permanent redirects.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type redirect struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new redirect annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return redirect{r}
|
||||
return redirect{
|
||||
r: r,
|
||||
annotationConfig: redirectAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress
|
||||
|
@ -51,9 +94,12 @@ func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
|||
// If the Ingress contains both annotations the execution order is
|
||||
// temporal and then permanent
|
||||
func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
r3w, _ := parser.GetBoolAnnotation("from-to-www-redirect", ing)
|
||||
r3w, err := parser.GetBoolAnnotation(fromToWWWRedirAnnotation, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tr, err := parser.GetStringAnnotation("temporal-redirect", ing)
|
||||
tr, err := parser.GetStringAnnotation(temporalRedirectAnnotation, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -70,12 +116,12 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}, nil
|
||||
}
|
||||
|
||||
pr, err := parser.GetStringAnnotation("permanent-redirect", ing)
|
||||
pr, err := parser.GetStringAnnotation(permanentRedirectAnnotation, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prc, err := parser.GetIntAnnotation("permanent-redirect-code", ing)
|
||||
prc, err := parser.GetIntAnnotation(permanentRedirectAnnotationCode, ing, r.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsMissingAnnotations(err) {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -127,3 +173,12 @@ func isValidURL(s string) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a redirect) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a redirect) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, redirectAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -43,7 +43,7 @@ func TestPermanentRedirectWithDefaultCode(t *testing.T) {
|
|||
ing := new(networking.Ingress)
|
||||
|
||||
data := make(map[string]string, 1)
|
||||
data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := rp.Parse(ing)
|
||||
|
@ -81,8 +81,8 @@ func TestPermanentRedirectWithCustomCode(t *testing.T) {
|
|||
ing := new(networking.Ingress)
|
||||
|
||||
data := make(map[string]string, 2)
|
||||
data[parser.GetAnnotationWithPrefix("permanent-redirect")] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix("permanent-redirect-code")] = strconv.Itoa(tc.input)
|
||||
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotation)] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix(permanentRedirectAnnotationCode)] = strconv.Itoa(tc.input)
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := rp.Parse(ing)
|
||||
|
@ -112,8 +112,8 @@ func TestTemporalRedirect(t *testing.T) {
|
|||
ing := new(networking.Ingress)
|
||||
|
||||
data := make(map[string]string, 1)
|
||||
data[parser.GetAnnotationWithPrefix("from-to-www-redirect")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix("temporal-redirect")] = defRedirectURL
|
||||
data[parser.GetAnnotationWithPrefix(fromToWWWRedirAnnotation)] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(temporalRedirectAnnotation)] = defRedirectURL
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, err := rp.Parse(ing)
|
||||
|
|
|
@ -27,6 +27,59 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
rewriteTargetAnnotation = "rewrite-target"
|
||||
sslRedirectAnnotation = "ssl-redirect"
|
||||
preserveTrailingSlashAnnotation = "preserve-trailing-slash"
|
||||
forceSSLRedirectAnnotation = "force-ssl-redirect"
|
||||
useRegexAnnotation = "use-regex"
|
||||
appRootAnnotation = "app-root"
|
||||
)
|
||||
|
||||
var rewriteAnnotations = parser.Annotation{
|
||||
Group: "rewrite",
|
||||
Annotations: parser.AnnotationFields{
|
||||
rewriteTargetAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the target URI where the traffic must be redirected. It can contain regular characters and captured
|
||||
groups specified as '$1', '$2', etc.`,
|
||||
},
|
||||
sslRedirectAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the location section is only accessible via SSL`,
|
||||
},
|
||||
preserveTrailingSlashAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines if the trailing slash should be preserved in the URI with 'ssl-redirect'`,
|
||||
},
|
||||
forceSSLRedirectAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation forces the redirection to HTTPS even if the Ingress is not TLS Enabled`,
|
||||
},
|
||||
useRegexAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines if the paths defined on an Ingress use regular expressions. To use regex on path
|
||||
the pathType should also be defined as 'ImplementationSpecific'.`,
|
||||
},
|
||||
appRootAnnotation: {
|
||||
Validator: parser.ValidateRegex(*parser.RegexPathWithCapture, false),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the Application Root that the Controller must redirect if it's in / context`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Config describes the per location redirect config
|
||||
type Config struct {
|
||||
// Target URI where the traffic must be redirected
|
||||
|
@ -71,12 +124,16 @@ func (r1 *Config) Equal(r2 *Config) bool {
|
|||
}
|
||||
|
||||
type rewrite struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new rewrite annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return rewrite{r}
|
||||
return rewrite{
|
||||
r: r,
|
||||
annotationConfig: rewriteAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -85,24 +142,45 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
config := &Config{}
|
||||
|
||||
config.Target, _ = parser.GetStringAnnotation("rewrite-target", ing)
|
||||
config.SSLRedirect, err = parser.GetBoolAnnotation("ssl-redirect", ing)
|
||||
config.Target, err = parser.GetStringAnnotation(rewriteTargetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to empty", rewriteTargetAnnotation)
|
||||
}
|
||||
config.Target = ""
|
||||
}
|
||||
config.SSLRedirect, err = parser.GetBoolAnnotation(sslRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to '%s'", sslRedirectAnnotation, a.r.GetDefaultBackend().SSLRedirect)
|
||||
}
|
||||
config.SSLRedirect = a.r.GetDefaultBackend().SSLRedirect
|
||||
}
|
||||
config.PreserveTrailingSlash, err = parser.GetBoolAnnotation("preserve-trailing-slash", ing)
|
||||
config.PreserveTrailingSlash, err = parser.GetBoolAnnotation(preserveTrailingSlashAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to '%s'", preserveTrailingSlashAnnotation, a.r.GetDefaultBackend().PreserveTrailingSlash)
|
||||
}
|
||||
config.PreserveTrailingSlash = a.r.GetDefaultBackend().PreserveTrailingSlash
|
||||
}
|
||||
|
||||
config.ForceSSLRedirect, err = parser.GetBoolAnnotation("force-ssl-redirect", ing)
|
||||
config.ForceSSLRedirect, err = parser.GetBoolAnnotation(forceSSLRedirectAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to '%s'", forceSSLRedirectAnnotation, a.r.GetDefaultBackend().ForceSSLRedirect)
|
||||
}
|
||||
config.ForceSSLRedirect = a.r.GetDefaultBackend().ForceSSLRedirect
|
||||
}
|
||||
|
||||
config.UseRegex, _ = parser.GetBoolAnnotation("use-regex", ing)
|
||||
config.UseRegex, err = parser.GetBoolAnnotation(useRegexAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if errors.IsValidationError(err) {
|
||||
klog.Warningf("%sis invalid, defaulting to 'false'", useRegexAnnotation)
|
||||
}
|
||||
config.UseRegex = false
|
||||
}
|
||||
|
||||
config.AppRoot, err = parser.GetStringAnnotation("app-root", ing)
|
||||
config.AppRoot, err = parser.GetStringAnnotation(appRootAnnotation, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
if !errors.IsMissingAnnotations(err) && !errors.IsInvalidContent(err) {
|
||||
klog.Warningf("Annotation app-root contains an invalid value: %v", err)
|
||||
|
@ -126,3 +204,12 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (a rewrite) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a rewrite) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, rewriteAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -129,6 +129,30 @@ func TestSSLRedirect(t *testing.T) {
|
|||
t.Errorf("Expected true but returned false")
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/$1/abc/$2"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ = NewParser(mockBackend{redirect: true}).Parse(ing)
|
||||
redirect, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.Target != "/xpto/$1/abc/$2" {
|
||||
t.Errorf("Expected /xpto/$1/abc/$2 but returned %s", redirect.Target)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("rewrite-target")] = "/xpto/xas{445}"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
i, _ = NewParser(mockBackend{redirect: true}).Parse(ing)
|
||||
redirect, ok = i.(*Config)
|
||||
if !ok {
|
||||
t.Errorf("expected a Redirect type")
|
||||
}
|
||||
if redirect.Target != "" {
|
||||
t.Errorf("Expected empty rewrite target but returned %s", redirect.Target)
|
||||
}
|
||||
|
||||
data[parser.GetAnnotationWithPrefix("ssl-redirect")] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
|
|
|
@ -23,18 +23,40 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
satisfyAnnotation = "satisfy"
|
||||
)
|
||||
|
||||
var satisfyAnnotations = parser.Annotation{
|
||||
Group: "authentication",
|
||||
Annotations: parser.AnnotationFields{
|
||||
satisfyAnnotation: {
|
||||
Validator: parser.ValidateOptions([]string{"any", "all"}, true, true),
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `By default, a request would need to satisfy all authentication requirements in order to be allowed.
|
||||
By using this annotation, requests that satisfy either any or all authentication requirements are allowed, based on the configuration value.
|
||||
Valid options are "all" and "any"`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type satisfy struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new SATISFY annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return satisfy{r}
|
||||
return satisfy{
|
||||
r: r,
|
||||
annotationConfig: satisfyAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses annotation contained in the ingress
|
||||
func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
satisfy, err := parser.GetStringAnnotation("satisfy", ing)
|
||||
satisfy, err := parser.GetStringAnnotation(satisfyAnnotation, ing, s.annotationConfig.Annotations)
|
||||
|
||||
if err != nil || (satisfy != "any" && satisfy != "all") {
|
||||
satisfy = ""
|
||||
|
@ -42,3 +64,12 @@ func (s satisfy) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return satisfy, nil
|
||||
}
|
||||
|
||||
func (s satisfy) GetDocumentation() parser.AnnotationFields {
|
||||
return s.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a satisfy) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, satisfyAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -83,7 +83,7 @@ func TestSatisfyParser(t *testing.T) {
|
|||
annotations := map[string]string{}
|
||||
|
||||
for input, expected := range data {
|
||||
annotations[parser.GetAnnotationWithPrefix("satisfy")] = input
|
||||
annotations[parser.GetAnnotationWithPrefix(satisfyAnnotation)] = input
|
||||
ing.SetAnnotations(annotations)
|
||||
|
||||
satisfyt, err := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
|
@ -23,18 +23,47 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
serverSnippetAnnotation = "server-snippet"
|
||||
)
|
||||
|
||||
var serverSnippetAnnotations = parser.Annotation{
|
||||
Group: "snippets",
|
||||
Annotations: parser.AnnotationFields{
|
||||
serverSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation allows setting a custom NGINX configuration on a server block. This annotation does not contain any validation and it's usage is not recommended!`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type serverSnippet struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new server snippet annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return serverSnippet{r}
|
||||
return serverSnippet{
|
||||
r: r,
|
||||
annotationConfig: serverSnippetAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to indicate if the location/s contains a fragment of
|
||||
// configuration to be included inside the paths of the rules
|
||||
func (a serverSnippet) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("server-snippet", ing)
|
||||
return parser.GetStringAnnotation(serverSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a serverSnippet) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a serverSnippet) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, serverSnippetAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("server-snippet")
|
||||
annotation := parser.GetAnnotationWithPrefix(serverSnippetAnnotation)
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
|
|
|
@ -24,19 +24,39 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
serviceUpstreamAnnotation = "service-upstream"
|
||||
)
|
||||
|
||||
var serviceUpstreamAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
serviceUpstreamAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation makes NGINX use Service's Cluster IP and Port instead of Endpoints as the backend endpoints`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type serviceUpstream struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new serviceUpstream annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return serviceUpstream{r}
|
||||
return serviceUpstream{
|
||||
r: r,
|
||||
annotationConfig: serviceUpstreamAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
defBackend := s.r.GetDefaultBackend()
|
||||
|
||||
val, err := parser.GetBoolAnnotation("service-upstream", ing)
|
||||
val, err := parser.GetBoolAnnotation(serviceUpstreamAnnotation, ing, s.annotationConfig.Annotations)
|
||||
// A missing annotation is not a problem, just use the default
|
||||
if err == errors.ErrMissingAnnotations {
|
||||
return defBackend.ServiceUpstream, nil
|
||||
|
@ -44,3 +64,12 @@ func (s serviceUpstream) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
|
||||
return val, nil
|
||||
}
|
||||
|
||||
func (s serviceUpstream) GetDocumentation() parser.AnnotationFields {
|
||||
return s.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a serviceUpstream) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, serviceUpstreamAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -74,7 +74,7 @@ func TestIngressAnnotationServiceUpstreamEnabled(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("service-upstream")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -93,7 +93,7 @@ func TestIngressAnnotationServiceUpstreamSetFalse(t *testing.T) {
|
|||
|
||||
// Test with explicitly set to false
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("service-upstream")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
@ -155,7 +155,7 @@ func TestParseAnnotationsOverridesDefaultConfig(t *testing.T) {
|
|||
ing := buildIngress()
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("service-upstream")] = "false"
|
||||
data[parser.GetAnnotationWithPrefix(serviceUpstreamAnnotation)] = "false"
|
||||
ing.SetAnnotations(data)
|
||||
|
||||
val, _ := NewParser(mockBackend{}).Parse(ing)
|
||||
|
|
|
@ -65,6 +65,90 @@ const (
|
|||
annotationAffinityCookieChangeOnFailure = "session-cookie-change-on-failure"
|
||||
)
|
||||
|
||||
var sessionAffinityAnnotations = parser.Annotation{
|
||||
Group: "affinity",
|
||||
Annotations: parser.AnnotationFields{
|
||||
annotationAffinityType: {
|
||||
Validator: parser.ValidateOptions([]string{"cookie"}, true, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation enables and sets the affinity type in all Upstreams of an Ingress. This way, a request will always be directed to the same upstream server. The only affinity type available for NGINX is cookie`,
|
||||
},
|
||||
annotationAffinityMode: {
|
||||
Validator: parser.ValidateOptions([]string{"balanced", "persistent"}, true, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the stickiness of a session.
|
||||
Setting this to balanced (default) will redistribute some sessions if a deployment gets scaled up, therefore rebalancing the load on the servers.
|
||||
Setting this to persistent will not rebalance sessions to new servers, therefore providing maximum stickiness.`,
|
||||
},
|
||||
annotationAffinityCanaryBehavior: {
|
||||
Validator: parser.ValidateOptions([]string{"sticky", "legacy"}, true, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation defines the behavior of canaries when session affinity is enabled.
|
||||
Setting this to sticky (default) will ensure that users that were served by canaries, will continue to be served by canaries.
|
||||
Setting this to legacy will restore original canary behavior, when session affinity was ignored.`,
|
||||
},
|
||||
annotationAffinityCookieName: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation allows to specify the name of the cookie that will be used to route the requests`,
|
||||
},
|
||||
annotationAffinityCookieSecure: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation set the cookie as secure regardless the protocol of the incoming request`,
|
||||
},
|
||||
annotationAffinityCookieExpires: {
|
||||
Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation is a legacy version of "session-cookie-max-age" for compatibility with older browsers, generates an "Expires" cookie directive by adding the seconds to the current date`,
|
||||
},
|
||||
annotationAffinityCookieMaxAge: {
|
||||
Validator: parser.ValidateRegex(*affinityCookieExpiresRegex, false),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation sets the time until the cookie expires`,
|
||||
},
|
||||
annotationAffinityCookiePath: {
|
||||
Validator: parser.ValidateRegex(*parser.URLIsValidRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the Path that will be set on the cookie (required if your Ingress paths use regular expressions)`,
|
||||
},
|
||||
annotationAffinityCookieDomain: {
|
||||
Validator: parser.ValidateRegex(*parser.BasicCharsRegex, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskMedium,
|
||||
Documentation: `This annotation defines the Domain attribute of the sticky cookie.`,
|
||||
},
|
||||
annotationAffinityCookieSameSite: {
|
||||
Validator: parser.ValidateOptions([]string{"None", "Lax", "Strict"}, false, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation is used to apply a SameSite attribute to the sticky cookie.
|
||||
Browser accepted values are None, Lax, and Strict`,
|
||||
},
|
||||
annotationAffinityCookieConditionalSameSiteNone: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation is used to omit SameSite=None from browsers with SameSite attribute incompatibilities`,
|
||||
},
|
||||
annotationAffinityCookieChangeOnFailure: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `This annotation, when set to false will send request to upstream pointed by sticky cookie even if previous attempt failed.
|
||||
When set to true and previous attempt failed, sticky cookie will be changed to point to another upstream.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var (
|
||||
affinityCookieExpiresRegex = regexp.MustCompile(`(^0|-?[1-9]\d*$)`)
|
||||
)
|
||||
|
@ -109,50 +193,50 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
|
|||
|
||||
cookie := &Cookie{}
|
||||
|
||||
cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing)
|
||||
cookie.Name, err = parser.GetStringAnnotation(annotationAffinityCookieName, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieName, "default", defaultAffinityCookieName)
|
||||
cookie.Name = defaultAffinityCookieName
|
||||
}
|
||||
|
||||
cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing)
|
||||
cookie.Expires, err = parser.GetStringAnnotation(annotationAffinityCookieExpires, ing, a.annotationConfig.Annotations)
|
||||
if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.Expires) {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieExpires)
|
||||
cookie.Expires = ""
|
||||
}
|
||||
|
||||
cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing)
|
||||
cookie.MaxAge, err = parser.GetStringAnnotation(annotationAffinityCookieMaxAge, ing, a.annotationConfig.Annotations)
|
||||
if err != nil || !affinityCookieExpiresRegex.MatchString(cookie.MaxAge) {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieMaxAge)
|
||||
cookie.MaxAge = ""
|
||||
}
|
||||
|
||||
cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing)
|
||||
cookie.Path, err = parser.GetStringAnnotation(annotationAffinityCookiePath, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookiePath)
|
||||
}
|
||||
|
||||
cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing)
|
||||
cookie.Domain, err = parser.GetStringAnnotation(annotationAffinityCookieDomain, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieDomain)
|
||||
}
|
||||
|
||||
cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing)
|
||||
cookie.SameSite, err = parser.GetStringAnnotation(annotationAffinityCookieSameSite, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSameSite)
|
||||
}
|
||||
|
||||
cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing)
|
||||
cookie.Secure, err = parser.GetBoolAnnotation(annotationAffinityCookieSecure, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieSecure)
|
||||
}
|
||||
|
||||
cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing)
|
||||
cookie.ConditionalSameSiteNone, err = parser.GetBoolAnnotation(annotationAffinityCookieConditionalSameSiteNone, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieConditionalSameSiteNone)
|
||||
}
|
||||
|
||||
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing)
|
||||
cookie.ChangeOnFailure, err = parser.GetBoolAnnotation(annotationAffinityCookieChangeOnFailure, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
klog.V(3).InfoS("Invalid or no annotation value found. Ignoring", "ingress", klog.KObj(ing), "annotation", annotationAffinityCookieChangeOnFailure)
|
||||
}
|
||||
|
@ -162,11 +246,15 @@ func (a affinity) cookieAffinityParse(ing *networking.Ingress) *Cookie {
|
|||
|
||||
// NewParser creates a new Affinity annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return affinity{r}
|
||||
return affinity{
|
||||
r: r,
|
||||
annotationConfig: sessionAffinityAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
type affinity struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -174,18 +262,18 @@ type affinity struct {
|
|||
func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
cookie := &Cookie{}
|
||||
// Check the type of affinity that will be used
|
||||
at, err := parser.GetStringAnnotation(annotationAffinityType, ing)
|
||||
at, err := parser.GetStringAnnotation(annotationAffinityType, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
at = ""
|
||||
}
|
||||
|
||||
// Check the affinity mode that will be used
|
||||
am, err := parser.GetStringAnnotation(annotationAffinityMode, ing)
|
||||
am, err := parser.GetStringAnnotation(annotationAffinityMode, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
am = ""
|
||||
}
|
||||
|
||||
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing)
|
||||
cb, err := parser.GetStringAnnotation(annotationAffinityCanaryBehavior, ing, a.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
cb = ""
|
||||
}
|
||||
|
@ -205,3 +293,12 @@ func (a affinity) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
Cookie: *cookie,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a affinity) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a affinity) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, sessionAffinityAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -23,18 +23,47 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
configurationSnippetAnnotation = "configuration-snippet"
|
||||
)
|
||||
|
||||
var configurationSnippetAnnotations = parser.Annotation{
|
||||
Group: "snippets",
|
||||
Annotations: parser.AnnotationFields{
|
||||
configurationSnippetAnnotation: {
|
||||
Validator: parser.ValidateNull,
|
||||
Scope: parser.AnnotationScopeLocation,
|
||||
Risk: parser.AnnotationRiskCritical, // Critical, this annotation is not validated at all and allows arbitrary configutations
|
||||
Documentation: `This annotation allows setting a custom NGINX configuration on a location block. This annotation does not contain any validation and it's usage is not recommended!`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type snippet struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new CORS annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return snippet{r}
|
||||
return snippet{
|
||||
r: r,
|
||||
annotationConfig: configurationSnippetAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
// used to indicate if the location/s contains a fragment of
|
||||
// configuration to be included inside the paths of the rules
|
||||
func (a snippet) Parse(ing *networking.Ingress) (interface{}, error) {
|
||||
return parser.GetStringAnnotation("configuration-snippet", ing)
|
||||
return parser.GetStringAnnotation(configurationSnippetAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a snippet) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a snippet) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, configurationSnippetAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -27,7 +27,7 @@ import (
|
|||
)
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
annotation := parser.GetAnnotationWithPrefix("configuration-snippet")
|
||||
annotation := parser.GetAnnotationWithPrefix(configurationSnippetAnnotation)
|
||||
|
||||
ap := NewParser(&resolver.Mock{})
|
||||
if ap == nil {
|
||||
|
|
|
@ -17,14 +17,47 @@ limitations under the License.
|
|||
package sslcipher
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
networking "k8s.io/api/networking/v1"
|
||||
|
||||
"k8s.io/ingress-nginx/internal/ingress/annotations/parser"
|
||||
"k8s.io/ingress-nginx/internal/ingress/errors"
|
||||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
sslPreferServerCipherAnnotation = "ssl-prefer-server-ciphers"
|
||||
sslCipherAnnotation = "ssl-ciphers"
|
||||
)
|
||||
|
||||
var (
|
||||
// Should cover something like "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"
|
||||
regexValidSSLCipher = regexp.MustCompile(`^[A-Za-z0-9!:+\-]*$`)
|
||||
)
|
||||
|
||||
var sslCipherAnnotations = parser.Annotation{
|
||||
Group: "backend",
|
||||
Annotations: parser.AnnotationFields{
|
||||
sslPreferServerCipherAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `The following annotation will set the ssl_prefer_server_ciphers directive at the server level.
|
||||
This configuration specifies that server ciphers should be preferred over client ciphers when using the SSLv3 and TLS protocols.`,
|
||||
},
|
||||
sslCipherAnnotation: {
|
||||
Validator: parser.ValidateRegex(*regexValidSSLCipher, true),
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow,
|
||||
Documentation: `Using this annotation will set the ssl_ciphers directive at the server level. This configuration is active for all the paths in the host.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type sslCipher struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// Config contains the ssl-ciphers & ssl-prefer-server-ciphers configuration
|
||||
|
@ -35,7 +68,10 @@ type Config struct {
|
|||
|
||||
// NewParser creates a new sslCipher annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return sslCipher{r}
|
||||
return sslCipher{
|
||||
r: r,
|
||||
annotationConfig: sslCipherAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// Parse parses the annotations contained in the ingress rule
|
||||
|
@ -45,7 +81,7 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
var err error
|
||||
var sslPreferServerCiphers bool
|
||||
|
||||
sslPreferServerCiphers, err = parser.GetBoolAnnotation("ssl-prefer-server-ciphers", ing)
|
||||
sslPreferServerCiphers, err = parser.GetBoolAnnotation(sslPreferServerCipherAnnotation, ing, sc.annotationConfig.Annotations)
|
||||
if err != nil {
|
||||
config.SSLPreferServerCiphers = ""
|
||||
} else {
|
||||
|
@ -56,7 +92,19 @@ func (sc sslCipher) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
}
|
||||
}
|
||||
|
||||
config.SSLCiphers, _ = parser.GetStringAnnotation("ssl-ciphers", ing)
|
||||
config.SSLCiphers, err = parser.GetStringAnnotation(sslCipherAnnotation, ing, sc.annotationConfig.Annotations)
|
||||
if err != nil && !errors.IsInvalidContent(err) && !errors.IsMissingAnnotations(err) {
|
||||
return config, err
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func (sc sslCipher) GetDocumentation() parser.AnnotationFields {
|
||||
return sc.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a sslCipher) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, sslCipherAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -33,22 +33,24 @@ func TestParse(t *testing.T) {
|
|||
t.Fatalf("expected a parser.IngressAnnotation but returned nil")
|
||||
}
|
||||
|
||||
annotationSSLCiphers := parser.GetAnnotationWithPrefix("ssl-ciphers")
|
||||
annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix("ssl-prefer-server-ciphers")
|
||||
annotationSSLCiphers := parser.GetAnnotationWithPrefix(sslCipherAnnotation)
|
||||
annotationSSLPreferServerCiphers := parser.GetAnnotationWithPrefix(sslPreferServerCipherAnnotation)
|
||||
|
||||
testCases := []struct {
|
||||
annotations map[string]string
|
||||
expected Config
|
||||
expectErr bool
|
||||
}{
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", ""}, false},
|
||||
{map[string]string{annotationSSLCiphers: "ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256"},
|
||||
Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}},
|
||||
{map[string]string{annotationSSLCiphers: ""}, Config{"", ""}},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}},
|
||||
{map[string]string{}, Config{"", ""}},
|
||||
{nil, Config{"", ""}},
|
||||
Config{"ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256", ""}, false},
|
||||
{map[string]string{annotationSSLCiphers: ""}, Config{"", ""}, false},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "true"}, Config{"", "on"}, false},
|
||||
{map[string]string{annotationSSLPreferServerCiphers: "false"}, Config{"", "off"}, false},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", annotationSSLPreferServerCiphers: "true"}, Config{"ALL:!aNULL:!EXPORT56:RC4+RSA:+HIGH:+MEDIUM:+LOW:+SSLv2:+EXP", "on"}, false},
|
||||
{map[string]string{annotationSSLCiphers: "ALL:SOMETHING:;locationXPTO"}, Config{"", ""}, true},
|
||||
{map[string]string{}, Config{"", ""}, false},
|
||||
{nil, Config{"", ""}, false},
|
||||
}
|
||||
|
||||
ing := &networking.Ingress{
|
||||
|
@ -61,7 +63,10 @@ func TestParse(t *testing.T) {
|
|||
|
||||
for _, testCase := range testCases {
|
||||
ing.SetAnnotations(testCase.annotations)
|
||||
result, _ := ap.Parse(ing)
|
||||
result, err := ap.Parse(ing)
|
||||
if (err != nil) != testCase.expectErr {
|
||||
t.Fatalf("expected error: %t got error: %t err value: %s. %+v", testCase.expectErr, err != nil, err, testCase.annotations)
|
||||
}
|
||||
if !reflect.DeepEqual(result, &testCase.expected) {
|
||||
t.Errorf("expected %v but returned %v, annotations: %s", testCase.expected, result, testCase.annotations)
|
||||
}
|
||||
|
|
|
@ -24,13 +24,32 @@ import (
|
|||
"k8s.io/ingress-nginx/internal/ingress/resolver"
|
||||
)
|
||||
|
||||
const (
|
||||
sslPassthroughAnnotation = "ssl-passthrough"
|
||||
)
|
||||
|
||||
var sslPassthroughAnnotations = parser.Annotation{
|
||||
Group: "", // TBD
|
||||
Annotations: parser.AnnotationFields{
|
||||
sslPassthroughAnnotation: {
|
||||
Validator: parser.ValidateBool,
|
||||
Scope: parser.AnnotationScopeIngress,
|
||||
Risk: parser.AnnotationRiskLow, // Low, as it allows regexes but on a very limited set
|
||||
Documentation: `This annotation instructs the controller to send TLS connections directly to the backend instead of letting NGINX decrypt the communication.`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
type sslpt struct {
|
||||
r resolver.Resolver
|
||||
r resolver.Resolver
|
||||
annotationConfig parser.Annotation
|
||||
}
|
||||
|
||||
// NewParser creates a new SSL passthrough annotation parser
|
||||
func NewParser(r resolver.Resolver) parser.IngressAnnotation {
|
||||
return sslpt{r}
|
||||
return sslpt{r: r,
|
||||
annotationConfig: sslPassthroughAnnotations,
|
||||
}
|
||||
}
|
||||
|
||||
// ParseAnnotations parses the annotations contained in the ingress
|
||||
|
@ -40,5 +59,14 @@ func (a sslpt) Parse(ing *networking.Ingress) (interface{}, error) {
|
|||
return false, ing_errors.ErrMissingAnnotations
|
||||
}
|
||||
|
||||
return parser.GetBoolAnnotation("ssl-passthrough", ing)
|
||||
return parser.GetBoolAnnotation(sslPassthroughAnnotation, ing, a.annotationConfig.Annotations)
|
||||
}
|
||||
|
||||
func (a sslpt) GetDocumentation() parser.AnnotationFields {
|
||||
return a.annotationConfig.Annotations
|
||||
}
|
||||
|
||||
func (a sslpt) Validate(anns map[string]string) error {
|
||||
maxrisk := parser.StringRiskToRisk(a.r.GetSecurityConfiguration().AnnotationsRiskLevel)
|
||||
return parser.CheckAnnotationRisk(anns, maxrisk, sslPassthroughAnnotations.Annotations)
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ func TestParseAnnotations(t *testing.T) {
|
|||
}
|
||||
|
||||
data := map[string]string{}
|
||||
data[parser.GetAnnotationWithPrefix("ssl-passthrough")] = "true"
|
||||
data[parser.GetAnnotationWithPrefix(sslPassthroughAnnotation)] = "true"
|
||||
ing.SetAnnotations(data)
|
||||
// test ingress using the annotation without a TLS section
|
||||
_, err = NewParser(&resolver.Mock{}).Parse(ing)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue