HTTP ban
⚠️ Warning: This plugin is alpha. Any feedback is welcome!
HTTP middleware designed to stand between your reverse-proxy and your web application. It can block requests made by banned IPs, either by sending a custom response, or redirecting to another HTTP server (such as iocaine).
It listens on a unique UNIX socket for all domains. It can proxy any TCP/IP or UNIX socket.
General configuration
systemd options
This plugin requires no capabilities. However, it exposes a UNIX socket for use by your reverse proxy.
You should give it a group that your reverse proxy can read and write to.
If you set
systemdtofalse, you should give this group to the reaction process instead. The httpban plugin will inherit its parent's group.
{ plugins: {
httpban: {
path: '/usr/bin/reaction-plugin-httpban',
systemd_options: {
# Give it the group of your reverse-proxy
Group: ['www-data'], # On Debian
# Group: ['http'], # On Arch
} } } }plugins:
httpban:
path: /usr/bin/reaction-plugin-httpban
systemd_options:
# Give it the group of your reverse-proxy
Group: ['www-data'] # On Debian
# Group: ['http'] # On Arch
We recommend also setting a RuntimeDirectory for httpban's socket:
{ plugins: {
httpban: {
// ...
systemd_options: {
// ...
// Will create the /run/httpban/ directory
RuntimeDirectory: ['httpban'],
} } } }plugins:
httpban:
# ...
systemd_options:
# ...
# Will create the /run/httpban/ directory
RuntimeDirectory: ['httpban']Action configuration
This plugin exposes one action of type httpban.
It permits to add (or delete) an IP address (or a CIDR range) to a set maintained by the plugin.
👩💻Each set referenced here must also be used in a location config.
{
ban: {
type: 'httpban',
options: {
set: 'global',
action: 'add',
},
},
unban: {
type: 'httpban',
options: {
set: 'global',
action: 'delete',
},
after: '6h',
},
}ban:
type: httpban
options:
set: global
action: add
unban:
type: httpban
options:
set: global
action: delete
after: 6h
Each action has 3 possible options:
set
The name of the IP set.
(Must also be referenced in a location's case for the configuration to be valid)
action
Whether we add or delete the IP to/from the IP set.
pattern
The name of the Pattern that holds the ip in this Filter.
It defaults to ip, but can be set to any name, depending on your configuration.
{ patterns: {
ipv4: {
type: 'ipv4',
},
},
streams: {
s1: {
filters: {
f1: {
regex: ['^<ipv4> ...'],
actions: {
ban: {
type: 'httpban',
options: {
set: 'peertube',
action: 'add',
pattern: 'ipv4',
} } } } } } } }patterns:
ipv4:
type: ipv4
streams:
s1:
filters:
f1:
regex: ['^<ipv4> ...']
actions:
ban:
type: httpban
options:
set: peertube
action: add
pattern: ipv4Top-level configuration
This plugin has top-level options, to configure the behavior of the HTTP middleware.
{ plugins: {
httpban: {
// ...
options: {
downstream_count: 1,
socket: {
// ...
},
upstreams: {
// ...
},
locations: {
// ...
} } } } }plugins:
httpban:
options:
downstream_count: 1
socket:
# ...
upstreams:
# ...
locations:
# ...proxy_count
The number of HTTP proxies downstream.
For example, if your setup is this:
client → nginx → reaction-plugin-httpban → web app,
you have one proxy (nginx) downstream of the plugin.
downstream_count: 1
Here:
client → ha proxy → apache httpd → reaction-plugin-httpban → web app,
you have 2 proxies (ha proxy and apache httpd) downstream of the plugin.
downstream_count: 2socket
httpban only exposes a UNIX socket for now.
I have in mind that reaction running on the same host as your reverse proxy is the way. But if this is a problem for you, please open an issue!
path is the path to the socket.
Prefer using absolute paths.
The plugin will try creating its parent directories if they don't exist.
mode is the socket's mode.
It must be given as a string in octal form.
⚠️ You must wrap the number in quotes, to ensure it will be treated as a string!
{ plugins: {
httpban: {
// ...
options: {
socket: {
path: '/run/httpban/httpban.sock',
mode: '660',
} } } } }plugins:
httpban:
options:
socket:
path: /run/httpban/httpban.sock
mode: '660'upstreams
Same concept as nginx's upstream {} blocks.
In this section we specify all the upstreams hosts we'll connect to: webapps, honeypots...
{ plugins: {
httpban: {
// ...
options: {
upstreams: {
iocaine: {
host: 'unix:///run/iocaine/iocaine.sock',
},
peertube: {
host: '127.0.0.1:5000',
},
} } } } }plugins:
httpban:
options:
upstreams:
iocaine:
host: 'unix:///run/iocaine/iocaine.sock'
peertube:
host: '127.0.0.1:5000'host
The unique option for a backend.
Must be either IP:PORT, or unix:///path/to/socket.
Valid examples:
127.0.0.1:3030(localhost IPv4)[::1]:3030(localhost IPv6)10.0.1.3:80[2001:db8:cab:acb::1]:7800unix:///run/iocaine/iocaine.sock
locations
This section contains the actual reverse proxy rules.
Location expressions and request matching
Each key can either be:
- An exact domain.
- An exact domain followed by a path.
Valid examples:
example.comwww.example.comwww.example.com/pastebinwww.example.com/pastebin/
No glob patterns are interpreted for now.
- If httpban receive a request for an unconfigured domain, it will return an error.
- If there are different locations for a domain, the one with the longuest matching path will be used.
- Therefore, their order doesn't matter.
Example A: with root
Configuration:
{ plugins: { httpban: { options: { locations: { 'example.com/A': { // ... }, 'example.com/b': { // ... }, 'example.com': { // ... }, } } } } }plugins: httpban: options: locations: "example.com/A": # ... "example.com/b": # ... "example.com": # ...Requests that match
example.com/A:
example.com/Aexample.com/ABC.txtexample.com/A/example.com/A/index.htmlexample.com/A/C/Requests that matchexample.com/b:example.com/bexample.com/baby.txtexample.com/b/example.com/b/index.htmlexample.com/b/C/Requests that matchexample.com:example.com/example.com/C/hello.txtexample.com/a/example.com/B/Requests that will produce an error:www.example.com/
Example B: without root
Configuration:
{ plugins: {
httpban: {
options: {
locations: {
'example.com/A': {
// ...
},
'example.com/b': {
// ...
},
} } } } }plugins:
httpban:
options:
locations:
"example.com/A":
# ...
"example.com/b":
# ...
Requests that match example.com/A:
example.com/Aexample.com/ABC.txtexample.com/A/example.com/A/index.htmlexample.com/A/C/
Requests that match example.com/b:
example.com/bexample.com/baby.txtexample.com/b/example.com/b/index.htmlexample.com/b/C/
Requests that will produce an error:
example.com/example.com/C/hello.txtexample.com/a/example.com/B/www.example.com/
default
The default response that must be served for this location.
Conceptually the same as
elsein programming languages.
Takes either:
upstreamcode+inlinecode+page
upstream
String. The name of an upstream in the upstreams section.
code
Integer between 100 and 599. An HTTP status code.
inline
String. Some inline text that will form the response's body.
page
String. The path to a file that will form the response's body.
⚠️ The file is only read at startup and kept in memory. This will perform badly if the file is big (i.e. a ZIP bomb). Please use a proper HTTP server to do that (see NGINX + static website)
default examples:
{ plugins: {
httpban: {
options: {
upstreams: {
peertube: {
host: '127.0.0.1:5000',
},
},
locations: {
'peertube.example.com': {
default: {
upstream: 'peertube',
},
},
'example.com/A': {
default: {
code: 404,
inline: 'Not found, soworry 👉👈',
},
},
'example.com/b': {
default: {
code: 403,
page: '/var/lib/reaction/forbidden.html',
},
},
} } } } }plugins:
httpban:
options:
locations:
'peertube.example.com':
default:
upstream: 'peertube'
'example.com/A':
default:
code: 404
inline: 'Not found soworry 👉👈'
'example.com/b':
default:
code: 403
page: '/var/lib/reaction/forbidden.html'case
The response that must be served for this location, if the request's IP is in the specified set.
Conceptually the same as
if/else ifin programming languages.
Takes the same options as case + the set option.
set
String. The set's name.
(Must also be referenced in an HTTPban action for the configuration to be valid)
{ plugins: {
httpban: {
options: {
upstreams: {
peertube: {
host: '127.0.0.1:5000',
},
},
locations: {
'peertube.example.com': {
// If the IP is in the set 'peertube-password-bruteforce',
// We won't reach to peertube but return this error instead
case: {
set: 'peertube-password-bruteforce',
code: 429,
inline: 'Too many requests. Try again later ><',
},
// By default, reach to peertube
default: {
upstream: 'peertube',
},
},
} } } } }plugins:
httpban:
options:
locations:
'peertube.example.com':
# If the IP is in the set 'peertube-password-bruteforce',
# We won't reach to peertube but return this error instead
case:
set: 'peertube-password-bruteforce'
code: 429
inline: 'Too many requests. Try again later ><'
# By default, reach to peertube
default:
upstream: 'peertube'Multiple cases
If you want multiple cases in a location, case can be a list too.
The order is important, as sets will be checked in order.
{ plugins: {
httpban: {
options: {
upstreams: {
peertube: {
host: '127.0.0.1:5000',
},
iocaine: {
host: '127.0.0.1:42069',
},
},
locations: {
'peertube.example.com': {
case: [
// If the IP is in the set 'peertube-password-bruteforce',
// We won't reach to peertube but return this error instead
{
set: 'peertube-password-bruteforce',
code: 429,
inline: 'Too many requests. Try again later ><',
},
// Else if the IP is in the set 'AI scrapers',
// We won't reach to peertube but to iocaine instead
{
set: 'AI scrapers',
upstream: 'iocaine',
}
],
// By default, reach to peertube
default: {
upstream: 'peertube',
},
},
} } } } }plugins:
httpban:
options:
locations:
'peertube.example.com':
case:
# If the IP is in the set 'peertube-password-bruteforce',
# We won't reach to peertube but return this error instead
- set: 'peertube-password-bruteforce'
code: 429
inline: 'Too many requests. Try again later ><'
# Else if the IP is in the set 'AI scrapers',
# We won't reach to peertube but to iocaine instead
- set: 'AI scrapers'
upstream: 'iocaine'
# By default, reach to peertube
default:
upstream: 'peertube'Reverse proxy configuration
Let's take this setup as an example:
client → reverse proxy → reaction-plugin-httpban → web app
Any reverse proxy can work with reaction.
⚠️ There is one caveat to the HTTP ban plugin: its lifecycle follows the lifecycle of reaction, which needs to restart when its conf changes. This can cause short downtimes.
↪️ The recommended way to handle this caveat is to fall back to the upstream web app when the HTTP ban plugin is down. When the plugin is down, its protections will be momentarily inactive:
client → reverse proxy → web app
Currently, only nginx is documented. Any contribution (Apache HTTPd, Caddy, Traefik...) is appreciated.
NGINX + web app
Let's look at this minimal example:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blog.example.com;
location / {
proxy_set_header Host $host;
proxy_pass http://127.0.0.1:8080;
}
}
We'll rewrite it to use NGINX's upstream mechanism:
upstream blog {
server 127.0.0.1:8080;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blog.example.com;
location / {
proxy_set_header Host $host;
proxy_pass http://blog;
}
}
Let's use HTTP ban first, and only fall back to our original upstream if HTTP ban is down:
upstream blog {
# max_fails=1 asks to switch to the backup after 1 failed attempt
# fail_timeout=1 asks to try again after 5 seconds
server unix:/run/httpban/httpban.sock max_fails=1 fail_timeout=5;
# backup asks to use this server only when the other is down
server 127.0.0.1:8080 backup;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blog.example.com;
location / {
proxy_set_header Host $host;
proxy_pass http://blog;
}
}
External NGINX documentation for the upstream module
In HTTPban, we'll have:
{ plugins: {
httpban: {
options: {
upstreams: {
blog: {
host: '127.0.0.1:8080',
},
},
locations: {
'blog.example.com': {
case: [
// TODO
],
default: {
upstream: 'blog',
},
},
} } } } }plugins:
httpban:
options:
upstreams:
blog:
host: '127.0.0.1:8080'
locations:
'blog.example.com':
case:
# TODO
default:
upstream: 'blog'NGINX + static website
The setup will be slightly different, we'll have
client → nginx → reaction-plugin-httpban → nginx (again!)
Let's take this minimal example:
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blog.example.com;
location / {
root /srv/www/blog.example.com;
}
}
We'll rewrite it to have NGINX calling itself, with a second, local-only server on port 81, thus approaching the setup of the previous section:
upstream blog {
server 127.0.0.1:81;
}
server {
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name blog.example.com;
location / {
proxy_set_header Host $host;
proxy_pass http://blog;
}
}
server {
listen 127.0.0.1:81;
server_name blog.example.com;
location / {
root /srv/www/blog.example.com;
}
}
We can then continue in the same way as in the previous section.