Search code examples
kubernetesingress-nginx

kubernetes ingress-nginx ignore special characters in path


I'm trying to have a rule listening to a specific path containing a dollar sign like this:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: metadata-ingress
  annotations:
    nginx.ingress.kubernetes.io/enable-cors: "true"
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
    nginx.ingress.kubernetes.io/use-regex: "false"
spec:
  ingressClassName: public
  tls:
  - hosts:
    - mydomain.com
  rules:
  - host: mydomain.com
    http:
      paths:
      - path: /api/v2/$metadata
        pathType: Prefix
        backend:
          service:
            name: busybox
            port:
              number: 8280

I don't want any url rewrite or anything fancy, just want this specific path to be caught and forwarded to this service.

Without the "$" it works.

I thought disabling regex with use-regex: "false" would fix it, but no.

I also tried using the url encoded value for $ : %24metadata but it doesn't help either.

I also tried to use "exact" instead of "prefix" as the pathType but no.


Solution

  • I can't reproduce your problem, but I thought I walk through my test setup and you can tell me if anything is different. For the purpose of testing different paths, I have two deployments using the traefik/whoami image (this just provides a useful endpoint that shows us -- among other things -- the hostname and path involved in the request).

    That looks like:

    apiVersion: v1
    kind: Service
    metadata:
      labels:
        app: example
        component: app1
      name: example-app1
    spec:
      ports:
      - name: http
        port: 80
        targetPort: http
      selector:
        app: example
        component: app1
    
    ---
    apiVersion: v1
    kind: Service
    metadata:
      labels:
        app: example
        component: app2
      name: example-app2
    spec:
      ports:
      - name: http
        port: 80
        targetPort: http
      selector:
        app: example
        component: app2
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: example
        component: app1
      name: example-app1
    spec:
      selector:
        matchLabels:
          app: example
          component: app1
      template:
        metadata:
          labels:
            app: example
            component: app1
        spec:
          containers:
          - image: docker.io/traefik/whoami:latest
            name: whoami
            ports:
            - containerPort: 80
              name: http
    
    ---
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      labels:
        app: example
        component: app2
      name: example-app2
    spec:
      selector:
        matchLabels:
          app: example
          component: app2
      template:
        metadata:
          labels:
            app: example
            component: app2
        spec:
          containers:
          - image: docker.io/traefik/whoami:latest
            name: whoami
            ports:
            - containerPort: 80
              name: http
    
    

    I've also deployed the following Ingress resource, which looks mostly like yours, except I've added a second paths config so that we can compare requests that match /api/v2/$metadata vs those that do not:

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      annotations:
        cert-manager.io/cluster-issuer: house
        nginx.ingress.kubernetes.io/enable-cors: "true"
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
      name: example
    spec:
      ingressClassName: nginx
      rules:
      - host: example.apps.infra.house
        http:
          paths:
          - backend:
              service:
                name: example-app1
                port:
                  name: http
            path: /
            pathType: Prefix
          - backend:
              service:
                name: example-app2
                port:
                  name: http
            path: /api/v2/$metadata
            pathType: Prefix
      tls:
      - hosts:
        - example.apps.infra.house
        secretName: example-cert
    

    With these resources in place, a request to https://example.apps.infra.house/ goes to app1:

    $ curl -s https://example.apps.infra.house/ | grep Hostname
    Hostname: example-app1-596fcf48bd-dqhvc
    

    Whereas a request to https://example.apps.infra.house/api/v2/$metadata goes to app2:

    $ curl -s https://example.apps.infra.house/api/v2/\$metadata | grep Hostname
    Hostname: example-app2-8675dc9b45-6hg7l
    

    So that all seems to work.


    We can, if we are so inclined, examine the nginx configuration that results from that Ingress. On my system, the nginx ingress controller runs in the nginx-ingress namespace:

    $ kubectl -n nginx-ingress get deploy
    NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
    ingress-nginx-controller   1/1     1            1           8d
    

    The configuration lives in /etc/nginx/nginx.conf in the container. We can cat the file to stdout and look for the relevant directives:

    $ kubectl -n nginx-ingress exec deploy/ingress-nginx-controller cat /etc/nginx/nginx.conf
    ...
                    location /api/v2/$metadata/ {
                    ...
                    }
    ...
    

    Based on your comment, the following seems to work:

    apiVersion: networking.k8s.io/v1
    kind: Ingress
    metadata:
      name: example
      annotations:
        nginx.ingress.kubernetes.io/ssl-redirect: "true"
        nginx.ingress.kubernetes.io/rewrite-target: "/$2"
        cert-manager.io/cluster-issuer: house
    spec:
      ingressClassName: nginx
      tls:
        - hosts:
          - example.apps.infra.house
          secretName: example-cert
      rules:
      - host: example.apps.infra.house
        http:
          paths:
          - path: /app1(/|$)(.*)
            pathType: Prefix
            backend:
              service:
                name: example-app1
                port:
                  name: http
    
          # Note the use of single quotes (') here; this is
          # important; using double quotes we would need to
          # write `\\$` instead of `\$`.
          - path: '/api/v2/\$metadata'
            pathType: Prefix
            backend:
              service:
                name: example-app2
                port:
                  name: http
    

    The resulting location directives look like:

    location ~* "^/api/v2/\$metadata" {
    ...
    }
    location ~* "^/app1(/|$)(.*)" {
    ...
    }
    

    And a request for the $metadata path succeeds.