Open-source project
denyhosts/denyhosts avatar
denyhosts/denyhosts

DenyHosts blocks SSH brute force by rewriting your host access list

Automated host blocking from SSH brute force attacks

707 stars124 forksPythonGPL-2.0

At a glance

What is it?
DenyHosts is a Python utility that watches sshd logs and adds repeat offenders to the host access list. It is a good fit for a classic single server with tcp_wrappers, and a poor fit for anything that no longer uses them or wants per-key authentication instead.
Who is it for?
Adopt DenyHosts when you run sshd on a server where the host access list is still enforced and you want automatic blocking without writing your own log parser, and skip it if your hosts use key-only authentication or a modern firewall in front of sshd. Verify first that your system actually honours /etc/hosts.deny, since the whole mechanism depends on it, and read the 2015-era release notes before assuming the code path still matches current sshd logging formats.
Can I use it commercially?
Yes, with conditions. GPL-2.0 is a copyleft licence: if you distribute software that includes it, you must release that software's source code under the same licence. Running it internally without distributing it does not trigger that obligation.
Is it still maintained?
Yes. The repository last received commits 114 days ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The mechanism is the host access list, not a firewall

DenyHosts is described as a utility aimed at thwarting sshd brute force attacks, developed by Phil Schwartz and maintained by a number of developers. The mechanism it relies on is the classic Unix host access list, and understanding that is the whole point. Repeated failed ssh attempts are counted per source address, and once a host crosses the configured threshold it is written into the access list so that the TCP wrapper layer refuses further connections from it. That is a different model from a packet filter. No rules are inserted into netfilter, no ports are closed, and the offending connection is refused at connection time rather than being dropped mid stream. The practical consequence is that the blocking depends entirely on the host still consulting the access list, a mechanism that most modern Linux distributions have deprecated or removed. It also means DenyHosts does not log a password attempt, because it never sees the session, only the failed connection recorded in the sshd log.

Configuration is a single file that must exist before anything runs

The documentation is explicit that DenyHosts requires a configuration file to be created before it can function, and that the sample file denyhosts.conf contains most of the possible settings and should be copied and then edited. That sample file sits at the top level of the repository, next to denyhosts.py, the man page denyhosts.8 and the daemon-control-dist script. The structure of a configuration file like this is what makes the tool tunable rather than fixed: thresholds, the log file to watch, the report period and the purge age all live in one place, which is unusual for a tool this old. The setup script also detects an existing configuration file and offers to back it up before overwriting, writing the old copy to a timestamped name in /etc. The daemon-control script carries its own small set of paths near the top, and those defaults are described as reasonable for many systems while still needing to be matched to your particular one.

Installing from the source tarball takes three commands as root

The documented source path is short. Unpack the tarball, change into the directory, then install:

bash
    $ cd denyhosts
bash
    # python setup.py install

That step installs the DenyHosts modules into python's site-packages directory. A binary distribution in rpm or deb form is also mentioned for people comfortable installing packages on their system, with the note that installing from source is the fallback if you are unsure. The declared Python floor in setup.py is python_requires=">=3.8", and the setup script installs the denyhosts.py entry script, the daemon-control-dist script, the configuration file into /etc, and the man page into /usr/share/man/man8. There is also a python3Conversion.sh script in the tree, a reminder that this codebase predates the Python 3 migration and was converted after the fact.

The dependency story has a wrinkle worth reading closely

The README says the software depends on the ipaddr Python module, which is available in most Linux and BSD repositories. The packaging metadata still lists requires=["ipaddr"], but the requirements file is more revealing. The ipaddr pin is conditioned on python_version < '3.0', and there are further environment markers for mock on Python older than 3.3 and configparser on Python older than 3.4. What remains unconditional is requests, plus a chain of urllib3 pins keyed to specific interpreter versions. The last of those is marked in the file as pinned by Snyk to avoid a vulnerability, which tells you the maintainers are tracking security advisories in that chain. For a reader deciding whether to deploy this, the practical point is that the declared dependency set and the shipped requirements file are not the same thing, and the unconditional part of the file is requests and urllib3 rather than the ipaddr module the README highlights.

Running as a daemon means editing a shell script and a crontab

Daemon mode is the recommended mode, and it is set up with a copy of the sample control script followed by a small edit. The paths near the top of the file are the part you touch:

bash
    # cp daemon-control-dist daemon-control
bash
    DENYHOSTS_BIN   = "/usr/bin/denyhosts.py"
    DENYHOSTS_LOCK  = "/var/lock/subsys/denyhosts"
    DENYHOSTS_CFG   = "/etc/denyhosts.conf"

The script then has to be made executable by root, with the ownership and mode set as shown, and the daemon started through it:

bash
    # chmod 700 daemon-control
bash
    # daemon-control start

