Skip to main content
Somebody will work through system IDs on your SMPP port, or passwords on your API, or addresses on your customer portal. FireFlo writes down every refusal; fail2ban reads that and blocks the address at the firewall. The split is deliberate. FireFlo reports, fail2ban decides, the firewall enforces. Nothing in the gateway or the panel blocks an address itself, so a fault in this feature cannot take your gateway with it — and if you do not run fail2ban, you lose the blocking and keep the record.
Available from 0.9.22. Earlier releases record refused logins among ordinary traffic in the main logs, with no separate file to watch.

What gets recorded

One line per refusal, in a file that holds nothing else. The gateway’s file looks like this, and is meant to be read by hand as well as by fail2ban:
so grep ip=203.0.113.9 auth-failed.log answers the obvious question with no tooling. SMPP and gRPC lines also name the listener and the port the attempt arrived on, so you can see which port is being worked:
It is empty on a healthy gateway. Nothing writes to it until somebody fails to log in, which is why it can afford to be on everywhere and cheap for fail2ban to watch.
It is on by default and does not follow QUARKUS_LOG_FILE_ENABLE, unlike every other log file. That variable defaults to off, and a gateway with general file logging off would otherwise record nothing here — leaving you with jails that look configured and protect nothing.Set FIREFLO_AUTH_LOG_ENABLE=false to turn it off, knowing that turns off the evidence, not the enforcement: existing bans stand and nothing new is ever recorded.

What deliberately is not recorded

This is the more important half, and it is what makes the feature safe to switch on. Only failures to prove identity go in. Everything that happens after a credential has been accepted stays out, however much it looks like a refusal — because the caller is an authenticated customer, and blocking them cuts off their working logins too.
The API answers 403 for this, and it answers 403 for a wrong password as well. A jail reading the HTTP access log could not tell them apart, and would block a paying customer whose registration is still pending. That is the whole reason refused logins have a file of their own.
The credential was good; the product is a typo or a catalogue that has not caught up. The bind is refused, and nothing is recorded for blocking.
Capacity, not an attack. A customer whose client reconnects harder than the limit allows is not guessing anything. Those refusals are counted on the metrics endpoint instead — see Prometheus and Grafana.
gRPC sessions live in the gateway’s memory, so a restart — or a customer’s channel moving to another node — expires every one of them, and each customer’s next call is refused once with reason=sessionExpired before it logs in again. That reason never bans. Counting it would ban every reconnecting customer after every restart, on every port. It is still written, so the Security page shows it. A gRPC call with no token at all is missingCredential, and does count.
Behind a proxy, the address on every line is the proxy’s unless the gateway trusts its X-Forwarded-For — quarkus.http.proxy.proxy-address-forwarding for REST, and for gRPC smsg.grpc.proxy.forwarding (which follows the REST setting when unset). Without it, fail2ban bans the proxy and every customer behind it. See Running the gRPC API.

Fill in the never-ban list first

Before you enable a single jail. A ban is on an address, and an address usually carries a whole customer — so one wrong password in their configuration takes their working logins down with it.
One address or CIDR per line, # comments, IPv4 and IPv6 both. What belongs in it:
  • your monitoring and uptime probes, which authenticate on a loop and will eventually get one wrong
  • your own office and VPN, so a mistyped password cannot lock you out of the panel you would fix it from
  • any customer whose traffic you cannot afford to interrupt while the numbers are still being tuned
Two properties are worth knowing, because both are why this file exists rather than a longer ignoreip line in the jail: No installer ever rewrites it. The jail file is rewritten on every upgrade, so an address added there disappears silently and the first you hear of it is a banned customer. Changes apply on the next attempt. No reload, no restart, no service interruption — which is exactly what you want, because you edit this file at the moment somebody is being blocked and you need it to stop now.
An ignored address is still recorded. This stops the ban, not the log line, so a trusted address that starts failing to sign in still appears on the Security page. That is deliberate: it is how a compromised trusted host stays visible instead of becoming the one place nobody looks.

Turning it on

