Advertisement
Open Source Projects by Phil Schwartz

Adding country-level blocking to DenyHosts with GeoIP data

DenyHosts was designed to recognize repeated SSH login failures and respond by adding hostile addresses to a deny list. That model works well when the goal is to stop individual attackers, but it becomes less convenient when an administrator wants to restrict SSH access by geography. A server may receive thousands of probes from a region where no legitimate users are expected to connect.

I added country-level filtering by combining DenyHosts event processing with a local GeoIP database. The feature allows an administrator to define countries that should be blocked or permitted, while retaining the existing threshold-based protection for addresses that do not match a geographic rule.

The main challenge was keeping the new lookup step predictable. A failed lookup should not disable SSH protection, and a stale or missing database should not turn into an accidental global block. The implementation therefore treats location data as an additional policy signal rather than as the sole basis for every decision.

Defining the geographic policy

The first design decision was whether DenyHosts should support a blacklist, a whitelist, or both. A blacklist is safer for general-purpose installations because it blocks selected countries while leaving unknown locations subject to normal attack detection. A whitelist is useful for private administration servers, but a bad database result could lock out every administrator.

I chose a configuration model that makes the policy explicit. Administrators can specify country codes to deny, country codes to allow, and the action to take when GeoIP data is unavailable. Two-letter ISO country codes keep configuration compact and avoid tying the feature to regional names that may vary between databases.

The geographic rule is evaluated when DenyHosts has an address that it would otherwise examine or add to its blocked-host list. This preserves the existing workflow: log parsing identifies a source address, policy determines how it should be handled, and the normal deny mechanism performs the enforcement.

Choosing and loading GeoIP data

GeoIP databases have changed over time. Older deployments commonly used the free GeoIP Country database and its Python bindings, while newer installations may use the GeoIP2 or MaxMind DB format. I kept the lookup layer separate from the attack-detection code so that the database provider and file format could change without rewriting the DenyHosts monitoring logic.

The database is loaded once when the service starts, rather than opened for every failed login. This reduces disk activity during a password-guessing burst and makes lookup latency almost invisible. The loader validates the configured path and records a clear warning if the file cannot be opened.

Country data is approximate. It describes the registered or routed location of an IP address, not the physical location of a person. VPN endpoints, cloud servers, proxies, mobile carriers, and redistributed address blocks can all produce results that differ from an administrator’s expectations. GeoIP should therefore support an access policy, not replace authentication, key management, or network-level controls.

Integrating the lookup into DenyHosts

The integration point matters more than the lookup itself. I placed the country check after an address had been parsed and normalized, but before the code committed it to the deny list. That made the feature compatible with existing log formats and prevented malformed input from reaching the GeoIP library.

The decision path is straightforward:

  1. Normalize the source address and reject invalid input.
  2. Query the local country database.
  3. Apply the allow or deny country policy.
  4. Fall back to the configured behavior when no country is returned.
  5. Continue with ordinary DenyHosts thresholds and persistence.

An address blocked by country policy is recorded separately from an address blocked after repeated failures. This distinction improves diagnostics and gives administrators a way to understand why a host was denied. It also avoids confusing geographic policy with evidence of a successful or attempted intrusion.

Policy choice Typical use Benefit Main risk
Deny selected countries Public servers with known unwanted regions Limits broad background scanning Can block legitimate travelers or cloud users
Allow selected countries Private administration services Creates a narrow geographic perimeter A database error can cause lockout
Ignore unknown locations Conservative mixed environments Keeps unknown addresses under normal detection Some unwanted traffic receives no geographic decision
Block unknown locations Highly restricted systems Produces a strict boundary VPNs, new ranges, and stale data may be rejected

Handling missing and changing data

A security feature should fail in a controlled direction. For a country blacklist, I use a fail-open geographic result: if the database is missing, expired, or unable to identify an address, DenyHosts continues its normal SSH attack analysis. This means the added feature is unavailable, but the original protection remains active.

A country whitelist has a different operational risk. Administrators who use it should test the database path, verify their own provider addresses, and keep console or out-of-band access available. The software should make the selected fallback behavior visible in logs instead of silently treating every failed lookup as a match.

Database updates also need operational attention. Country assignments change, providers reorganize address space, and a previously acceptable hosting network may later appear under a different country code. The database should be updated independently of the DenyHosts package, with a short validation step before the service is restarted.

Preserving compatibility and performance

DenyHosts has historically supported modest Linux systems, so the feature had to avoid unnecessary dependencies and expensive processing. A local database is preferable to a remote lookup service because it avoids network delays, external availability problems, and privacy concerns about sending every suspicious address to a third party.

The lookup code also needs to handle both IPv4 and IPv6 deliberately. Older country databases and bindings may support IPv4 more reliably than IPv6. When an IPv6 address cannot be classified, the configured fallback should apply consistently, and the event should be logged with enough detail to diagnose the limitation.

I tested the change with known addresses from several countries, private addresses, malformed log entries, missing database files, and repeated failures from the same host. I also checked that country blocking did not bypass existing exclusions for trusted addresses. Regression testing was important because DenyHosts users may have customized paths, log formats, and storage backends.

Configuration guidance for administrators

A geographic rule is most useful when it is narrow, observable, and easy to reverse. Before enabling a whitelist, I recommend running in a logging or audit mode long enough to identify legitimate administrative addresses. The same review helps reveal whether a hosting provider, corporate VPN, or monitoring service operates from a location that would otherwise be denied.

Useful operational practices include:

The feature should also be documented as an access-control aid rather than a complete intrusion-prevention system. SSH keys, disabled password authentication, rate limiting, firewalls, and timely system updates remain essential. Country filtering reduces noise and exposure, but it cannot distinguish a legitimate user from a hostile user who connects through an allowed network.

What the change adds to DenyHosts

The country-level capability gives DenyHosts a broader response than per-address blocking while preserving its original purpose. Instead of waiting for every source to cross a failed-login threshold, an administrator can reject traffic from locations that have no legitimate role in the environment. At the same time, normal detection remains available for unknown or permitted regions.

The implementation works because it keeps responsibilities separate: log parsing identifies activity, GeoIP supplies contextual data, policy code makes the decision, and the existing deny-list mechanism enforces it. That structure also leaves room for future database formats and additional address policies without making the monitor harder to maintain.

The source code, licensing details, and related Linux utilities are available through my project portfolio. Developers interested in the implementation can inspect the lookup boundary, adapt it to a current GeoIP library, and test the policy against their own SSH logs before deploying it on a production host.