Whitelist, Gray List, Blacklist: The Three-Door Security Model

September 17, 2026

A community server needs doors with different rules. Some addresses should always get in. Some burned their welcome and should cool off. And a few — after a human looked at what they did — should never come back, from any transport, ever.

Phosphor’s connection security grew one layer at a time, and the result is a three-list model that’s worth explaining as a unit, because each list answers a different question.

The whitelist: never touchable

The whitelist is a set of IP prefixes that the ban machinery cannot touch, no matter what. Loopback is on it. Your LAN is on it. If an entry matches an incoming address — exact match or prefix — the ban manager checks it first and returns “not banned” unconditionally.

This is a hard guarantee, not a preference. The enforcement path is ordered so whitelist supremacy is structural: even a permanent blacklist entry covering 127.0.0.1 would simply never fire. That’s deliberate — the fastest way to ruin a self-hosted server is letting an automated system lock the admin out of their own machine.

The gray list: strikes with a clock

The gray list is the automated layer. Failed attempts accumulate strikes per IP; hit the threshold and the address is banned for a fixed window, after which it expires on its own. It’s for credential-guessing scripts and misconfigured clients — the kind of traffic that should stop now and be allowed back after it’s had a think.

In-memory by design: gray-list bans are punishments for a moment, not verdicts. Restart the server and the slate is clean, because the judgment “this address misbehaved recently” simply ages out.

The sysop sees the live gray list from the SysOp panel — every ban with its reason and expiry — and can clear any entry manually, wiping both the ban and the strike history.

The black list: human judgment, permanent

The new tier is for the third question: what about the address that deserves to never come back? That’s a judgment call, and judgment calls shouldn’t expire because the process restarted.

bbs.ip_blacklist is a database table — prefix, reason, who added it, when. Entries are added from the SysOp panel ([A], with a reason prompt), listed ([X]), and removed ([R]) — and removal is a deliberate, separate act, because a permanent ban should be as hard to undo as it is to earn. Prefix matching means you can blacklist a whole subnet (10.0.0.) or one address. At startup the server loads the table into the shared ban manager, so entries survive restarts the way they should.

The layering rule is the same one as the whitelist: the blacklist is checked after the whitelist, so no permanent entry can ever cover the loopback or LAN ranges. Human judgment on the outside doors; structural protection on the inside one.

The door that didn’t lock

Shipping enforcement exposed a gap that taught us something about multi-transport systems. The ban check lived on the SSH listener — new session, resolve the client IP, isBanned(), close if so, before authentication even begins. The web terminal had the same check in its auth handler. JNet got fed violations into the same tracker.

Telnet — the oldest transport on the box — had no check at all. The Netty pipeline accepted connections and went straight to session setup. A blacklisted or gray-listed address could walk up to port 2323 and get the full login prompt. Banned users were only stopped on two of three doors.

The fix is a pipeline handler that fires at channelActive — the earliest possible moment, mirroring the SSH listener — resolves the remote address with the same unresolvable-falls-back-to-loopback rule, refuses banned IPs with a log line, and sits in the pipeline before the telnet protocol handler. Null-safe too: when no ban manager is wired (tests, console-only runs), the handler is simply never added.

public void channelActive(ChannelHandlerContext ctx) {
    String ip = resolveIp(ctx.channel().remoteAddress());
    if (bans.isBanned(ip)) {
        LOG.info("IP-BAN-REFUSED ip=" + ip + " transport=telnet");
        ctx.close();
    } else {
        ctx.fireChannelActive();
    }
}

The lesson generalizes: when a system gains a second and third transport, every policy needs an owner on each path. Security that lives in one listener isn’t server security; it’s SSH security. Test it per-transport with an embedded channel that fakes the remote address — the same trick the framework’s own tests use.

Three doors, one principle

Whitelist says we trust you structurally. Gray list says you’re on timeout. Blacklist says a person looked at you and decided. Each has its own lifetime, its own storage, and its own undo path, and the checks are ordered so no automated verdict can ever override the admin’s structural guarantees.

A digital third place needs exactly this: not a moat, but doors that behave differently — and a bouncer with a memory that survives closing time.

Full post: https://phosphorbbs.net/blog/whitelist-gray-list-blacklist-the-three-door-security-model/

#BBS #security #digitalthirdplace #dtp #socialterminal #selfhosted #opensource #jterm