Skip to main content

Jails

A jail is a filter over a log plus what to do when it matches. The protected product pushes it, because it is the one that knows what its logs look like.

Pushing a jail​

PUT /jails/{name} takes the jail and its filter as text:

{
"jailConf": "[odoo-login]\nenabled = true\nfilter = odoo-login\nlogpath = /var/lib/odoo/*/logs/odoo.log\nport = http,https\n",
"filterConf": "[Definition]\nfailregex = ^.*Login failed for (?:db:\\S+ )?login:\\S+ from <HOST>\\s*$\n"
}

Three rules the call enforces:

  • The jail's section header must match the name in the route.
  • The filter must define a failregex.
  • Every log path must be in a log folder mounted into the container. See Where a jail may read.

SSH on a Synology NAS​

DSM's SSH server writes no Failed password lines, so fail2ban's own SSH filter counts nothing on a Synology NAS and the jail reports zero failures however many wrong passwords arrive. Start the jail from the SSH on Synology DSM template instead: its filter reads the two lines DSM writes with the address for each failed session, so three wrong passwords in one session count as two failures. Set its port to the one DSM's SSH listens on (Control Panel > Terminal & SNMP), and keep the chain at INPUT, because SSH runs on the NAS itself.

Odoo​

Start the jail from the Odoo: failed logins template. Its filter counts every refused sign-in on Odoo 15.0 to 20.0, in both forms Odoo writes:

OdooThe line it writes for a refused sign-in
15.0 to 18.0Login failed for db:<database> login:<login> from <address>
19.0 and 20.0Login failed for login:<login> from <address>

The filter accepts the line from any module, so a module that refuses a sign-in itself and writes Odoo's own wording is counted too. Login successful lines are not counted. The address banned is the last one on the line, so a login name that contains from <address> cannot get another address banned.

Two things Odoo has to do for the jail to see anything:

  • Log at INFO or below. The line is an INFO line, so a log_level of warn or higher writes nothing to count. Odoo also has to write its log to a file (logfile in its configuration), and that file has to be mounted into this container at the path the jail names. See The log has to be mounted.
  • Run with --proxy-mode behind a proxy (proxy_mode = True), with the proxy setting X-Forwarded-For. Without it Odoo writes the proxy's own address on every line, and the jail bans the proxy.

The Odoo Host Manager pushes its own Odoo jail to its hosts, so nothing here is needed there.

A jail saved from the template in 1.0.2 or earlier counts nothing on Odoo 19 and 20

Fixing the template does not change a jail already saved from it: the jail keeps the filter it was saved with, which needs the database in the line. On the Jails page, choose Edit on that jail and replace the failregex line in its Filter with the template's new one, then Save:

failregex = ^.* \S+: Login failed for (?:db:\S+ )?login:.* from <HOST>\s*$

Or remove the jail and add it again from the template.

A jail's own chain​

By default every jail bans into the chain on the Options page. A jail can ban into another one with a chain line in its own section, which outranks the Options page for that jail alone:

[sshd]
chain = INPUT
enabled = true

That is how one host protects its own SSH on INPUT and a published container port on DOCKER-USER at the same time. In the console, the jail editor's Chain list writes and removes that line for you; As on the Options page removes it, so the jail inherits again. See Which chain the bans go into.

A bad definition never reaches the daemon​

The definition is written, checked with fail2ban's own configuration test, applied with a reload, and rolled back if the daemon refuses it.

A definition that would stop the daemon therefore never reaches it: you get an error, and the jails you already had keep running.

Where jails are kept​

Every jail is kept in the service's database, with its filter. fail2ban still reads files, so the service writes each jail's files from the database, at every start and on every change.

A jail file added by hand is removed at the next start

The files are output, not the record. A file in the jail folder that the database does not hold is removed when the service starts, so the daemon never runs a jail nobody can see on the Jails page. Add jails on the Jails page or with PUT /jails/{name}.

On the first start after updating from a release that kept jails only as files, every jail file is brought into the database once, with its filter, and nothing is lost.

The log has to be mounted​

Read-only, into this container, at the path the jail names. A jail pointed at a path that is not there matches nothing for ever and reports it nowhere except fail2ban's own log - so a jail that looks perfect can protect nothing. See Installing Fail2Ban.

Where a jail may read​

The daemon reads a jail's logs as root, and every line it matches is shown on the Bans page and in the notifications - so a jail may read logs, and only logs. At start-up the service reads which folders are mounted into its container, and a jail is refused when one of its log paths is:

  • outside every one of those folders, or inside the service's own configuration folder (/config), which holds its settings, keys and accounts;
  • not a full path, or climbing out of a folder with ..;
  • written with a fail2ban variable such as %(sshd_log)s. Write the path out in full.

Every log path counts: each line of a multi-line logpath, globs included, and a logpath argument given to an action, such as the mail action that sends the matching lines. The refusal names the path and the folders that are allowed.

The compose files mount /var/log. The Odoo Host Manager mounts /var/log, its instance tree, its nginx logs and a log folder of its own, each at the path it has on the host, so its jails need nothing extra. To allow fewer folders than are mounted, list them:

Jails__LogRoots__0=/var/log/nginx
Jails__LogRoots__1=/var/lib/odoo

A jail stored before 0.6.15 that reads outside these folders keeps running, but is refused the next time it is saved.

The routes​

RouteWhat it doesLicense
GET /jails, GET /jails/{name}Every stored jail, and one with its definitionAlways available
PUT /jails/{name}Creates or replaces a jail and applies itAlways available
DELETE /jails/{name}Stops a jail and removes itAlways available
POST /reloadMakes the daemon re-read its configurationAlways available
GET /optionsThe global options every jail inheritsAlways available
PUT /optionsChanges them and applies themAlways available

No route here is gated. What the license decides is whether a jail's ban writes a firewall rule or only a WOULD BAN line in the log. See Licensing.

Global options​

Every jail inherits them: ban time, find time, retry count and the like. Changing them applies them, so a change here reaches every jail at once - which is what you want for a policy and worth remembering before you narrow one.