I am planning to use nginx as reserve proxy in front my Apache server.
Our site is under heavy DoS attack, where attacker sends a lot of
malformed HTTP request to flood Apache. I intend to filter out the
attacker by:
Setting up nginx as reserve proxy
If user connect for the first time, redirect to a test.php page, where
a captcha is used to detect if it is really human.
If pass, a secret cookie is set to identify this user for a period of
time. With this cookie, user will be able to go further into Apache.
As a result, I need to have some kind of rewrite rule based on checking
cookie value (this is calculated by sha1 REMOTE_ADDR + REQUEST_DATE, for
example, for each user). Could anyone suggest a easy way of implementing
this with nginx?
I am writing a module to do the above-mentioned task. I need to access
system date, or the date of the request (for example
$_SERVER[‘REQUEST_TIME’] in PHP) to put into security session; but I
could not find this variable in Nginx 7.x. Exists such variable in
Nginx, or I have to get the value from external library?
This directive define one secret_cookie check (you can define more than
one as required). You can set individual secret_cookie check to on or
off separately. Other settings are:
NAME: Name of the cookie of interest.
RULE: If a cookie with such a name existed in HTTP header,
secret_cookie module will check it again this rule. The rule is: t =
time to live, s=salt value, u=user agent, a=remote IP address. For
example, rule=usat will check if the content of the cookie of interest
is the sha value of user agent + salt value + remote IP address + time
to live. You can repeat the rule if required (for example, rule=susast
will add salt repeatedly for several place)
DURATION: The time for the cookie to live. If the cookie is valid for
1 hour, then duration=3600. The duration check is only meaningful if
rule contains “time to live”.
SALT: A random string to increase the secure of secret cookie. Only
meaningful if the rule contains salt.
UA_LIMIT: Sometime the user agent can be very long, which will cause
problem for secret_cookie check (more processing time, more memory etc).
You can limit the check to only first few byte of the user agent. For
example, ua_limit=25 means checking only first 25 bytes.
log=on/off: Turn on or off the logging of secret_cookie checking.
One example config with secret_cookie module enable:
# Turn secret_cookie module on
secret_cookie on;
# Duration: 1 day = 86400, 2 day = 172800, 3 day = 259200, 1 weeks =
604800
# Define AntiDoS cookie
secret_cookie_def on name=AntiDoS rule=sutas duration=86400
salt=2j3ns3a ua_limit=10 log=off;
# Define RestrictedArea cooke
secret_cookie_def on name=RestrictedArea rule=tsau duration=7200
salt=3nh3323 us_limit=20 log=on;
# If AntiDoS cookie is not set, redirect to verification page
if ($secret_cookie_value !~ (AntiDoS)) {
rewrite ^(.*)$ /verification/index.php;
}
location ~* /verification/.*\.php$ {
# This is the verification location, where you should setup a php
captcha for user/bot identification.
# If captcha is valid, assign user an AntiDoS cookie which match the
above rule.
}
# These are protected areas, only Admin can enter
location ~* ^/(admin|forum/admincp)/ {
# If RestrictedArea cookie is not set, then redirect to admin
verification page
if ($secret_cookie_value !~ (RestrictedArea)){
rewrite ^(.*)$ /admin_verification/index.php last;
}
}
location ~* /admin_verification/.*\.php$ {
# Again, this is verification location, where you should setup a php
captcha for admin identification.
# If captcha is valid, assign admin an RestrictedArea cookie which
match the above rule.
}
// A pointer to post-handler function to validate the “validperiod”
param
static ngx_conf_post_handler_pt ngx_http_secret_cookie_valid_period_p =
ngx_http_secret_cookie_valid_period;
static ngx_conf_post_handler_pt ngx_http_secret_cookie_name_p =
ngx_http_secret_cookie_name;
static ngx_conf_post_handler_pt ngx_http_secret_cookie_rule_p =
ngx_http_secret_cookie_rule;
static ngx_command_t ngx_http_secret_cookie_commands[] = {
/* Define secret_cookie directive (on or off).
* Location: Main, HTTP, Location
* Take: 1 parameter, a Flag
*/
{ ngx_string(“secret_cookie”),
NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
ngx_conf_set_flag_slot, // Convert on/off to value
NGX_HTTP_LOC_CONF_OFFSET, // Instruct value to be written to
Location Config,
offsetof(ngx_http_secret_cookie_conf_t, enable), // with this
offset
NULL }, // No post-handler is necessary.
cookie is set and valid.
*/
static ngx_int_t
ngx_http_secret_cookie_variable(ngx_http_request_t *r,
ngx_http_variable_value_t *v,
uintptr_t data)
{
ngx_http_secret_cookie_ctx_t *rctx;
ngx_http_secret_cookie_conf_t *cf;
cf = ngx_http_get_module_loc_conf(r, ngx_http_secret_cookie_module);
// Only perform cookie search if the module is enabled
if (cf->enable){
rctx = ngx_http_secret_cookie(r, cf);
if ((rctx != NULL) &&
(rctx->secret_cookie_status == NGX_HTTP_SECRET_COOKIE_SET))
{
*v = ngx_http_variable_null_value;
return NGX_OK;
}
}
// Always return true (i.e. secret_cookie not set),
// unless module is enalbed and secret_cookie is checked…
*v = ngx_http_variable_true_value;
return NGX_OK;
}
/*
This is the main function: It looks for a secret cookie
in the HTTP request header, and compared with constructed value
Return: Secret Cookie SET or NOT SET!
*/
static ngx_http_secret_cookie_ctx_t *
ngx_http_secret_cookie(ngx_http_request_t *r,
ngx_http_secret_cookie_conf_t *cf)
{
ngx_int_t n, alen, tlen, slen, ulen, total_len;
ngx_http_secret_cookie_ctx_t *ctx;
time_t timestamp; // Current UNIX timestamp
u_char *user_agent = NULL, *salt = NULL,
*remote_addr = NULL, *last_change = NULL;
u_char *secret_cookie;
u_char *p;
ngx_sha1_t sha_ctx; #if (NGX_DEBUG)
// A string for print debug info
ngx_str_t mypen;
// If existed, secret_cookie was checked before, so stop!
if (ctx){ #if (NGX_DEBUG)
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“The secret_cookie was checked successfully before,
aborting!”); #endif
return ctx;
}
// Otherwise, start the checking process
if (ctx == NULL) { #if (NGX_DEBUG)
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“Context variable for secret_cookie not available,
creating one!”); #endif
// This r->pool temporary memory will be destroyed later,
// after this request is served!
ctx = ngx_pcalloc(r->pool,
sizeof(ngx_http_secret_cookie_ctx_t));
if (ctx == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: Out of memory”);
return NULL;
}
ngx_http_set_ctx(r, ctx, ngx_http_secret_cookie_module);
}
// This is copied from USERID Module:
n = ngx_http_parse_multi_header_lines(&r->headers_in.cookies,
&cf->name,
&ctx->secret_cookie);
if (n == NGX_DECLINED) { #if (NGX_DEBUG)
ngx_log_debug(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“The secret_cookie not found in the header!”); #endif
return ctx;
} #if (NGX_DEBUG)
ngx_log_debug2(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“Got a secret_cookie: "%V" - "%V"”, &cf->name,
&ctx->secret_cookie);
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“Rule to compute the secret_cookie: "%V"”,
&cf->rule); #endif
// First, prepare the variables and count the required length
n = 0;
total_len = 0;
alen = -1;
ulen = -1;
tlen = -1;
slen = -1;
while (n < (ngx_int_t) cf->rule.len){
switch (cf->rule.data){
case ‘a’:
if (alen == -1){
// Prepare “remote-addr” buffer and its length
alen = r->connection->addr_text.len;
remote_addr = r->connection->addr_text.data; #if (NGX_DEBUG)
mypen.len = alen;
mypen.data = remote_addr;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP,
r->connection->log, 0,
“Remote address to compute the secret_cookie:
"%i" - "%V"”, alen, &mypen); #endif
}
total_len += alen;
break;
case ‘t’:
if (tlen == -1){
// Prepare “last_change” buffer and its length
// Obtain current time in second
timestamp = ngx_time();
// Calculate the last_change and convert it to
string
last_change = ngx_pcalloc(r->pool, NGX_INT64_LEN);
if (last_change == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log,
0,
“secret_cookie: Out of memory”);
return ctx;
}
p = ngx_num2str(last_change,
last_change + NGX_INT64_LEN,
(uint64_t) timestamp /
cf->validperiod);
tlen = p - last_change; #if (NGX_DEBUG)
mypen.len = tlen;
mypen.data = last_change;
ngx_log_debug4(NGX_LOG_DEBUG_HTTP,
r->connection->log, 0,
“Last change (time) to compute the
secret_cookie: "%i" / "%i" = "%V" ("%i")”,
timestamp, cf->validperiod, &mypen, tlen); #endif
}
total_len += tlen;
break;
case ‘s’:
if (slen == -1){
// Prepare “salt” buffer and its length
slen = cf->salt.len;
salt = cf->salt.data; #if (NGX_DEBUG)
mypen.len = slen;
mypen.data = salt;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP,
r->connection->log, 0,
“Salt to compute the secret_cookie: "%i" -
"%V"”, slen, &mypen); #endif
}
total_len += slen;
break;
case ‘u’:
if (ulen == -1){
// Prepare “user-agent” buffer and its length
ulen = r->headers_in.user_agent->value.len;
user_agent = r->headers_in.user_agent->value.data; #if (NGX_DEBUG)
mypen.len = ulen;
mypen.data = user_agent;
ngx_log_debug2(NGX_LOG_DEBUG_HTTP,
r->connection->log, 0,
"User agent to compute the secret_cookie: "%i"
"%V"", ulen, &mypen); #endif
}
total_len += ulen;
default:
// Do not copy char
break;
}
n++;
}
// Second, create the content of secret_cookie
secret_cookie = ngx_pcalloc(r->pool, total_len);
if (secret_cookie == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: Out of memory”);
return ctx;
}
n = 0;
p = secret_cookie;
while (n < (ngx_int_t) cf->rule.len){
switch (cf->rule.data){
case ‘a’:
// This ngx_copy faster than ngx_cpymem with buffer <
16 bytes
p = ngx_copy(p, remote_addr, alen);
break;
case ‘t’:
p = ngx_copy(p, last_change, tlen);
break;
case ‘s’:
p = ngx_copy(p, salt, slen);
break;
case ‘u’:
p = ngx_cpymem(p, user_agent, ulen);
break;
}
n++;
} #if (NGX_DEBUG)
mypen.len = total_len;
mypen.data = secret_cookie;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“The content of the secret_cookie: "%i" "%V"”,
total_len, &mypen); #endif
// Third, perform sha1 on the content
p = ngx_pcalloc(r->pool, SHA_DIGEST_LENGTH);
if (p == NULL) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: Out of memory”);
return ctx;
}
n = ngx_sha1_init(&sha_ctx);
if (n == 0) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: SHA1 init fails”);
return ctx;
}
n = ngx_sha1_update(&sha_ctx, secret_cookie, total_len);
if (n == 0) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: SHA1 update fails”);
return ctx;
}
n = ngx_sha1_final(p, &sha_ctx);
if (n == 0) {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: SHA1 final fails”);
return ctx;
} #if (NGX_DEBUG)
mypen.len = SHA_DIGEST_LENGTH;
mypen.data = p;
ngx_log_debug1(NGX_LOG_DEBUG_HTTP, r->connection->log, 0,
“The content of the secret_cookie after sha1: "%i"
"%V"”, SHA_DIGEST_LENGTH, &mypen); #endif
if (ngx_raw_vs_hex(p, SHA_DIGEST_LENGTH, ctx->secret_cookie.data,
2*SHA_DIGEST_LENGTH) == 0){
ctx->secret_cookie_status = NGX_HTTP_SECRET_COOKIE_SET;
} else {
ngx_log_error(NGX_LOG_ERR, r->connection->log, 0,
“secret_cookie: Saved cookie does not match”);
}
// Return the result
return ctx;
}
/* Add $secret_cookie_set variable to the http process.
Called by “preconfiguration” hook!
*/
static ngx_int_t
ngx_http_secret_cookie_add_variable(ngx_conf_t *cf)
{
ngx_http_secret_cookie_variable_t *var;
ngx_http_variable_t *v;
for (var = ngx_http_secret_cookie_vars; var->name.len; var++) {
v = ngx_http_add_variable(cf, &var->name,
NGX_HTTP_VAR_CHANGEABLE);
if (v == NULL) {
return NGX_ERROR;
}
This function is called when location configuration is created.
It will create a default configuration for secret_cookie module.
*/
static void *
ngx_http_secret_cookie_create_conf(ngx_conf_t *cf)
{
ngx_http_secret_cookie_conf_t *conf;
Set default value /
conf->enable = NGX_CONF_UNSET;
/ This should be set by ngx_pcalloc():
conf->rule = ngx_null_string;
conf->salt = ngx_null_string;
conf->name = ngx_null_string; */
conf->validperiod = NGX_CONF_UNSET;
return conf;
}
/*
This function is called when location configuration is merged to main
config.
It will create a default configuration for secret_cookie module.
*/
static char *
ngx_http_secret_cookie_merge_conf(ngx_conf_t *cf, void *parent, void
*child)
{
ngx_http_secret_cookie_conf_t *prev = parent;
ngx_http_secret_cookie_conf_t *conf = child;
// Merge previous to current configration, with a default value:
ngx_conf_merge_value(conf->enable, prev->enable,
NGX_HTTP_SECRET_COOKIE_OFF);
ngx_conf_merge_str_value(conf->rule, prev->rule,
NGX_HTTP_SECRET_COOKIE_DEFAULT_RULE);
ngx_conf_merge_str_value(conf->salt, prev->salt,
NGX_HTTP_SECRET_COOKIE_DEFAULT_SALT);
ngx_conf_merge_str_value(conf->name, prev->name,
NGX_HTTP_SECRET_COOKIE_DEFAULT_NAME);
ngx_conf_merge_uint_value(conf->validperiod, prev->validperiod,
NGX_HTTP_SECRET_COOKIE_DEFAULT_VALID_PERIOD);
if (conf->validperiod < NGX_HTTP_SECRET_COOKIE_MIN_VALID_PERIOD) {
ngx_conf_log_error(NGX_LOG_EMERG, cf, 0,
“Valid period is too short!”);
return NGX_CONF_ERROR;
}
return NGX_CONF_OK;
}
/*
This function is called after secret_cookie_valid_period is passed
from config to module. Here you can validate the param, or change the
// First count the required length,
i = 0;
len = 0;
while (i < rule->len){
switch (ngx_tolower(rule->data)){
case ‘a’:
case ‘t’:
case ‘s’:
case ‘u’:
len++;
break;
default:
// Do not copy char
break;
}
i++;
}
if (len == 0) {
return “must contain combination of "s" (Salt) and "a"
(Remote-address) and "u" (User-agent) and "t" (Time)”;
}
// Then do the copying
new = ngx_pnalloc(cf->pool, len);
i = 0;
len = 0;
while (i < rule->len){
switch (ngx_tolower(rule->data)){
case ‘a’:
case ‘t’:
case ‘s’:
case ‘u’:
new = ngx_tolower(rule->data);
len++;
break;
default:
// Do not copy char
break;
}
i++;
}
rule->len = len;
rule->data = new;
return NGX_CONF_OK;
}
/*
Convert an int64 to string presentation
*/
static u_char * ngx_num2str(u_char *buf, u_char *last, uint64_t ui64)
{
u_char p, temp;
/
* we need temp only,
* but icc issues the warning
*/
size_t len;
uint32_t ui32;
p = temp + NGX_INT64_LEN;
if (ui64 <= NGX_MAX_UINT32_VALUE) {
ui32 = (uint32_t) ui64;
do {
*--p = (u_char) (ui32 % 10 + '0');
} while (ui32 /= 10);
} else {
do {
*–p = (u_char) (ui64 % 10 + ‘0’);
} while (ui64 /= 10);
}
/* number safe copy */
len = (temp + NGX_INT64_LEN) - p;
if (buf + len > last) {
len = last - buf;
}
return ngx_cpymem(buf, p, len);
}
/*
Comparison of a raw string and a hex-style string
*/
static ngx_int_t ngx_raw_vs_hex(u_char rawbuf, size_t rlen, u_char hexbuf, size_t hlen)
{
static u_char hex[] = “0123456789abcdef”;
u_char hexchar[] = “aa”;
ngx_int_t len, i;
// Compare by the shortest length
len = ((rlen < hlen / 2) ? rlen : hlen / 2);
i = 0;
while (i < len){
// Calculate a hexchar from rawbuff
hexchar[1] = hex & 0xf];
hexchar[0] = hex[(rawbuf>>4) & 0xf];
if ((hexchar[0] != hexbuf[2i]) || (hexchar[1] !=
hexbuf[2i+1])){
return 1;
}
i++;
}
return 0;
}
Thank to nginx, our server was able to mitigate the Slowloris DoS
attack, which killed Apache with partial or malformed HTTP headers. The
above mentioned secret_cookie module helped to filter out all other
valid botnet requests with this config:
[code]## If secret_cookie not set, redirect to verification page
if ($secret_cookie_not_set) { #store the old uri
set $uri_old $uri$is_args$args;
rewrite ^(.*)$ /verification/index.php;
}
Verification page: a php code that check for user response (for
example recaptcha),
and setting the secret_cookie if response is valid
location ~* /verification/.*.php$ {
# Do not allow direct access
internal;
# If IP in the white list,
# return without the checking process
if ($whitelist ~ ^bot) {
rewrite ^ $uri_old last;
}
# Turn off the proxy’s cache
proxy_cache off;
# Turn off access log, but turn on error log
access_log off;
error_log /usr/local/nginx-proxy3/logs/verification.log error;
# Set the rate limite for verification page
limit_req zone=vzone burst=15 nodelay;
# Also set the connection limit for verification page
limit_conn gulag 5;
include /usr/local/nginx-proxy3/conf/fastcgi.conf;
fastcgi_pass 127.0.0.1:9002;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME
And here is an example index.php code for user verification:
[code]<?php
$x = rand(1,15);
$y = rand(1,15);
Secret cookie setting, must match one define in nginx.conf
$secret_cookie_salt = “aDt%&sa”;
$secret_cookie_valid_period = 86400;
$secret_cookie_name = “secret_cookie”;
$errormsg = “”;
$result_cookie_name = “verify_human”;
$uri_cookie_name = “reference_uri”;
$verification_uri = ‘/verification/index.php’;
$test_uri = ‘/verification/’;
$domain = “.example.org”;
setcookie($result_cookie_name,
sha1($secret_cookie_salt . strval($x+$y) .
$secret_cookie_salt),
time() + 600,
‘/verification’, // Only available in ‘verification’ folder
$domain, FALSE, TRUE);
if (isset($_SERVER[‘URI_OLD’]) && (strpos($_SERVER[‘URI_OLD’],
$test_uri) !== 0)){
setcookie($uri_cookie_name,
$_SERVER[‘URI_OLD’],
time() + 600,
‘/verification’, // Only available in 'verification
folder
$domain, FALSE, TRUE);
}
if (isset($_GET) && isset($_GET[‘result’])) {
if (isset($_COOKIE) && isset($_COOKIE[$result_cookie_name])){
if (sha1($secret_cookie_salt . $_GET[‘result’] .
$secret_cookie_salt)
== $_COOKIE[$result_cookie_name]){
// Set the secret cookie and redirect
setcookie($secret_cookie_name,
sha1($secret_cookie_salt .
} else {
$errormsg = “Cannot verify your result. Try again!! You should
also check if: (1) your browser does accept cookies, (2) your proxy, if
any, does accept cookies…”;
}
}
You need also to download OpenSSL source code, and copy folder
openssl/crypto/sha to nginx/src. This step is important, as the
secret_cookie module depends on OpenSSL at compile time.
Copy sha/sha.h in openssl/crypto/sha into nginx/src/core. You will
get compilation error if not doing this.
Now create a folder name nginx/src/secret_cookie to store the
secret_cookie module. Put the config and ngx_http_secret_cookie_module.c
(described below) in this folder.
Go to folder nginx and now you can configure the make: