NGINX Lua Modules

Without additional backend development, HUMAN Security's Application Integrity product can integrated into an NGINX reverse proxy using the lua-nginx-module.

Prerequisites

  • NGINX that has been installed with the LuaJIT. We recommend OpenResty's LuaJIT2, but there may be better alternatives depending on your operating system.
  • LuaRocks - the lua package manager
  • A backend service protected by NGINX

Getting Started

Your account manager can provide a software package which includes the following files:

  • README.md - contains this information and a link to this documentation
  • nginx.example - an example nginx config file for use with envsubst (see documentation)
  • lua-plugins/injector.lua - injects a script into the <head> of an HTML document
  • lua-plugins/mitigation.lua - requests an ACTION from the mitigation API
  • lua-plugins/mitigation/ - contains the plugin modules
  • lua-plugins/tests/ - contains the unit tests

Configuration

Although most of the plugin variables can be easily configured in NGINX server blocks, individual NGINX http blocks may require some specific configuration.

  1. The release contains the required Application Integrity Lua modules. Once the Lua modules are copied on to the file system, NGINX must be configured with their location. The below NGINX configuration example snippet, which should be located within an http{} block, indicates that the Lua modules are located at /etc/lua-plugins/.

    lua_package_path "/etc/lua-plugins/?.lua;;";
    more_clear_headers Server;
    server_tokens off;
    
  • Place this in the http{} block of your NGINX configuration
  • The lua_package_path should be set to the directory in which themitigation/ directory, mentioned above resides. In our case the lua-plugins directory was decompressed into the /etc/ directory, and as the mitigation/ directory resides in there, this is where we point Nginx to
  • The following two lines

    more_clear_headers Server;
    server_tokens off;
    

    are optional, but will tell NGINX to not set the Server header, giving away the configuration. Note however, this would require NGINX to be compiled with the more_clear_headers flag set. We don't recommend exposing information about the NGINX server so this might be something to consider.

  • DNS resolution. Lua needs to be given explicit capability by NGINX to be able to do DNS resolution. We recommend setting this within the server block of the application you wish to protect. You can use whichever resolver you wish, in this case we are using Google's DNS server however any that is publically available will work.

    resolver 8.8.8.8;
    
  1. Server block configuration

    Configuration Required Type Default Example Description
    $block_redirect_status_code true string "302" "302" The HTTP code that you would like to be sent with the redirect. If this is not set, the code will be 307 Moved Temporarily
    $block_redirect_url false string / /error This is the url that any blocked request will be redirected to. If it's not set, then the Mitigation API lua plugin will redirect to the root of the domain.
    $block_spa_response_body false string '{"error":"bad request"}' '{"error":"you have entered an incorrect password"}' Specifies the default body to respond with on blocking an SPA request
    $block_spa_response_code false string "400" "200" Specifies the http response code that will accompany the SPA response payload
    $custom_fields true string NONE '{"some_field": "some_value"}' Custom fields sent to the Mitigation API.
    $detection_tag_ci false string NONE CUSTOMER_ID Human security will have provided you with a customer ID. Although this is not a secret, we recommend setting it as an environment variable, however it is fine to hard code this value
    $detection_tag_dt false integer NONE DETECTIONTAGID You will have been provided with a TAG ID. You can set this within the nginx.conf here
    $detection_tag_host false string NONE sub.example.com This is a host that either is a CNAME on your organisations domain that points to Human Security's Mitigation engine, or a domain that Human Security has provided you with. In either case, the actual domain should be obfuscated
    $detection_tag_mo false string "2" "2" This is the tag mode. This should be always "2" for active interception
    $detection_tag_path false string NONE /ag/CUSTOMER_ID/clear.js The path that the tag will live at. This is another obfuscated path that will either be configured on a CNAME at your domain, or will be provided to you by Human Security
    $detection_tag_si false integer NONE SITE_ID An identifier set by the customer (you) to identify the site internally
    $detection_tag_spa false string "0" "0" Specifies if the integration is within a single page application or not
    $mitigation_api_et true string NONE 1 A value representing the type of interaction / transaction to be protected.
    $mitigation_api_key false string NONE API_KEY Human Security will have provided you with an API Key. This value should be set as an environment variable and not hardcoded
    $mitigation_api_policy_name false string NONE allowforlogin A policy that the customer has setup in the mitigation API.
  2. Remote Address

    Depending on how your environment is setup, it is useful to add the following to your server blocks so that NGINX will set the headers correctly. The correct values of client's IPs are required as part of requests to the Mitigation API. In the case the client is proxied, this will expose their real IP address as opposed to the IP address of the proxy

    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $remote_addr;
    
  3. Injecting the script tag in HTML responses

    The modules will attempt to inject the script tag into any HTML response. Add the following to your server block. This resets the content header so that NGINX will recalculate it, and defines the script that will do the injection.

    header_filter_by_lua_block {
        ngx.header.content_length = nil;
    }
    body_filter_by_lua_file /etc/lua-plugins/injector.lua;
    
  4. Intercepting the request and forwarding data to the Mitigation API

    This next block tells to NGINX to pass any request to the mitigation API. The plugin itself will decide whether it should act on the request based on the method. Currently, only POST, PUT and PATCH request methods are supported.

    lua_need_request_body on;
    access_by_lua_file /etc/lua-plugins/mitigation.lua;
    

    The following table lists the expected response of the plugins using default configuration settings.

    Situation Default Code Default Response Default Effect Headers Set
    action = block 307 Redirects to headers["Referer"] or / depending on whether or not headers["Referer"] is set Client is redirected to root path X-MITIGATION-RESULT
    action = allow N/A Let request through Client continues to backend route X-MITIGATION-RESULT
    Error during processing N/A Let request through Client continues to backend route. Customer to decide what to do X-MITIGATION-ERROR
    Unexpected Status Code N/A Let request through Client continues to backend route. Customer to decide what to do. Header informs client backend of the received status code (expected status code is 200) X-MITIGATION-STATUS

