ingress nginx: rewrite rule logic and location lookup

10/1/2019

I deployed in my k8s cluster a nginx ingress controller to reach a backend application.

When testing my ingress resource configuration, I noticed that if I add a rewrite rule, I also need to declare a path for the rewritten URI pointing to the same service.

For example, the following ingress config doesn't work:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: fieldprov-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /wetty
    nginx.ingress.kubernetes.io/enable-rewrite-log: "true"
    kubernetes.io/ingress.class: "field-management"
spec:
  tls:
  - hosts:
    - ccfanhe09.sce-lab.com
  rules:
    - host: ccfanhe09.sce-lab.com
      http:
        paths:
         - path: /provisioning
           backend:
            serviceName: fieldprov-app
            servicePort: 3000

The controller will redirect me to its default backend server associated to "/"

fd10::2:102 - [fd10::2:102] - - [01/Oct/2019:19:34:07 +0000] "GET /provisioning HTTP/2.0" 304 0 "-" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0" 236 0.003 [default-fieldprov-app-3000] [] [fd10::1:10a]:
3000 0 0.003 304 64895f25fd3fb9937f66ebbcee369c81
fd10::2:102 - [fd10::2:102] - - [01/Oct/2019:19:34:07 +0000] "GET /wetty/socket.io/socket.io.js HTTP/2.0" 404 159 "https://ccfanhe10.sce-lab.com:30000/provisioning" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firef
ox/60.0" 71 0.002 [upstream-default-backend] [] 127.0.0.1:8181 159 0.003 404 8310972dd6b39f294bae7550305bd7c2
fd10::2:102 - [fd10::2:102] - - [01/Oct/2019:19:34:07 +0000] "GET /wetty/wetty.min.js HTTP/2.0" 404 159 "https://ccfanhe10.sce-lab.com:30000/provisioning" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0" 2
4 0.001 [upstream-default-backend] [] 127.0.0.1:8181 159 0.002 404 47875d655cf4bcd02fc6212dc6142848
fd10::2:102 - [fd10::2:102] - - [01/Oct/2019:19:34:07 +0000] "GET /wetty/wetty.min.js HTTP/2.0" 404 159 "https://ccfanhe10.sce-lab.com:30000/provisioning" "Mozilla/5.0 (Windows NT 6.1; Win64; x64; rv:60.0) Gecko/20100101 Firefox/60.0" 2
4 0.001 [upstream-default-backend] [] 127.0.0.1:8181 159 0.000 404 08d1fd4cc72a5475d0fcfb4ca6a4cb2b``

So to me it's like the controller is doing a 2nd lookup after having applied the rewrite rule.

Below is my nginx.conf associated to the ingress config described above:

