#right now there is a lot of logging to error_log so during an attack those logs will fill the disk eventually. #a good idea would be to use a syslog server and log to a socket instead of a file for IO optimization #logging could also be disabled in production #depending on cluster setup some things can be changed here. #keepalive 128; or proxy_bind on multiple local ips can be used to mitigate local port exhaustion #most likely with this setup it's not the case #if this runs on the same machine as the application server UNIX sockets should be used instead of TCP upstream tor { server unix:/run/tor_pass1.sock weight=10 fail_timeout=30s; server unix:/run/tor_pass2.sock weight=10 fail_timeout=30s; } access_by_lua_no_postpone on; lua_package_path "/etc/nginx/resty/?.lua;;"; init_by_lua_block { allowed_hosts = { --torconfig "mainonion", --torconfig "masterbalanceonion", --i2pconfig "i2paddress" } function in_array(tab, val) for index, value in ipairs(tab) do if value == val then return true end end return nil end function split(str, sep) local result = {} local regex = ("([^%s]+)"):format(sep) for each in str:gmatch(regex) do table.insert(result, each) end return result end local function calc_circuit(proxyheaderip) if not proxyheaderip then return end local cg = split(proxyheaderip, ":") local g1 = cg[5] local g2 = cg[6] local glen = string.len(g1) if (glen < 4) then for i = (4 - glen),1,-1 do g1 = "0" .. g1 ::loop_label_1:: end end glen = string.len(g2) if (glen < 4) then for i = (4 - glen),1,-1 do g2 = "0" .. g2 ::loop_label_2:: end end local d1 = (string.sub(g1,1,1) .. string.sub(g1,2,2)) local d2 = (string.sub(g1,3,3) .. string.sub(g1,4,4)) local d3 = (string.sub(g2,1,1) .. string.sub(g2,2,2)) local d4 = (string.sub(g2,3,3) .. string.sub(g2,4,4)) local circuit_id = ((((bit.lshift(tonumber(d1, 16), 24)) + (bit.lshift(tonumber(d2, 16), 16))) + (bit.lshift(tonumber(d3, 16), 8))) + tonumber(d4, 16)) return circuit_id end function kill_circuit(premature, clientip, headerip) local circuitid = calc_circuit(headerip) if not circuitid then return end local sockfile = "unix:/etc/tor/c1" local response = "Closing circuit " .. circuitid .. " " local sock = ngx.socket.tcp() sock:settimeout(1000) local ok, err = sock:connect(sockfile) if not ok then ngx.log(ngx.ERR, "failed to connect to tor: " .. err) return end ngx.log(ngx.ERR, "connected to tor") local bytes, err = sock:send("authenticate \"torauthpassword\"\n") if not bytes then ngx.log(ngx.ERR, "failed authenticate to tor: " .. err) return end local data, err, partial = sock:receive() if not data then ngx.log(ngx.ERR, "failed receive data from tor: " .. err) return end local response = response .. " " .. data local bytes, err = sock:send("closecircuit " .. circuitid .. "\n") if not bytes then ngx.log(ngx.ERR, "failed send data to tor: " .. err) return end local data, err, partial = sock:receive() if not data then ngx.log(ngx.ERR, "failed receive data from tor: " .. err) return end local response = response .. " " .. data ngx.log(ngx.ERR, response) sock:close() return end } #rate limits should be set to the maximum number of resources (css/images/iframes) a page will load. Those should be kept to a minimum for performance reasons #limiting by proxy_protocol_addr works only if tor is properly passing HiddenServiceExportCircuitID in haproxy form. #limiting by cookie_ works regardless and must be used, otherwise an attacker can solve a captcha by hand and add it to a script/bot to spam #limiting by X-I2P-DestHash works when using i2p and passing the request to nginx #torconfiglimit_req_zone $proxy_protocol_addr zone=circuits:50m rate=requestratelimitvaluer/s; limit_req_zone $cookie_dcap zone=capcookie:50m rate=requestratelimitvaluer/s; #i2pconfiglimit_req_zone $http_x_i2p_desthash zone=i2pdesthash:50m rate=requestratelimitvaluer/s; #caching of dynamic static elements (admin controlled only!) proxy_cache_path /etc/nginx/cache/ levels=1:2 keys_zone=static:60m use_temp_path=off max_size=500m; #proxy_protocol only makes sense with V3 onions (exportcircuitid) otherwise it will break things. #kill_circuit can't be used without it server { #torconfig listen unix:/var/run/nginx1 proxy_protocol bind; #i2pconfig listen 127.0.0.1:6969 backlog=65536 reuseport; #i2pconfig allow 127.0.0.1; #torconfig allow unix:; deny all; proxy_cache_key "$host$request_uri$is_args$args"; proxy_cache_valid 200 1d; proxy_cache_min_uses 1; proxy_cache_use_stale error timeout invalid_header http_500 http_502 http_503 http_504; proxy_ignore_headers X-Accel-Expires Expires Cache-Control; proxy_set_header Host $host; proxy_cache_lock on; proxy_cache_background_update on; proxy_cache_revalidate on; proxy_cache_methods GET; more_clear_headers 'Server:*'; more_clear_headers 'Vary*'; more_clear_headers 'kill*'; #the following is an example of how to cache static content on the front #this reduces the amount of requests and makes your site appear faster. # location /favicon.ico { # limit_except GET { # deny all; # } # proxy_cache static; # proxy_pass http://tor; # } # # location ~* ^/((images|fonts|css)/)?.*\.(ico|css|jpeg|jpg|png|ttf|webp|pdf)$ { # limit_except GET { # deny all; # } # proxy_cache static; # proxy_pass http://tor; # } #what do do when rate limit is triggered, blacklist the cookie (if exists) and kill circuit location @ratelimit { error_log /var/log/nginx/ratelimit.log; access_by_lua_block { local pa = "no_proxy" if ngx.var.proxy_protocol_addr ~= nil then pa = ngx.var.proxy_protocol_addr end local cook = require "resty.cookie" local cookie, err = cook:new() if not cookie then ngx.log(ngx.ERR, err) return end local field, err = cookie:get("dcap") if field then local blocked_cookies = ngx.shared.blocked_cookies blocked_cookies:set(field, 1, sessionconfigvalue) end ngx.log(ngx.ERR, "Rate limited " .. ngx.var.remote_addr .. "|" .. pa) if pa ~= "no_proxy" then local ok, err = ngx.timer.at(0, kill_circuit, ngx.var.remote_addr, ngx.var.proxy_protocol_addr) if not ok then ngx.log(ngx.ERR, "failed to create timer: ", err) return end end ngx.exit(444) } } #what do do when waf is triggered, just show the error page and kill circuit for now. #naxsi seems to kick in before everything else except rate limiter but if it does trash traffic won't make it to the application servers anyway #doesn't make sense to blacklist cookie as it will annoy users location /waf { error_log /var/log/nginx/error.log; default_type text/html; content_by_lua_block { ngx.say("Error") ngx.say("") ngx.say("

