"ddos challenge" style script #4

Open
luna wants to merge 20 commits from claude/ddos-protection-challenge-01CMAtrK6Dt24x9Q3v6Gz9fS into mistress
5 changed files with 35 additions and 62 deletions
Showing only changes of commit 0b0a9c7aaa - Show all commits

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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()

View 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