The documentation tells you to check the daemon log, typically at /var/log/denyhosts, to confirm the daemon is running successfully. There is also a cron option for people who prefer not to run a background process, and the documentation points at the FAQ for the details of that, which is one place where the source is thin and the reader is sent elsewhere.

Starting it by hand is a single command with two flags

The command line invocation is documented separately from the daemon wrapper, and it is what you would put in an rc.local script or run while debugging:

bash
     # python /usr/local/bin/denyhosts --config /etc/denyhosts.conf --daemon

That launches DenyHosts and runs it in the background, using the /etc/denyhosts.conf configuration file to dictate its behaviour. Two flags, config and daemon, and the documentation describes it as usually running with a few common parameters. Automatic startup is offered in two ways. The preferred method is a symbolic link from /etc/init.d pointing at the daemon-control script, followed by registering it with chkconfig if that tool is present:

bash
    # ln -s /usr/share/denyhosts/daemon-control denyhosts
bash
    # chkconfig --add denyhosts

If chkconfig is absent, the documentation notes you can create the symlinks in /etc/rc2.d, /etc/rc3.d and /etc/rc5.d manually but says that is beyond the scope of the document. The alternative method is an entry in /etc/rc.local pointing at the daemon-control script.

Release tags stopped in 2015 while commits continued in 2026

The version history is the most important operational fact about this project, and it is split in two. The tagged releases are old: v3.1 is a version 3.1 beta from 2015-12-20, v3.0 from 2015-06-27 and v2.10 from 2015-02-20. The repository itself is not archived, and the last push was on 2026-07-01, so work has continued long after the last tag. The consequence for an adopter is that there is no released version that captures the current code. A deployment from the repository is running something that has never been versioned or announced. The setup metadata names the licence as GPL v2, and the repository contains a LICENSE.txt. There is a CHANGELOG.txt, a TODO file, a plugins/ directory and a tests/ directory in the tree, plus a denyhosts.service unit file, which indicates a systemd deployment path alongside the SysV scripts the documentation still focuses on.

Where DenyHosts is the wrong tool

The boundary is the host access list, and it is a hard one. If your Linux distribution or your server configuration no longer enforces /etc/hosts.deny through the TCP wrapper layer, DenyHosts will happily maintain the file and block nothing, which is the worst kind of failure because it looks like it is working. A second limit is what the tool measures. It counts failed logins by source address, so a distributed brute force spread across many addresses never trips a per-host threshold, and a single attacker who knows the threshold interval can stay under it. A third limit is that host access lists are address based, so a user behind a shared NAT or a corporate gateway can be locked out along with the attacker, and there is no user or key granularity in the mechanism. For a modern server where sshd accepts keys only and password authentication is turned off, the whole premise disappears, because there is no password to brute force. A firewall rule that drops repeated source addresses, or a modern intrusion prevention system, attacks the same problem with tools that are still enforced by the kernel and that can be scoped by port and by rate rather than only by address.

Editorial conclusion

Adopt DenyHosts when you run sshd on a server where the host access list is still enforced and you want automatic blocking without writing your own log parser, and skip it if your hosts use key-only authentication or a modern firewall in front of sshd. Verify first that your system actually honours /etc/hosts.deny, since the whole mechanism depends on it, and read the 2015-era release notes before assuming the code path still matches current sshd logging formats.

Frequently asked questions

How do I install DenyHosts from source?

Unpack the source distribution file, change into the denyhosts directory, then run python setup.py install as root, which installs the DenyHosts modules into python's site-packages directory. Binary rpm and deb packages are also mentioned for those comfortable with their system's package tooling.

What does DenyHosts need before it will run?

A configuration file must be created first, because DenyHosts requires one before it can function. The sample file denyhosts.conf at the top of the repository contains most of the possible settings and should be copied to /etc and edited.

How do I start DenyHosts as a daemon?

Copy daemon-control-dist to daemon-control, edit the paths near the top of that script to match your system, make it executable by root with chmod 700, and then run daemon-control start. The daemon log at /var/log/denyhosts shows whether it started successfully.

Does DenyHosts still get released?

The tagged releases are old: v3.1 is a 3.1 beta from 2015-12-20, with v3.0 and v2.10 before it. The repository is not archived and the last push was on 2026-07-01, so code changes have continued without new version tags.

Which Python version and modules does DenyHosts need?

The setup metadata declares python_requires ">=3.8" and lists ipaddr as a requirement, while the requirements file conditions the ipaddr pin on Python older than 3.0 and leaves requests and urllib3 unconditional, with urllib3 pins per interpreter version.

Official sources

  1. denyhosts/denyhosts on GitHub
  2. Issues
  3. License: GPL-2.0
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/denyhosts-denyhosts.svg)](https://hysenlabs.com/projects/denyhosts-denyhosts)

Was this useful?