On a gateway that is already installed — which is most of them:
On a fresh install, as part of it:
install refuses on a host that already has one, so the first form is the one you want unless you are building a new host. update --with-fail2ban works from 0.9.24; on 0.9.22 and 0.9.23 it accepts the flag and silently does nothing, which is what the fail2ban verb exists to route around.
Off unless you ask for it. It edits host security configuration, and a flag that arrived by surprise on an upgrade is how an install starts refusing its own customers. It refuses rather than half-installing when fail2ban is not present, because jails written for a fail2ban that is not there produce a host that looks protected and is not. From 0.15.9, update also refreshes an installed filter.d/fireflo-gateway.conf when the release’s differs — the 0.15.9 filter is the one that ignores sessionExpired — and reloads fail2ban. It never installs a filter where fail2ban was not set up. What it writes: The panel jail is written only when a control panel is actually installed on the host. A jail whose log path matches nothing starts, reports itself as running, and protects nothing — so an absent panel is said out loud instead.

The two settings that matter

maxretry on the gateway jail is deliberately generous. A misconfigured ESME retries a bad bind in a tight loop, so a value tuned for SSH would ban a customer with a typo within seconds. The panel jail is tighter — a person typing a password does not get five wrong, and nothing there retries automatically. Bans cover every port, not just the listener’s — that is deliberate, on the grounds that somebody working through SMPP passwords has no business anywhere on the host. It also means a false positive costs that address everything, which is why the never-ban list above is not optional and why a reboot releases them.

Seeing who was refused, and who is blocked

Security, under Gateway in the control panel. All three surfaces in one list — because the same actor moves between them, and an address working through system IDs is often the one trying the panel afterwards — with filters, a window, and refusals-per-hour so a burst reads as a shape rather than as rows. The blocked list is asked of fail2ban directly rather than reconstructed, so it cannot drift from what is actually dropping traffic. The recorded bans and releases are still kept, because they are the only record of an address that was blocked and has since been let go. On the host:

A reboot releases FireFlo’s bans

fail2ban keeps its bans in a database and restores every unexpired one when it restarts, so without this a reboot would bring back an address banned ten minutes earlier. Since a ban covers every port — including SSH — that would make a reboot useless as an escape from a lockout, which is the one thing a reboot is for. So --with-fail2ban also installs a small unit that runs after fail2ban at boot and releases FireFlo’s jails. Only FireFlo’s: your sshd, postfix and other jails keep their bans, because clearing those is not this installer’s business.
The cost is that an attacker mid-ban is also released by a reboot. With a one-hour ban and reboots being rare, that is a small window, and they are banned again on their next attempt — whereas an operator locked out of their own host by a whole-port ban has no other lever.

Forcing a disconnect, and holding a client out

Fail2ban answers repeated failed logins. When a client is logged in and misbehaving, use Force disconnect instead — on a server’s page, on a login’s live-sessions box under the account’s Logins tab, or on Security:
  • Choose the target: an IP address, an account, or a login. It’s matched exactly, never by prefix, so acme never touches acme2. A preview lists the sessions that would be dropped before anything happens.
  • Choose what happens next:
    • Allow them to reconnect: the sessions are dropped, and a client with a reconnect loop is back within seconds.
    • Keep them out for 15 minutes, 1 hour, 24 hours or until lifted: new SMPP binds from that target are refused on every node while the hold lasts.
  • Lifting: active holds are listed on Security → Held out, with who made them and why. Lift ends a hold at once on the node you’re on, and on the others within ten seconds.
A refused bind from a held-out client is logged with reason=heldOut, which the fail2ban filter ignores — the hold is already the response, and a host-wide ban on top would outlast it. Holds are kept in the database (from 0.12.8); without one they last until the gateway restarts. Disabling a login still stops it binding for good, and now drops its live sessions in one step.

Releasing an address

From the Security page, if the panel was given the grant at install time, or on the host:
The panel’s Unban button is a privilege, and it is the only thing in the panel that changes the host. The grant is one command path — not fail2ban-client itself, which would also reach the verb that adds a block — and the wrapper behind it refuses any jail that is not FireFlo’s and anything that is not a literal address. There is no verb there that can block anybody.It is still a privilege. Anyone who reaches an operator session can release an address, and every release is recorded in the panel’s own audit log with who did it.
If the grant was not written — no panel on the host, or --with-fail2ban was never passed — the button is absent and the page prints the host command instead.

Check it before you trust it

A filter that matches nothing and a filter that is working look identical once enabled. Both report the jail as running.
Expect a non-zero match count against a file that has refusals in it. Then prove the other half, which is the one that matters: from an address that is not on the never-ban list, send a message as a properly authenticated customer whose sender ID has not been approved, take the 403, and confirm nothing was banned. Without that second check, “it blocks attackers” and “it blocks customers” are the same test.

Tokens and access

The management port’s two secrets, and why they differ.

Binds and listeners

When a customer cannot bind and nothing is blocking them.