Using environment variables in Nginx configurations

It can be useful to set the parameters based on environment variables. In the following example the parameters are set by templating the values from environment variables

# required variables
set $mitigation_api_key "$MITIGATION_API_KEY";
set $detection_tag_ci "$DETECTION_TAG_CI";
set $detection_tag_dt "$DETECTION_TAG_DT";
set $mitigation_api_et "1";
set $detection_tag_si "12345";
set $detection_tag_host "$DETECTION_TAG_HOST";
set $detection_tag_path "$DETECTION_TAG_PATH";
set $detection_tag_spa "0";
set $detection_tag_mo "2";

# optional variables
set $block_redirect_url "$BLOCK_REDIRECT_URL";
set $block_redirect_status_code "$BLOCK_REDIRECT_STATUS_CODE";
set $custom_fields "$CUSTOM_FIELDS";

The right hand side values get replaced with environment variables using envsubst. Below is a very basic example of using envsubst to replace two specific variables in a conf file and output a new conf file

shell envsubst '$DETECTION_TAG_CI $DETECTION_TAG_DT' < nginx.conf.template > nginx.conf

Examples

All examples and conf files will need

set $mitigation_api_key "API_KEY"; # the api key provided to you by your account manager
set $mitigation_api_et "1"; # the event type
set $detection_tag_ci "CUSTOMER_ID"; # your customer ID
set $detection_tag_dt "DETECTION_TAG_ID"; # your tag ID
set $detection_tag_si "SITE_ID"; # a site identifier, specified by the customer

Catch All

The following is the most basic example. It will send all non-GET requests to the mitigation api and inject the script tag on all responses that contain an </head> and/or <body> tag. This assumes that you have unzipped the release to /etc/lua-plugins

worker_processes auto;
pcre_jit on;

events {
    worker_connections 1024;
}

http {

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    include mime.types;
    default_type application/octet-stream;
    gzip on;

    access_log /dev/stdout;

    lua_package_path "/etc/lua-plugins/?.lua;;";
    more_clear_headers Server;
    server_tokens off;

    server {
        listen 3000;
        server_name some.example.com localhost;
        resolver 8.8.8.8;
        client_header_buffer_size 8k;
        large_client_header_buffers 8 64k;
        error_log /dev/stdout debug;

        location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2|woff|ttf)$ {
            root /usr/share/nginx/html;
            index index.html index.htm;
        }

        location ^~ / {
            default_type text/html;

            # required variables
            set $mitigation_api_key "API_KEY";
            set $detection_tag_ci "CUSTOMER_ID";
            set $detection_tag_dt "DETECTION_TAG_ID";
            set $mitigation_api_et "1";
            set $detection_tag_si "SITE_ID";
            set $detection_tag_host "sub.example.com";
            set $detection_tag_path "/ag/CUSTOMER_ID/clear.js";
            set $detection_tag_spa "0";
            set $detection_tag_mo "2";

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;

            header_filter_by_lua_block {
                ngx.header.content_length = nil;
            }
            body_filter_by_lua_file /etc/lua-plugins/injector.lua;
            lua_need_request_body on;
            access_by_lua_file /etc/lua-plugins/mitigation.lua;

            proxy_pass http://localhost:$BACKEND_PORT;
        }
        error_page 500 502 503 504 /50x.html;
        location = /50x.html {
            root html;
        }
    }
}

