The hosts_access language will happily grant access by default if both hosts.allow and hosts.deny are missing. This builds a policy that allows named clients and denies everything else, then checks the files and rule order without restarting a service. It uses the hosts_access(5) language supplied by Debian's libwrap0 package, version 7.6.q-33. Allow about 15 minutes if you already know which wrapped daemon and client network you need.
You need shell access and a service that actually calls the TCP wrapper library. These files do not firewall packets and do not affect daemons that never consult libwrap. The examples are configuration changes, so use an account with sudo only for the edit, and keep an existing session open while testing.
Start by checking what is installed and whether either access file exists:
$ dpkg-query -W -f='${Package} ${Version}\n' libwrap0:amd64
libwrap0 7.6.q-33
$ ls -l /etc/hosts.allow /etc/hosts.deny
The second command may report that one or both paths do not exist. That is not an error in this language: a missing access file is treated as empty, and with both absent, access is effectively allowed by default. The relevant paths are /etc/hosts.allow and /etc/hosts.deny, searched in that order.
Checkpoint: Write down the daemon process name that the service presents to libwrap. It is normally the process's argv[0], and for inetd-managed services it is the name in the inetd configuration. A rule for the wrong name will silently miss.
For a small trusted network, a mostly closed policy is easier to reason about: put explicit permissions in /etc/hosts.allow, then put ALL: ALL in /etc/hosts.deny. The allow file is searched first, so an allowed match wins before the blanket deny is even examined.
Do not paste the example below until you have replaced the sample daemon and network. Here, sshd is allowed from the local host and from the IPv4 network 192.0.2.0/24. The documentation uses LOCAL for hosts whose names contain no dot; it does not mean every address on a private network:
# /etc/hosts.allow
sshd: LOCAL 192.0.2.0/24
# /etc/hosts.deny
ALL: ALL
The IPv4 network expression is a net and prefix length, matching addresses in that network rather than depending on reverse DNS. If you only need one client, use its address instead of a broad range. The manual also supports IPv6 prefix notation such as [2001:db8:1234::]/64.
Back up existing files before changing them, so you have a quick recovery path if a daemon or monitoring check behaves differently from your test:
$ sudo cp --preserve=all /etc/hosts.allow /etc/hosts.allow.bak
$ sudo cp --preserve=all /etc/hosts.deny /etc/hosts.deny.bak
If a source file is missing, cp will fail; save the existing file under a different name, or skip the backup for that path after confirming it does not exist. Do not create a backup by truncating the original.
Edit with your normal privileged editor:
$ sudoedit /etc/hosts.allow
$ sudoedit /etc/hosts.deny
Each active line has the shape daemon_list : client_list. Lists may contain names separated by spaces or commas. Blank lines and lines starting with # are ignored, and a backslash immediately before a newline continues a long rule. Keep a final newline on every rule: the manual lists a missing terminating newline as a diagnostic condition.
Within each file, rules are read top to bottom and the search stops at the first match. The allow file is considered in full before the deny file, so a later allow rule cannot rescue a client that already matched an earlier allow rule with a different shell command, and a deny rule cannot override an allow match.
ALL matches every client or daemon in the position where it is used..example.org matches hostnames ending in that domain.10.20. matches addresses whose leading numeric fields are 10.20.*.example.org and host?.example.org use shell-like wildcards for hostnames and addresses.EXCEPT subtracts a list, as in ALL: .example.org EXCEPT jump.example.org.Wildcard matching cannot combine with net/mask matching, leading-dot hostname matching or an address pattern ending in a dot: use one matching style per part of a rule. Host and address checks are case-insensitive except for NIS netgroup lookups, and Debian has disabled NIS netgroup support in this package since version 7.6.q-33.
The installed package provides the library and manual pages, but not the optional tcpdmatch or tcpdchk test programs. You can still verify the file syntax and policy with a controlled connection to the real wrapped service.
First inspect the resulting text as root, without changing it:
$ sudo sed -n '1,120p' /etc/hosts.allow /etc/hosts.deny
Next test from an allowed client and an intentionally unlisted client. Use a harmless protocol-level check appropriate to the daemon; for SSH, this asks for a version exchange without logging in:
$ ssh -o PreferredAuthentications=none -o PubkeyAuthentication=no -o ConnectTimeout=5 [email protected]
$ ssh -o PreferredAuthentications=none -o PubkeyAuthentication=no -o ConnectTimeout=5 [email protected]
The allowed connection should reach normal SSH authentication. The denied connection should be rejected by the service or close before authentication. Exact wording varies by client, so also check the daemon's journal or syslog at the test time: TCP wrapper messages are reported through the system logging service, and a successful TCP handshake alone does not prove a later wrapper check allowed the service.
Checkpoint: If a permitted client is rejected, check the daemon token, source address, and whether the service is linked with libwrap. If a supposedly denied client is accepted, treat that as evidence the service does not consult these files, or that the client matched an earlier allow rule. Do not solve it by adding random rules to both files.
Name-based rules can stop matching during a resolver failure: the manual says a timed-out name lookup leaves the hostname unavailable even when the name exists. Prefer an address or network expression where that is practical, and keep the rule narrow enough that a temporary lookup result cannot grant unexpected access.
KNOWN and UNKNOWN are not simple alternatives to an address list: they depend on whether hostnames and addresses can be resolved, so transient DNS trouble changes their result. PARANOID matches a hostname that does not agree with its address; in the default build, tcpd may drop such requests before consulting the tables.
Avoid using client usernames as an authentication boundary. Username lookups use IDENT-style protocols, work only for TCP services and suitable remote daemons, can delay a connection for the ten-second default timeout, and can be forged by a compromised client. The manual recommends treating the result as untrusted: network addresses, firewall policy and the daemon's own authentication should carry the security decision.
A rule may have a third field containing a shell command, but that command runs through /bin/sh with standard streams connected to /dev/null. It runs synchronously unless the command ends with &, and the inetd PATH is not reliable, so a typo or unsafe expansion can turn an access rule into code execution triggered by a network client.
Leave the third field out unless you have a tested operational reason to use it. If you later add one, use absolute program paths, review the documented percent substitutions, test the command with harmless logging, and remember that characters from expansions are replaced with underscores to reduce shell confusion. Never use an access rule to run a destructive command or to probe a remote finger service.
If a legitimate client is locked out, use an existing console or an already-open administrative session. Remove or comment the specific rule that caused the mistake, then retest. To restore the saved configuration, inspect the backups first:
$ sudo diff -u /etc/hosts.allow.bak /etc/hosts.allow || true
$ sudo diff -u /etc/hosts.deny.bak /etc/hosts.deny || true
$ sudo cp --preserve=all /etc/hosts.allow.bak /etc/hosts.allow
$ sudo cp --preserve=all /etc/hosts.deny.bak /etc/hosts.deny
The last two commands overwrite the live files, so run them only once the displayed backups are the versions you intend to restore. The wrapper library normally reads the files for each request, so a service restart should not be necessary, but follow the service's own documentation if it caches access decisions.
/etc/hosts.allow contains only the intended exceptions, and /etc/hosts.deny supplies the default deny.