NGINX Integration

NGINX is an HTTP and reverse proxy server which became famous as one of the fastest web servers out there, heimdall can be integrated with by making use of the ngx_http_auth_request_module. In such setup, NGINX delegates authentication and authorization to heimdall. If heimdall answers with a 2XX code, NGINX grants access and forwards the original request to the upstream service. If heimdall returns 401 or 403, the access is denied with the corresponding error code. Any other response code returned by heimdall is considered an error.

If there is no matching rule on heimdall side, heimdall responds with 404 Not Found, which, as said above will be treated by NGINX as error. To avoid such situations, you can define a default rule, which is anyway recommended to have secure defaults, which, depending on the configuration, would then respond either with 401 or 403, or still 405 if you do not allow a specific HTTP method.

Since NGINX is highly configurable and heimdall supports different integration options, you can use any of the configuration examples given below. All of these enable heimdall to build the URL of the protected backend server for rule matching purposes.

In most cases you must configure heimdall to trust your NGINX instances by setting trusted_proxies for the Decision, respectively Proxy service. Exceptions are described in the sections below.

Forward only the host information in the header

With this method you set the X-Forwarded-Host to let heimdall know the host, respectively domain the request was sent to. All other URL parts (schema, path and query parameter) as well as the HTTP method are then inferred from the URL and the request heimdall receives.

Example 1. Possible Configuration
# nginx.conf
...

location / {
  auth_request            /_auth;  (1)
  auth_request_set        $auth_cookie $upstream_http_set_cookie;  (2)
  add_header              Set-Cookie $auth_cookie;
  auth_request_set        $authHeader0 $upstream_http_authorization;  (3)
  proxy_set_header        'Authorization' $authHeader0;
  # mitigate HTTPoxy Vulnerability
  # https://www.nginx.com/blog/mitigating-the-httpoxy-vulnerability-with-nginx/
  proxy_set_header Proxy  "";
  ...
}

location = /_auth {  (4)
  internal;
  proxy_method             $request_method; (5)
  proxy_pass               http://heimdall:4456$request_uri; (6)
  proxy_pass_request_body  off;  (7)
  proxy_set_header         Content-Length   "";
  proxy_set_header         X-Forwarded-Host $http_host;  (8)
  proxy_set_header         X-Forwarded-For  $remote_addr;  (9)
}
1Configures NGINX to forward every request to the internal /_auth endpoint (this is where the actual heimdall integration happens - see below).
2When the response from heimdall returns, this and the next line set the Cookies set by heimdall in the response (whether you need this depends on your Contextualizers and Unifiers configuration)
3When the response from heimdall returns, this and the next line set the Authorization header set by heimdall in the response (which header to set depends again on your Contextualizers and Unifiers configuration)
4This is where the "magic" happens
5Configure NGINX to use the HTTP method used by its client. Without this setting the implementation of proxy_path will use the HTTP GET method.
6Configures NGINX to pass the request to heimdall and sets the request path and queries from the original request
7Disables sending of the request body. If your heimdall rules make use of the body, you should set this to on and remove the next line.
8This is where you forward the host information to heimdall
9Not really required, but makes the remote address available to heimdall and thus to the mechanisms used in by the rules. Requires trusted_proxies to be configured.
Instead of using X-Forwarded-Host you could also make use of the Host header. In that case, there is no need to configure the trusted_proxies. The X-Forwarded-For will then however be ignored as considered not trusted.

Forward all information in X-Forwarded-* headers

With this method you set the X-Forwarded-Method, X-Forwarded-Proto, X-Forwarded-Host and X-Forwarded-Path to let heimdall know the host, respectively domain url and the used HTTP method.

Compared to the previous integration option, the configuration only differs in the definition of the internal /_auth endpoint. So, the example configuration is limited to that part only.

Proper configuration of trusted_proxies is mandatory if using this option.
Example 2. Possible Configuration
# nginx.conf
...

location = /_auth {
  internal;
  proxy_pass               http://heimdall:4456;  (1)
  proxy_pass_request_body  off;
  proxy_set_header         Content-Length         "";
  proxy_set_header         X-Forwarded-Method     $request_method;  (2)
  proxy_set_header         X-Forwarded-Proto      $scheme;  (3)
  proxy_set_header         X-Forwarded-Host       $http_host;  (4)
  proxy_set_header         X-Forwarded-Path       $request_uri;  (5)
  proxy_set_header         X-Forwarded-For        $remote_addr;
}
1Configures NGINX to pass the request to heimdall.
2Let NGINX forward the used HTTP method to heimdall.
3Let NGINX forward the used HTTP schema to heimdall.
4Let NGINX forward the used host to heimdall.
5Let NGINX forward the used path and query parameter to heimdall.

Forward all information in X-Forwarded-Uri and X-Forwarded-Method headers

This method is a simplified alternative to the previous one in which heimdall receives everything required to know the host url and the HTTP method in HTTP headers.

Proper configuration of trusted_proxies is mandatory if using this option.

The difference is again in the definition of the internal /_auth endpoint. So, the example configuration is limited to that part.

Example 3. Possible Configuration
# nginx.conf
...