Route Management

The following example is very similar to above, however it defines a couple of major differences. These differences are listed here and commented within the example for easier reading.

  1. Variables that are shared between endpoints are part of the server, and not location block
  2. An NGINX location block to handle signup attempts that will be redirected to if a signup is blocked by the mitigation API
  3. The /signup route is a vanilla HTML/CSS website and redirects a blocked userto a /catch endpoint, however it informs the client that the redirect is a 200. This code is configured to deceive the client rather than inform them.
  4. Different routes to define different configuration for /login vs /signup
  5. The /login route is an SPA and defines a response code and body to respond with when a request is blocked in the case of an SPA
worker_processes auto;
pcre_jit on;

events {
    worker_connections 1024;
}

http {

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;
    keepalive_timeout 65;
    types_hash_max_size 2048;
    include mime.types;
    default_type application/octet-stream;
    gzip on;

    access_log /dev/stdout;

    lua_package_path "/etc/lua-plugins/?.lua;;";
    more_clear_headers Server;
    server_tokens off;

    server {
        listen 3000;
        server_name some.example.com localhost;
        resolver 8.8.8.8;
        client_header_buffer_size 8k;
        large_client_header_buffers 8 64k;
        error_log /dev/stdout debug;

        location ~* \.(?:ico|css|js|gif|jpe?g|png|woff2|woff|ttf)$ {
            root /usr/share/nginx/html;
            index index.html index.htm;
        }

        # 1. Variables that are shared between endpoints are part of the server, and not location block
        set $mitigation_api_key "API_KEY";
        set $mitigation_api_et "1";
        set $detection_tag_ci "CUSTOMER_ID";
        set $detection_tag_dt "DETECTION_TAG_ID";
        set $detection_tag_host "sub.example.com";
        set $detection_tag_path "/ag/CUSTOMER_ID/clear.js";

        location ^~ /catch {
            add_header Content-Type text/html;
            header_filter_by_lua_block {
                ngx.header.content_length = nil;
            }
            body_filter_by_lua_file /etc/lua-plugins/regex-injector.lua;
            return 200 '<html><body><h1>You have been caught!</h1></body></html>';
        }

        # 2. An NGINX location block to handle signup attempts that will be redirected to if a signup is blocked by the mitigation API
        location ^~ /signup {
            default_type text/html;

            # required variables
            set $detection_tag_spa "0";
            set $detection_tag_mo "2";
            set $detection_tag_si "SITE_ID";

            # 3. The /signup route is a vanilla HTML/CSS website and redirects a blocked userto a /catch endpoint, 
            # however it informs the client that the redirect is a 200. This code is configured to deceive the client rather than inform them.
            set $block_redirect_url "/catch";
            set $block_redirect_status_code "200";

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;

            header_filter_by_lua_block {
                ngx.header.content_length = nil;
            }
            body_filter_by_lua_file /etc/lua-plugins/injector.lua;
            lua_need_request_body on;
            access_by_lua_file /etc/lua-plugins/mitigation.lua;

            proxy_pass http://localhost:$BACKEND_PORT;
        }
        # 4. Different routes to define different configuration for /login vs /signup
        location ^~ /login {
            default_type text/html;

            # required variables
            set $detection_tag_spa "1";
            set $detection_tag_mo "2";
            set $detection_tag_si "SITE_ID";
            # 5. The /login route is an SPA and defines a response code and body to respond with when a request is blocked in the case of an SPA
            set $block_spa_response_code "200";
            set $block_spa_response_body '{"success":"you are now logged in"}';

            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $remote_addr;

            header_filter_by_lua_block {
                ngx.header.content_length = nil;
            }
            body_filter_by_lua_file /etc/lua-plugins/injector.lua;
            lua_need_request_body on;
            access_by_lua_file /etc/lua-plugins/mitigation.lua;

            proxy_pass http://localhost:$BACKEND_PORT;
        }

        error_page 500 502 503 504 /50x.html;

        location = /50x.html {
            root html;
        }
    }
}