Advertisement
Open Source Projects by Phil Schwartz

A Dependency-Free Rate Limiter for DenyHosts

DenyHosts was designed to detect repeated SSH authentication failures and respond by adding abusive addresses to a deny list. That model works well when an attacker makes a steady sequence of login attempts, but a burst of events can expose weaknesses in any security daemon. A process that reacts to every line independently may spend too much time parsing, writing files, and checking the same address repeatedly.

I implemented rate limiting directly inside DenyHosts rather than adding a third-party package. The goal was straightforward: measure how frequently an address generated suspicious activity, suppress redundant work during a short interval, and preserve DenyHosts’ existing behavior for hosts that crossed the blocking threshold.

The implementation had to remain compatible with a small, portable Python installation. DenyHosts is intended for servers where simplicity matters, so the feature needed to use the standard library, ordinary configuration files, and the project’s existing persistence model.

Why Rate Limiting Belonged In The Daemon

DenyHosts already receives a stream of authentication failures from SSH logs. Each event contains enough information to identify a source address, classify the failure, and update the appropriate counters. That made the log-processing layer the natural place to apply a rate limit before expensive actions occurred.

Without a limit, a busy attack can cause repeated database updates, frequent deny-list checks, and unnecessary file operations. A large number of nearly identical events may also make diagnostic logs harder to read. Rate limiting reduces this noise while preserving the evidence needed to identify a coordinated or sustained attack.

I treated rate limiting as a control on processing, rather than as a replacement for blocking. The existing host thresholds remained authoritative. The new logic simply prevented the daemon from performing the same work too frequently within a defined window.

Choosing A Simple Algorithm

I used a fixed-window counter keyed by IP address and event category. For each key, DenyHosts stores the beginning of the current interval and the number of events observed during that interval. When a new event arrives, the daemon compares the current time with the stored timestamp.

If the interval has expired, the counter resets and the new event starts a fresh window. If the interval is still active, the counter increments. Once the configured limit is reached, subsequent matching events can be ignored or handled through the existing blocking path, depending on the event type and DenyHosts policy.

A fixed window is less precise than a token bucket or sliding window, but it has important advantages here. It requires only two values per key, has predictable memory use, and can be serialized without special data structures. The algorithm is easy to audit, which is valuable in security-related code that may run continuously with elevated privileges.

Keeping The Implementation Dependency-Free

The core implementation uses Python dictionaries, integer timestamps, and the existing configuration parser. I avoided external caching systems, database drivers, and concurrency frameworks. That keeps installation simple and ensures the limiter behaves consistently on older Linux distributions.

The state record is intentionally small. Conceptually, each entry contains an event key, the start time of its current window, and its count. A helper function handles expiration, incrementing, and limit checks so that individual log parsers do not implement subtly different versions of the same rule.

Time handling also needed care. I used wall-clock timestamps because DenyHosts already persists state and compares values across daemon cycles. The code treats clock adjustments defensively: a timestamp that appears to be in the future is reset rather than allowed to suppress processing indefinitely.

Configuration And Runtime Behavior

The feature exposes the interval and maximum event count through DenyHosts configuration. Administrators can therefore choose a conservative setting for a public SSH server or a more tolerant setting for an internal host with legitimate automation. A disabled or missing limit preserves the previous behavior, which makes deployment less disruptive.

The following model illustrates the settings I used while testing the implementation:

Setting Purpose Example Value
Rate limit interval Length of the counting window 60 seconds
Maximum events Events accepted for one key per window 20
State cleanup interval How often stale entries are removed 300 seconds
Persistence mode Whether counters survive restarts Enabled
Action after limit Ignore duplicates or invoke policy Existing policy

Stale entries are removed during routine maintenance rather than by creating a timer for every address. This avoids additional threads and prevents a long-running daemon from accumulating data for addresses that have disappeared from the logs.

Persistence is useful when an attacker restarts a connection or when the daemon itself is briefly restarted. I kept persisted records compatible with the existing state format wherever possible. If an old record lacks rate-limit fields, the loader initializes them safely instead of failing the entire service.

Testing Bursts And Legitimate Traffic

I tested the limiter with synthetic log lines representing rapid authentication failures from one address. The first events were processed normally, the configured threshold was reached, and additional duplicates stopped generating unnecessary updates. After the window expired, the same address could produce a new sequence of events.

I also tested several addresses concurrently to ensure that one noisy source did not consume a global allowance. Keys had to be isolated by address and, where appropriate, by failure category. This matters because a limit shared across all hosts could allow one attacker to affect detection for every other source.

For operational validation, I compared the daemon’s behavior with real log patterns and reviewed how the resulting data appeared in reports. That kind of investigation is especially useful when diagnosing unusual network activity, as shown in Scratchy’s DDoS analysis.

Lessons From The Deployment

The main lesson was to keep rate limiting narrow. It should reduce repeated processing, not silently weaken DenyHosts’ core detection rules. Clear logging helped distinguish an address that was blocked from one whose duplicate events were merely being suppressed during the current interval.

I also found that configuration names and documentation matter as much as the counter itself. Administrators need to know whether the limit applies per address, per event type, or globally; whether counters survive a restart; and whether reaching the limit causes a block, a log message, or both.

For a dependable deployment, I recommend:

The resulting feature stayed aligned with DenyHosts’ original character: small, transparent, and practical. It uses the Python standard library, introduces no service dependency, and gives administrators a way to absorb noisy SSH attacks without turning every log entry into a separate expensive operation.

If you are maintaining DenyHosts or adapting the same approach to another Linux security utility, begin by identifying the existing event boundary, then add a small, testable counter layer before changing the blocking policy. Review the configuration and persistence behavior under realistic log bursts, and use the project’s source and documentation as the basis for further deployment work.