## start server ccfanhe10.sce-lab.com
    server {
        server_name ccfanhe10.sce-lab.com ;

        listen 80  ;
        listen [::]:80  ;
        listen 443  ssl http2 ;
        listen [::]:443  ssl http2 ;

        set $proxy_upstream_name "-";

        # PEM sha: 692b563bafa154b7d28350ef01e7c4d53ec2afd1
        ssl_certificate                         /etc/ingress-controller/ssl/default-fake-certificate.pem;
        ssl_certificate_key                     /etc/ingress-controller/ssl/default-fake-certificate.pem;

        ssl_certificate_by_lua_block {
            certificate.call()
        }

        location ~* "^/provisioning" {

            set $namespace      "default";
            set $ingress_name   "fieldprov-app";
            set $service_name   "fieldprov-app";
        set $service_port   "{0 3000 }";
            set $location_path  "/provisioning";

            rewrite_by_lua_block {
                lua_ingress.rewrite({
                    force_ssl_redirect = true,
                    use_port_in_redirects = false,
                })
                balancer.rewrite()
                plugins.run()
            }

            header_filter_by_lua_block {

                plugins.run()
            }
            body_filter_by_lua_block {

            }

            log_by_lua_block {

                balancer.log()

                monitor.call()

                plugins.run()
            }

            if ($scheme = https) {
                more_set_headers                        "Strict-Transport-Security: max-age=15724800; includeSubDomains";
            }

            rewrite_log on;

            port_in_redirect off;

            set $balancer_ewma_score -1;
            set $proxy_upstream_name    "default-fieldprov-app-3000";
            set $proxy_host             $proxy_upstream_name;
            set $pass_access_scheme $scheme;
            set $pass_server_port $server_port;
            set $best_http_host $http_host;
            set $pass_port $pass_server_port;

            set $proxy_alternative_upstream_name "";

            client_max_body_size                    1m;

            proxy_set_header Host                   $best_http_host;

            # Pass the extracted client certificate to the backend

            # Allow websocket connections
            proxy_set_header                        Upgrade           $http_upgrade;

            proxy_set_header                        Connection        $connection_upgrade;

            proxy_set_header X-Request-ID           $req_id;
            proxy_set_header X-Real-IP              $the_real_ip;

            proxy_set_header X-Forwarded-For        $the_real_ip;

            proxy_set_header X-Forwarded-Host       $best_http_host;
            proxy_set_header X-Forwarded-Port       $pass_port;
            proxy_set_header X-Forwarded-Proto      $pass_access_scheme;

            proxy_set_header X-Original-URI         $request_uri;

            proxy_set_header X-Scheme               $pass_access_scheme;

            # Pass the original X-Forwarded-For
            proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;

            # mitigate HTTPoxy Vulnerability
            # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
            proxy_set_header Proxy                  "";

            # Custom headers to proxied server

            proxy_connect_timeout                   5s;
            proxy_send_timeout                      60s;
            proxy_read_timeout                      60s;

            proxy_buffering                         off;
            proxy_buffer_size                       4k;
            proxy_buffers                           4 4k;
            proxy_request_buffering                 on;
            proxy_http_version                      1.1;

            proxy_cookie_domain                     off;
            proxy_cookie_path                       off;

            # In case of errors try the next upstream server before returning an error
            proxy_next_upstream                     error timeout;
            proxy_next_upstream_timeout             0;
            proxy_next_upstream_tries               3;

            rewrite "(?i)/provisioning" /wetty break;
            proxy_pass http://upstream_balancer;

            proxy_redirect                          off;

        }

        location ~* "^/" {

            set $namespace      "";
            set $ingress_name   "";
            set $service_name   "";
        set $service_port   "{0 0 }";
            set $location_path  "/";

            rewrite_by_lua_block {
                lua_ingress.rewrite({
                    force_ssl_redirect = true,
                    use_port_in_redirects = false,
                })
                balancer.rewrite()
                plugins.run()
            }

            header_filter_by_lua_block {

                plugins.run()
            }
            body_filter_by_lua_block {

            }

            log_by_lua_block {

                balancer.log()

                monitor.call()

                plugins.run()
            }

            if ($scheme = https) {
                more_set_headers                        "Strict-Transport-Security: max-age=15724800; includeSubDomains";
            }

            rewrite_log on;

            port_in_redirect off;

            set $balancer_ewma_score -1;
            set $proxy_upstream_name    "upstream-default-backend";
            set $proxy_host             $proxy_upstream_name;
            set $pass_access_scheme $scheme;
            set $pass_server_port $server_port;
            set $best_http_host $http_host;
            set $pass_port $pass_server_port;

            set $proxy_alternative_upstream_name "";

            client_max_body_size                    1m;

            proxy_set_header Host                   $best_http_host;

            # Pass the extracted client certificate to the backend

            # Allow websocket connections
            proxy_set_header                        Upgrade           $http_upgrade;

            proxy_set_header                        Connection        $connection_upgrade;

            proxy_set_header X-Request-ID           $req_id;
            proxy_set_header X-Real-IP              $the_real_ip;

            proxy_set_header X-Forwarded-For        $the_real_ip;

            proxy_set_header X-Forwarded-Host       $best_http_host;
            proxy_set_header X-Forwarded-Port       $pass_port;
            proxy_set_header X-Forwarded-Proto      $pass_access_scheme;

            proxy_set_header X-Original-URI         $request_uri;

            proxy_set_header X-Scheme               $pass_access_scheme;

            # Pass the original X-Forwarded-For
            proxy_set_header X-Original-Forwarded-For $http_x_forwarded_for;

            # mitigate HTTPoxy Vulnerability
            # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
            proxy_set_header Proxy                  "";

            # Custom headers to proxied server

            proxy_connect_timeout                   5s;
            proxy_send_timeout                      60s;
            proxy_read_timeout                      60s;

            proxy_buffering                         off;
            proxy_buffer_size                       4k;
            proxy_buffers                           4 4k;
            proxy_request_buffering                 on;
            proxy_http_version                      1.1;

            proxy_cookie_domain                     off;
            proxy_cookie_path                       off;

            # In case of errors try the next upstream server before returning an error
            proxy_next_upstream                     error timeout;
            proxy_next_upstream_timeout             0;
            proxy_next_upstream_tries               3;

            rewrite "(?i)/" /wetty break;
            proxy_pass http://upstream_balancer;

            proxy_redirect                          off;

        }

    }
    ## end server ccfanhe10.sce-lab.com

I don't understand the rewrite rule under "^ /" location. Also I thought that adding the break keyword to the rewrite rule would avoid any extra URI lookup but this is not the behavior I'm seeing. If I don't create a location for "/wetty" which points to the same service as "/provisioning", it doesn't work.

I'm looking for some explanation about the expected behavior in such condition.

Thanks for your support !!

-- laaubert
kubernetes-ingress
nginx-ingress

1 Answer

10/7/2019

Try to use capture groups as described in here or here section of ingres-nginx documentation.

Starting in Version 0.22.0, ingress definitions using the annotation nginx.ingress.kubernetes.io/rewrite-target are not backwards compatible with previous versions. In Version 0.22.0 and beyond, any substrings within the request URI that need to be passed to the rewritten path must explicitly be defined in a capture group.

Captured groups are saved in numbered placeholders, chronologically, in the form $1, $2 ... $n. These placeholders can be used as parameters in the rewrite-target annotation.

So Your ingress config should looks something like this:

apiVersion: networking.k8s.io/v1beta1
kind: Ingress
metadata:
  name: fieldprov-app
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /wetty/$2
    nginx.ingress.kubernetes.io/enable-rewrite-log: "true"
    kubernetes.io/ingress.class: "field-management"
spec:
  tls:
  - hosts:
    - ccfanhe09.sce-lab.com
  rules:
    - host: ccfanhe09.sce-lab.com
      http:
        paths:
         - path: /provisioning/(/|$)(.*)
           backend:
            serviceName: fieldprov-app
            servicePort: 3000

As for the nginx.conf it looks like that because its settings are injected from nginx-controller using lua-nginx-module, You can read about it here.


Update:

If the rewrite rule is working correctly there shouldn't be any extra lookups. I was not able to find anything like that in nginx ingress annotations.

You can check if rewrite rule is working by using curl -I -k <URI> command:

$ curl -I -k http://approot.bar.com/
HTTP/1.1 302 Moved Temporarily
Server: nginx/1.11.10
Date: Mon, 13 Mar 2017 14:57:15 GMT
Content-Type: text/html
Content-Length: 162
Location: http://stickyingress.example.com/app1
Connection: keep-alive
-- Piotr Malec
Source: StackOverflow