Use hosts.deny Safely for TCP Wrapper Access Rules

Get the search order of hosts.deny and hosts.allow backwards and a policy meant to protect a service can lock out a trusted client instead. This builds a small, reviewable TCP Wrapper policy in /etc/hosts.deny, an allow-list in /etc/hosts.allow, and a way to avoid the lockout. These files are read by services that use the libwrap access-control library; they do not protect every daemon on the machine.

Allow about fifteen minutes for a read-only review, or thirty minutes if you are changing a live policy. The examples use libwrap0 version 7.6.q-33 and the installed hosts.deny(5) and hosts_access(5) manuals. You need a shell and root access only for inspecting or editing the files. Keep an existing administrative session open while testing a network access change.

1. Confirm that the target service can use libwrap

Start by identifying the service you intend to control. This is an ordinary, read-only check:

$ dpkg-query -W -f='${Package} ${Version}\n' libwrap0
libwrap0 7.6.q-33
$ ldconfig -p | grep 'libwrap\.so\.0'
        libwrap.so.0 (libc6,x86-64) => /lib/x86_64-linux-gnu/libwrap.so.0

That proves the library is installed, not that a particular daemon calls it. Check the daemon's own documentation or package build information before relying on a rule: the installed package contains the library and manual pages, but not the tcpd, tcpdmatch or tcpdchk test commands. Do not treat the mere existence of /etc/hosts.deny as proof that a service is protected.

Checkpoint: write down the daemon process name exactly as the service presents it to libwrap. The manual calls this the argv[0] value, and it may not match the package or systemd unit name.

2. Back up both access files before editing

Read the current files first. These commands need elevated privileges on a normally configured machine:

$ sudo ls -l /etc/hosts.allow /etc/hosts.deny
$ sudo sed -n '1,160p' /etc/hosts.allow
$ sudo sed -n '1,160p' /etc/hosts.deny

Before changing either file, make a timestamped copy in the same directory:

$ stamp=$(date +%Y%m%d-%H%M%S)
$ sudo cp --preserve=all /etc/hosts.allow "/etc/hosts.allow.$stamp"
$ sudo cp --preserve=all /etc/hosts.deny "/etc/hosts.deny.$stamp"

The variable only names the backups. Check that both copies exist:

$ sudo ls -l "/etc/hosts.allow.$stamp" "/etc/hosts.deny.$stamp"

Do not skip this on a remote host. Replacing a file is easy; recovering a policy from memory after losing access is not.

3. Put specific exceptions in hosts.allow

libwrap checks /etc/hosts.allow first. A matching allow rule grants access immediately, so a later deny rule cannot override it. For a mostly closed policy, put the explicitly trusted sources here:

# /etc/hosts.allow
sshd: 192.0.2.44
sshd: .admin.example

The first client is one example address from the documentation range: replace it with the address you actually intend to trust. A leading dot matches host names in that domain, but it depends on name resolution, so prefer a known address or network range where that is practical.

Each rule has the form daemon_list : client_list. Names in either list can be separated by spaces or commas. Lines starting with # and blank lines are ignored, and a trailing backslash joins the next line, useful for a long list but easy to misread during an outage.

Checkpoint: reread the file and confirm the daemon name and every client value are exact. If the service has more than one listening endpoint, remember the usual rule matches only the daemon and client: server endpoint patterns are a separate, more specialised form.

4. Add the default denial in hosts.deny

Only once the allow list is correct should you add a default denial:

# /etc/hosts.deny
ALL: ALL

This denies every daemon and client pair that reached the deny file without matching an allow rule. It does not deny a client already accepted by hosts.allow: the two files are not merged into one rule list, allow is searched first, then deny, and the search stops at the first match.

Editing this file is security-sensitive and can disrupt access. Use an editor with elevated privileges, save the file with a final newline, and keep your current session open:

$ sudoedit /etc/hosts.deny
$ sudo tail -n 5 /etc/hosts.deny
ALL: ALL

There is no service restart in this workflow. The library consults the files while checking a request, so restarting an unrelated service will not make a non-libwrap daemon start honouring them.

5. Test from an allowed and a denied client

Test the real service from a source that should be allowed, then from one that should be denied. Use a maintenance window if the service matters. A successful connection proves the service accepted it; it does not by itself prove which file matched.

Check the daemon logs at the same time. Syntax errors, oversized rules, missing final newlines and failed system calls are reported through syslog according to the manual. A useful read-only check:

$ sudo journalctl --since '10 minutes ago' --no-pager | grep -iE 'tcpd|hosts\.(allow|deny)|wrap'

The exact log message depends on the daemon and logging configuration. If a permitted client is denied, inspect hosts.allow first. If an unwanted client is still accepted, verify the daemon actually uses libwrap and that its process name matches the rule.

Do not lean on UNKNOWN or KNOWN casually: they depend on name and address lookups, and a temporary DNS failure can flip the result. PARANOID matches a host whose name does not match its address, which can be a deliberate policy, but it can also reject clients during a reverse-DNS problem.

6. Use patterns narrowly

The language supports host names, addresses, IPv4 net/mask and prefix-length ranges, IPv6 prefix ranges, named files of patterns, wildcards and the EXCEPT operator. A few forms cover most policies:

# /etc/hosts.allow
sshd: 192.0.2.44, 2001:db8:1234::/48
imapd: .mail.example EXCEPT workstation.mail.example

# /etc/hosts.deny
ALL: ALL

Do not copy these documentation addresses into production; replace them with values you have reviewed. A leading dot is a domain-suffix pattern, while an address ending in a dot is an address-prefix pattern, and the two are not interchangeable. The manual also warns that wildcard matching cannot be combined with net/mask matching, leading-dot host matching or trailing-dot address matching.

Keep username matching out of a first policy. IDENT-style lookups work only for some TCP clients, can delay connections, and are not trustworthy when the client may be compromised. If you do need a username pattern, treat an unknown result as an expected case and test that failure path deliberately.

7. Recover cleanly when the policy is wrong

If a test shows a trusted client is blocked, restore the last known-good pair rather than guessing at a new rule:

$ sudo cp --preserve=all "/etc/hosts.allow.$stamp" /etc/hosts.allow
$ sudo cp --preserve=all "/etc/hosts.deny.$stamp" /etc/hosts.deny

If stamp is no longer set in your current shell, list the backups and use the exact timestamp you reviewed:

$ sudo ls -1t /etc/hosts.allow.* /etc/hosts.deny.*

Restoring these files changes access control immediately for new checks and may interrupt current connection attempts. Confirm access from the trusted client, then remove an obsolete backup only once you are certain it is no longer needed. Do not delete the only recovery copy mid-incident.

Done means