"ddos challenge" style script #4
5 changed files with 35 additions and 62 deletions
|
|
@ -10,8 +10,10 @@ aproxy is an Activity Pub Reverse Proxy Framework built on OpenResty (NGINX + Lu
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
```sh
|
```sh
|
||||||
# Install test dependencies (only needed once)
|
# Install test dependencies (only needed once for project setup)
|
||||||
make testdeps
|
make testdeps
|
||||||
|
|
||||||
|
# run this to setup the PATH so that it works
|
||||||
eval (luarocks-5.1 path --bin)
|
eval (luarocks-5.1 path --bin)
|
||||||
|
|
||||||
# Run test suite
|
# Run test suite
|
||||||
|
|
|
||||||
|
|
@ -252,23 +252,8 @@ protected_paths = {}
|
||||||
|
|
||||||
4. **Use PCRE regex**: Patterns are PCRE regular expressions, so you can use advanced patterns like `^/api/v[0-9]+/search$` for complex matching.
|
4. **Use PCRE regex**: Patterns are PCRE regular expressions, so you can use advanced patterns like `^/api/v[0-9]+/search$` for complex matching.
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
The module includes comprehensive tests. To run them:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install test dependencies (once)
|
|
||||||
make testdeps
|
|
||||||
eval $(luarocks-5.1 path --bin)
|
|
||||||
|
|
||||||
# Run tests
|
|
||||||
make test
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
## Security Considerations
|
||||||
|
|
||||||
1. **Token Generation**: The module uses Lua's `math.random` for token generation. For production use with high security requirements, consider integrating a cryptographically secure random source.
|
|
||||||
|
|
||||||
2. **Cookie Security**: Cookies are set with `HttpOnly` and `SameSite=Lax` flags for security. Consider adding `Secure` flag if you're running HTTPS only.
|
2. **Cookie Security**: Cookies are set with `HttpOnly` and `SameSite=Lax` flags for security. Consider adding `Secure` flag if you're running HTTPS only.
|
||||||
|
|
||||||
3. **Shared Dictionary Size**: Size the shared dictionaries appropriately:
|
3. **Shared Dictionary Size**: Size the shared dictionaries appropriately:
|
||||||
|
|
@ -281,37 +266,3 @@ make test
|
||||||
set_real_ip_from 10.0.0.0/8; # Your proxy IP range
|
set_real_ip_from 10.0.0.0/8; # Your proxy IP range
|
||||||
real_ip_header X-Forwarded-For;
|
real_ip_header X-Forwarded-For;
|
||||||
```
|
```
|
||||||
|
|
||||||
## Limitations
|
|
||||||
|
|
||||||
- **Shared State**: Uses nginx shared memory, so it's per-server. In a multi-server setup, bans and tokens aren't shared across servers.
|
|
||||||
- **Memory Limits**: Shared dictionaries have fixed sizes. Old entries are evicted when full (LRU).
|
|
||||||
- **Not a Complete Solution**: This helps with volumetric attacks and simple bots, but sophisticated attackers can bypass it. Use in combination with other security measures.
|
|
||||||
|
|
||||||
## Combining With Other Scripts
|
|
||||||
|
|
||||||
This module can be combined with other aproxy scripts for defense in depth:
|
|
||||||
|
|
||||||
```lua
|
|
||||||
return {
|
|
||||||
version = 1,
|
|
||||||
wantedScripts = {
|
|
||||||
-- First layer: Challenge-response for DDoS/bot protection
|
|
||||||
['ddos_protection_challenge'] = {
|
|
||||||
ban_duration = 3600,
|
|
||||||
token_duration = 86400,
|
|
||||||
-- ... config
|
|
||||||
},
|
|
||||||
|
|
||||||
-- Second layer: Restrict specific expensive endpoints
|
|
||||||
['pleroma_restrict_unauthenticated_search'] = {},
|
|
||||||
|
|
||||||
-- Third layer: Allowlist for webfinger
|
|
||||||
['webfinger_allowlist'] = {
|
|
||||||
accounts = {'user@domain.com'}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Scripts execute in order, so the challenge runs first, filtering out bots before they hit your more specific rules.
|
|
||||||
|
|
|
||||||
|
|
@ -2,18 +2,16 @@
|
||||||
-- Similar to Cloudflare's "Under Attack" mode
|
-- Similar to Cloudflare's "Under Attack" mode
|
||||||
-- Presents a challenge page with a honeypot link
|
-- Presents a challenge page with a honeypot link
|
||||||
|
|
||||||
|
local resty_random = require "resty.random"
|
||||||
|
local resty_string = require "resty.string"
|
||||||
|
|
||||||
local function generateToken()
|
local function generateToken()
|
||||||
-- Generate a random token for validation
|
-- Generate a cryptographically strong random token
|
||||||
-- Using ngx.time() and math.random for simplicity
|
local strong_random = resty_random.bytes(16, true)
|
||||||
-- In production, consider using a more secure method
|
while strong_random == nil do
|
||||||
math.randomseed(ngx.time() * 1000 + math.random(1, 1000))
|
strong_random = resty_random.bytes(16, true)
|
||||||
local chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
|
|
||||||
local token = {}
|
|
||||||
for i = 1, 32 do
|
|
||||||
local idx = math.random(1, #chars)
|
|
||||||
token[i] = chars:sub(idx, idx)
|
|
||||||
end
|
end
|
||||||
return table.concat(token)
|
return resty_string.to_hex(strong_random)
|
||||||
end
|
end
|
||||||
|
|
||||||
local function getCookieValue(cookie_header, cookie_name)
|
local function getCookieValue(cookie_header, cookie_name)
|
||||||
|
|
@ -161,8 +159,11 @@ end
|
||||||
|
|
||||||
local function serveQuestionChallenge(original_uri, state)
|
local function serveQuestionChallenge(original_uri, state)
|
||||||
-- Select a random question
|
-- Select a random question
|
||||||
math.randomseed(ngx.time() * 1000 + math.random(1, 1000))
|
local random_byte = resty_random.bytes(1, true)
|
||||||
local q_idx = math.random(1, #QUESTIONS)
|
while random_byte == nil do
|
||||||
|
random_byte = resty_random.bytes(1, true)
|
||||||
|
end
|
||||||
|
local q_idx = (string.byte(random_byte) % #QUESTIONS) + 1
|
||||||
local question = QUESTIONS[q_idx]
|
local question = QUESTIONS[q_idx]
|
||||||
|
|
||||||
-- Generate a challenge ID to store the correct answer
|
-- Generate a challenge ID to store the correct answer
|
||||||
|
|
|
||||||
3
test.lua
3
test.lua
|
|
@ -9,6 +9,9 @@ end
|
||||||
package.preload['resty.string'] = function()
|
package.preload['resty.string'] = function()
|
||||||
return require('tests.mock_resty_string')
|
return require('tests.mock_resty_string')
|
||||||
end
|
end
|
||||||
|
package.preload['resty.random'] = function()
|
||||||
|
return require('tests.mock_resty_random')
|
||||||
|
end
|
||||||
|
|
||||||
-- Create a mock shared dictionary
|
-- Create a mock shared dictionary
|
||||||
local function createMockSharedDict()
|
local function createMockSharedDict()
|
||||||
|
|
|
||||||
16
tests/mock_resty_random.lua
Normal file
16
tests/mock_resty_random.lua
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
-- Mock implementation of resty.random for testing
|
||||||
|
|
||||||
|
local random = {}
|
||||||
|
|
||||||
|
function random.bytes(len, strong)
|
||||||
|
-- For testing, generate predictable random bytes using math.random
|
||||||
|
-- In real OpenResty, this would use cryptographic random sources
|
||||||
|
local bytes = {}
|
||||||
|
for i = 1, len do
|
||||||
|
-- Use math.random to generate bytes (0-255)
|
||||||
|
table.insert(bytes, string.char(math.random(0, 255)))
|
||||||
|
end
|
||||||
|
return table.concat(bytes)
|
||||||
|
end
|
||||||
|
|
||||||
|
return random
|
||||||
Loading…
Add table
Add a link
Reference in a new issue