location = /_auth {
  internal;
  proxy_pass               http://heimdall:4456;  (1)
  proxy_pass_request_body  off;
  proxy_set_header         Content-Length         "";
  proxy_set_header         X-Forwarded-Method     $request_method;  (2)
  proxy_set_header         X-Forwarded-Uri        $scheme://$http_host$request_uri;  (3)
  proxy_set_header         X-Forwarded-For        $remote_addr;
}
1Configures NGINX to pass the request to heimdall.
2Let NGINX forward the used HTTP method to heimdall.
3Let NGINX forward the entire used HTTP URL to heimdall.

Integration with NGINX Ingress Controller.

The integration option, described in the Forward all information in X-Forwarded-* headers section corresponds more or less to the way how the ngnix.conf file is generated by the default nginx-ingress template used by the NGINX Ingress Controller. Instead of making use of X-Forwarded-Method, the nginx ingress controller sets the X-Original-Method header which is also supported by heimdall. The only missing parts are the request path and the query parameter. So you can integrate heimdall by adding the annotations as shown in the example below to your ingress configuration.

The configuration used in the example below requires proper configuration of trusted_proxies. Otherwise heimdall will use the HTTP method, used by the nginx ingress controller while communicating with heimdall (which is HTTP GET) instead of the value sent in the X-Original-Method.
Example 4. Possible Configuration
nginx.ingress.kubernetes.io/auth-url: "http://<heimdall service name>.<namespace>.svc.cluster.local:<decision port>/$request_uri" (1)
nginx.ingress.kubernetes.io/auth-response-headers: Authorization (2)
    # other annotations required
1Configures the controller to pass the request path and query parameters to heimdall’s decision service endpoint with <heimdall service name>, <namespace> and <decision port> depending on your configuration.
2Let NGINX forward the Authorization header set by heimdall to the upstream service. This configuration depends on your Contextualizers and Unifiers configuration

The $request_uri nginx variable does already contain a slash at the beginning if a path part is present in the request URL. For that reason, the proper configuration for the nginx.ingress.kubernetes.io/auth-url would rather look like http://<heimdall service name>.<namespace>.svc.cluster.local:<decision port>$request_uri (no slash before $request_uri). Nevertheless, the example above makes use of that variable by adding yet another slash in front of it. This is required due to a bug in the nginx ingress controller implementation, which fails to parse an nginx template of the form http://heimdall:4456$request_uri (no slash after the port part), resulting in the following error

Location denied. Reason: "could not parse auth-url annotation: http://some-service.namespace.svc.cluster.local:4456$request_uri is not a valid URL: parse \"http://some-service.namespace.svc.cluster.local:4456$request_uri\": invalid port \":4456$request_uri\" after host"

With that additional slash however, all requests to heimdall will have a duplicate slash (e.g. //test) in the URL path part if the path part is present. If the path part is absent, that is, the value of the $request_uri is empty, there is still one slash, so that e.g. a request to https://my-domain:80 will result in e.g. the following url for communication with heimdall: https://heimdall:4456/

Heimdall has an automatic workaround for that: if the call is done by the nginx ingress controller and there is // as suffix in the path, the first slash is removed. There is however no possibility to fix that for requests without the path part (see above). If this is an issue in your context, consider using the integration option described below.

Alternatively, you can also use the nginx.ingress.kubernetes.io/configuration-snippet and nginx.ingress.kubernetes.io/server-snippet annotations and provide configuration as described above for the vanilla nginx. There will be no issues with slashes then at all.

Example 5. Possible Configuration

This example makes use of the very first integration option described above.

nginx.ingress.kubernetes.io/configuration-snippet: |
  auth_request               /_auth; (1)
  auth_request_set           $authHeader0 $upstream_http_authorization;  (2)
  proxy_set_header           'Authorization' $authHeader0;
nginx.ingress.kubernetes.io/server-snippet: |
  location = /_auth { (3)
    internal;
    proxy_method             $request_method; (4)
    proxy_pass               http://<heimdall service name>.<namespace>.svc.cluster.local:<decision port>$request_uri; (5)
    proxy_pass_request_body  off; (6)
    proxy_set_header         Content-Length   "";
    proxy_set_header         Host $http_host; (7)
  }
# other annotations required
1Configures NGINX ingress controller to make use of an external auth service and pass incoming request to /_auth route.
2Let NGINX forward the Authorization header set by heimdall to the upstream service. This configuration depends on your Contextualizers and Unifiers configuration
3The implementation of the _auth route.
4Configures NGINX to use the HTTP method used by its client. Without this setting the implementation of proxy_path will use the HTTP GET method.
5Forwards the request to heimdall and sets the request path and queries from the original request (no slash this time)
6Disables sending of the request body. If your heimdall rules make use of the body, you should set this to on and remove the next line.
7This is where you forward the host information to heimdall
If you rely on the domain, the request came from in your rules, you should rather make use of the X-Forwarded-Host header in lieu of the Host header and configure the trusted_proxies property.

Checkout the Kubernetes quickstarts on GitHub for a working demo.

Last updated on Jun 24, 2023