Error

") ngx.say("

Your browser sent a request that this server could not understand.

") ngx.say("

Most likely your input contains invalid characters (\" , `, etc.) that except for passwords should not be used.

") ngx.say("

This may also happen if you are trying to send contact information or external links.

") ngx.say("

Please go back, check your input and try again.

") proxyip = "no_proxy" torip = ngx.var.remote_addr if ngx.var.proxy_protocol_addr ~= nil then proxyip = ngx.var.proxy_protocol_addr end ngx.log(ngx.ERR, "WAF triggered " .. torip .. "|" .. proxyip) if proxyip ~= "no_proxy" then local ok, err = ngx.timer.at(0, kill_circuit, torip, proxyip) if not ok then ngx.log(ngx.ERR, "failed to create timer: ", err) return end end } } location @502 { default_type text/html; content_by_lua_block { ngx.say("502 Timeout") ngx.say("") ngx.say("

502 Timeout

") ngx.say("

It seems this endgame front doesn't have a stable connection to the backend right now.

") ngx.say("

To fix it you can try to reload the page. If that doesn't work, and you end back here, try the following:

") ngx.say("

On Tor, if getting a new circuit doesn't work, Try to get a brand new Tor identity. If that doesn't work come back later.

") ngx.say("

On I2P, just try and refresh again and again. If that doesn't work restart I2P and wait a couple minutes before trying again.

") } } location /kill { access_by_lua_block { proxyip = "no_proxy" torip = ngx.var.remote_addr if ngx.var.proxy_protocol_addr ~= nil then proxyip = ngx.var.proxy_protocol_addr end ngx.log(ngx.ERR, "Kill area visited" .. torip .. "|" .. proxyip) local cook = require "resty.cookie" local cookie, err = cook:new() if not cookie then ngx.log(ngx.ERR, err) return end local field, err = cookie:get("dcap") if field then local blocked_cookies = ngx.shared.blocked_cookies blocked_cookies:set(field, 1, sessionconfigvalue) end if proxyip ~= "no_proxy" then local ok, err = ngx.timer.at(0, kill_circuit, torip, proxyip) if not ok then ngx.log(ngx.ERR, "failed to create timer: ", err) return end end ngx.exit(444) } } location / { aio threads; aio_write on; #access_log /var/log/nginx/access.log; error_log /var/log/nginx/error.log; #rate limits per circuit ID (prevents many requests on a single tor circuit) #torconfig limit_req zone=circuits burst=streamratelimitvalue nodelay; #torconfig error_page 503 =503 @ratelimit; #rate limits based on captcha cookie. if an attacker or bot solves the capcha by hand and inputs the cookie in a script #the cookie will be blacklisted by all fronts (eventually) and subsequent requests dropped. limit_req zone=capcookie burst=streamratelimitvalue nodelay; error_page 503 =503 @ratelimit; #rate limits based on the i2p destination hash (prevents many requests on a single i2p client connection) *DOES NOT KILL CIRCUITS* #i2pconfig limit_req zone=i2pdesthash burst=streamratelimitvalue nodelay; #i2pconfig error_page 503 =503 @ratelimit; error_page 502 =502 @502; #check if access captcha is solved and other things access_by_lua_file lua/cap.lua; SecRulesEnabled; #LearningMode; DeniedUrl /waf; CheckRule "$SQL >= 8" BLOCK; CheckRule "$RFI >= 8" BLOCK; CheckRule "$TRAVERSAL >= 4" BLOCK; CheckRule "$EVADE >= 4" BLOCK; CheckRule "$XSS >= 8" BLOCK; include "/etc/nginx/naxsi_whitelist.rules"; proxy_set_header Host $host; proxy_pass http://backendurl; header_filter_by_lua_block { local cookie, err = cook:new() if not cookie then ngx.log(ngx.ERR, err) return end if ngx.resp.get_headers()['kill'] ~= nil then local field, err = cookie:get("dcap") if field then local blocked_cookies = ngx.shared.blocked_cookies blocked_cookies:set(field, 1, sessionconfigvalue) end end } } }