NGINX Lua Modules
Without additional backend development, HUMAN Security's BotGuard for Applications product can be 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.
Checklist
This is a list of things worth checking before you start onboarding. Please speak to your HUMAN representative about this.
- Do you use multipart uploads, or have binary file upload capabilities that you wish to protect? We support all upload types as long as you are configured to use signal cookies, or signal headers. However, if you are configured to use signals injected into the body of requests, we do not support multipart file uploads.
- Does your server backend do gzip compression? We cannot inject scripts into HTML responses if the backend server is compressing the response. We advise that you compress at the NGINX level rather than at the server level. If you cannot disable this, then you will need to inject the script tags manually into your responses.
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
In your environment you will need to use lua-rocks to install some libraries
luarocks install lua-resty-http \
&& luarocks install lua-resty-cookie \
&& luarocks install lua-resty-session
Configuration
Although most of the plugin variables can be easily configured in NGINX server blocks, individual NGINX http
blocks may require some specific configuration.
The release contains the required BotGuard for Applications 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 themitigation/
directory resides there, this is where we point Nginx to. - The following two lines:
more_clear_headers Server; server_tokens off;
are optional, but tell NGINX to not set the
Server
header so at not to give away the configuration. Note however, this would require NGINX to be compiled with themore_clear_headers
flag set. We don't recommend exposing information about the NGINX server so this might be something to consider.- Place this in the
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;
Server block configuration:
Configuration Required Type Default Example Description $block_redirect_status_code
true string "307" "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 hardcode 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 organisation's 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. $global_override
false string "" "true" Enabling this will bypass all mitigation and scraping protection. Use this to quickly flip the state of the mitigation interception. $log_prefix
false string "MITIGATION-API" "SCRAPING-API" Certain logging information (e.g. decision making) will be prefixed with this. $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 be hardcoded. $mitigation_api_policy_name
false string NONE allow_for_login A policy that the customer has set up in the mitigation API. $mitigation_api_ssl_verify
false string "true" "false" Disable verifying SSL connection certificates. $signal_headers
false integer "1" "1" Informs the BotGuard tag whether or not to use signal headers as opposed to injecting headers within the payload of a request. Set to "0" to disable signal headers. Note about signal headers
- If you are using signal headers, you need to allow underscores in headers. Add the following to your server block:
underscores_in_headers on; #required for signal headers
- NGINX by default doesn't allow headers of more than a certain size. It is necessary to define larger headers and payloads as part of your server blocks. The size of uploads are up to you, here it is set to 100M.
# buffers for headers and body client_header_buffer_size 512k; large_client_header_buffers 64 512k; client_max_body_size 100M; proxy_busy_buffers_size 512k; proxy_buffers 4 512k; proxy_buffer_size 256k;
Remote Address - depending on how your environment is set up, it can be 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 where 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;
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 in order to reset the content header so that NGINX will recalculate it, and define 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;
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
andPATCH
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-HMN-MITIGATION-RESULT action = allow N/A Let request through Client continues to backend route X-HMN-MITIGATION-RESULT Error during processing N/A Let request through Client continues to backend route. Customer to decide what to do X-HMN-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-HMN-MITIGATION-STATUS
SSL Verification
SSL verification will be the default as part of your NGINX configuration. This is slightly beyond the scope of this
documentation, however if your server has the ca-certificates
package installed and you can generate a signed pem
certificate you can get NGINX to verify SSL connections to the mitigation engine.
On Linux this would involve something like the following:
apk update && apk --no-cache add ca-certificates && rm -rf /var/cache/apk/*
cp /nginx-selfsigned.crt /etc/ssl/certs/nginx-selfsigned.pem
update-ca-certificates
Once that is done, you can add the following to your NGINX http block before your server block:
lua_ssl_verify_depth 2;
lua_ssl_trusted_certificate /etc/ssl/certs/nginx-selfsigned.pem;
Alternatively we have added the ability to disable SSL verification if you trust the connection between your server and your DNS resolver. If that is the case, you can set the environment variable to disable SSL verification in your server block:
set $mitigation_api_ssl_verify "false";
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:
envsubst '$DETECTION_TAG_CI $DETECTION_TAG_DT' < nginx.conf.template > nginx.conf
Examples
All examples and conf files will need the following set:
set $mitigation_api_key "API_KEY"; # your API key
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 a </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;
# generic properties
set $log_prefix "MITIGATION-API";
set $global_override "$MITIGATION_GLOBAL_OVERRIDE";
set $global_debug "$MITIGATION_GLOBAL_DEBUG";
# 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";
#signal method
set $signal_headers "1";
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;
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 the one above, however it defines some major differences which are listed below and commented within the example for easier reading.
- Variables that are shared between endpoints are part of the
server
, and notlocation
block. - An NGINX
location
block to handle signup attempts that will be redirected to if a signup is blocked by the mitigation API. - The
/signup
route is a vanilla HTML/CSS website and redirects a blocked user to 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. - Different routes to define different configurations for
/login
vs/signup
. - The
/login
route is an SPA and defines a response code and body to respond with when a request is blocked.
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";
# set a policy if there is one that you would like to apply
set $mitigation_api_policy_name "$MITIGATION_API_POLICY_NAME";
#signal method
set $signal_headers "1";
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/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;
}
}
}