How to Configure TCP Wrapper Security in Linux: Complete Host-Based Access Control Guide

how to configure TCP Wrapper Security in Linux

TCP Wrappers were the first host-based access control mechanism I learned on Linux, years before I properly understood iptables. They’re largely a legacy technology on modern distros now — glibc dropped built-in support for them years ago, and most contemporary services don’t link against libwrap at all — but they still show up in older systems, some third-party daemons, and a lot of certification and textbook material. Understanding them is useful both for maintaining legacy infrastructure and for appreciating why iptables/firewalld/nftables became the standard instead.

What TCP Wrappers Actually Are

TCP Wrappers is a library (libwrap) and a pair of configuration files (/etc/hosts.allow and /etc/hosts.deny) that provide simple access control for network services, based on the connecting client’s hostname or IP address. A service that’s been “wrapped” — either compiled against libwrap directly, or launched through the tcpd wrapper binary via inetd/xinetd — checks these two files before deciding whether to accept a connection.

This is fundamentally an application-layer access control, not a kernel-level one like netfilter/iptables. The service itself (or the wrapper daemon in front of it) consults the rules; the kernel’s network stack has no awareness of them at all.

Checking Whether a Service Supports TCP Wrappers

Not every service uses libwrap. Check if a binary is linked against it:

ldd /usr/sbin/sshd | grep libwrap

On modern OpenSSH builds, this typically returns nothing — recent OpenSSH versions dropped TCP Wrappers support in favor of relying on the OS firewall. Historically supported services include sshd (older builds), vsftpd, xinetd-launched services, and various daemons compiled with --with-libwrap.

Check what package provides the library on your system:

dpkg -l | grep tcpd        # Debian/Ubuntu
rpm -q tcp_wrappers         # RHEL/CentOS (older releases)

On many current RHEL/Fedora releases, tcp_wrappers is no longer packaged at all, reflecting its status as effectively deprecated technology.

Configuration Files

/etc/hosts.allow

Lists rules that explicitly permit access. Checked first.

/etc/hosts.deny

Lists rules that explicitly deny access. Checked second, only if nothing in hosts.allow already matched.

The Decision Logic

  1. If a connection matches a rule in hosts.allow, it’s allowed, and processing stops there.
  2. If not, and it matches a rule in hosts.deny, it’s denied.
  3. If it matches neither file, it’s allowed by default — an important and frequently misunderstood detail. TCP Wrappers is “default allow” unless you explicitly set up a deny-everything catch-all.

Basic Syntax

service_list : client_list [: option : option ...]
  • service_list — daemon name(s) as they identify themselves to libwrap (often the binary name, e.g., sshd, vsftpd), or ALL.
  • client_list — hostnames, IP addresses, IP/netmask pairs, wildcards, or ALL.

Practical Examples

Allow SSH Only from a Specific Network

/etc/hosts.allow:

sshd : 192.168.1.0/255.255.255.0
sshd : 10.0.0.5

/etc/hosts.deny:

sshd : ALL

This allows SSH only from the 192.168.1.0/24 subnet and the single host 10.0.0.5, denying everyone else.

Deny Everything by Default, Allow Explicitly (Recommended Pattern)

/etc/hosts.deny:

ALL : ALL

/etc/hosts.allow:

sshd : 192.168.1.0/24
vsftpd : 192.168.1.0/24, 10.0.0.10
ALL : 127.0.0.1

This is the security-conscious default-deny approach: block everything, then poke specific holes for specific services and networks, mirroring the same philosophy used with iptables/firewalld default-deny policies.

Allow by Hostname/Domain

sshd : .example.com

The leading dot means “any host in this domain” — resolved via reverse DNS, which is itself a weakness (DNS-based rules depend on DNS integrity and can be slow if reverse lookups aren’t fast).

Using Wildcards

ALL : LOCAL
sshd : ALL EXCEPT 203.0.113.0/24

LOCAL matches any hostname without a dot (i.e., on the local domain). EXCEPT lets you carve out an exclusion within a broader match.

Combining with Actions (spawn / banners)

TCP Wrappers supports basic logging and command execution via the spawn directive, historically used for simple intrusion notification:

sshd : ALL : spawn (/usr/bin/logger -t tcpwrapper "SSH connection attempt from %h") : allow

%h expands to the client hostname. This is a genuinely dated pattern by modern logging standards (compare to structured logging via journald, or a proper IDS), but it illustrates how flexible — and how easy to misconfigure — the option syntax can get.

Testing Your Configuration

The tcpdmatch utility simulates a connection against your current rules without actually needing a live client:

tcpdmatch sshd 192.168.1.50

Example output:

client:   address  192.168.1.50
server:   process  sshd
matched:  /etc/hosts.allow line 3
access:   granted

This is worth running after every change — it’s a fast, safe way to confirm your rule logic actually does what you think before a real connection attempt tests it for you.

Real-World Use Case: xinetd-Launched Services

TCP Wrappers historically paired closely with xinetd (and its predecessor inetd), since many services launched on-demand through the super-server were compiled against libwrap or launched via the tcpd binary explicitly:

service telnet
{
    disable     = no
    socket_type = stream
    protocol    = tcp
    wait        = no
    user        = root
    server      = /usr/sbin/tcpd
    server_args = /usr/sbin/in.telnetd
}

Here, tcpd is invoked first, consults hosts.allow/hosts.deny, and only then execs the real service (in.telnetd) if access is granted.

Why TCP Wrappers Declined in Favor of Netfilter-Based Firewalls

A few structural reasons this technology has largely faded from active use:

  1. Application-layer only — TCP Wrappers only protects services actually compiled against libwrap or launched through tcpd; it does nothing for services that bypass it, which is most modern software.
  2. No port-level granularity — rules match on service name and client address, not on the specific port/protocol combination the way iptables can.
  3. Default-allow by design — a genuinely dangerous default for a security control; forgetting a rule silently leaves access open, rather than silently blocking it.
  4. glibc dropped built-in support — modern glibc doesn’t include libwrap hooks by default, so fewer and fewer services support it at all.
  5. Kernel-level filtering is strictly more capable — iptables/nftables/firewalld operate before traffic even reaches the application, covering every service uniformly regardless of whether it was compiled with wrapper support.

Migrating TCP Wrapper Rules to iptables

If you’re modernizing a legacy configuration, the translation is generally straightforward. This TCP Wrappers rule:

sshd : 192.168.1.0/24

Becomes this iptables equivalent:

sudo iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j DROP

Or the firewalld rich-rule equivalent:

sudo firewall-cmd --permanent --zone=public --add-rich-rule='rule family="ipv4" source address="192.168.1.0/24" port protocol="tcp" port="22" accept'
sudo firewall-cmd --reload

The kernel-level version has the advantage of covering the port explicitly and applying regardless of whether the target service was ever compiled with libwrap support.

How libwrap Actually Intercepts Connections

It’s worth understanding the specific mechanism by which a service ends up consulting hosts.allow/hosts.deny at all, since “is this service even wrapped” is the very first question in any troubleshooting session.

There are two structurally different ways a service ends up subject to TCP Wrappers:

  1. Compiled-in support — the daemon’s own source code is linked against libwrap and explicitly calls its hosts_ctl() function as part of its own connection-acceptance logic. The daemon itself is aware of and cooperating with TCP Wrappers.
  2. External wrapping via tcpd — the daemon isn’t aware of TCP Wrappers at all; instead, a super-server (traditionally inetd, later xinetd) launches the generic tcpd binary first, which itself is linked against libwrap, checks the access rules, and only then exec()s the real service binary if access is granted.

The practical difference matters for troubleshooting: for the first case, the actual daemon’s own binary needs to show a libwrap dependency; for the second case, it’s tcpd (or the super-server’s config referencing tcpd) that matters, and the wrapped service’s own binary may show no libwrap linkage at all, since it was never compiled with awareness of the mechanism.

# Case 1: check the daemon binary directly
ldd /usr/sbin/some-daemon | grep libwrap

# Case 2: check whether xinetd is launching it through tcpd
grep -r "tcpd" /etc/xinetd.d/

The Full Option Syntax Beyond spawn

Beyond the spawn directive shown earlier, TCP Wrappers supports a small set of additional options worth knowing when reading an inherited configuration:

sshd : 192.168.1.0/24 : allow
sshd : ALL : deny
in.telnetd : ALL : twist /bin/echo "Access denied by policy."
vsftpd : .suspicious-domain.example : severity emerg
  • allow/deny — explicit terminal actions, useful when you want a single line to unambiguously decide the outcome rather than relying on which file (hosts.allow vs hosts.deny) the rule happens to live in.
  • twist — replaces the requested service entirely with a different command (historically used to run a decoy/honeypot response instead of the real service for connections you want to actively mislead rather than just silently reject).
  • severity — overrides the syslog severity level used for logging this particular match, useful for making certain rule matches stand out more prominently in log monitoring.

Comparing hosts.allow/hosts.deny Ordering With a Concrete Example

Because the “first file wins, then check the second” logic is easy to get backwards when reading it too quickly, here’s a fully worked example showing exactly how a specific connection gets evaluated.

/etc/hosts.allow:

sshd : 192.168.1.100

/etc/hosts.deny:

sshd : 192.168.1.0/24

A connection from 192.168.1.100 (specifically):

  1. Check hosts.allow — matches sshd : 192.168.1.100access granted, and evaluation stops entirely; hosts.deny is never consulted for this connection at all.

A connection from 192.168.1.50 (same subnet, different specific host):

  1. Check hosts.allow — no match (the allow rule was for one specific address, not the whole subnet).
  2. Check hosts.deny — matches sshd : 192.168.1.0/24access denied.

This ordering — always hosts.allow first, in full, before hosts.deny is even opened — is precisely why a single specific-host exception in hosts.allow can override an entire subnet block in hosts.deny, which is a genuinely useful pattern (allow one trusted host within an otherwise-blocked range) but also a common source of “why is this one host still getting through” confusion when reviewing an unfamiliar configuration.

TCP Wrappers and IPv6

A specific, often-overlooked limitation: classic TCP Wrappers syntax and the underlying libwrap implementation were designed in an IPv4-only era. IPv6 address matching support varies significantly by implementation and distro vintage, and older deployments may not correctly evaluate IPv6 client addresses against rules written in IPv4 dotted-decimal or CIDR notation at all.

tcpdmatch sshd ::1
tcpdmatch sshd 2001:db8::1

If you’re auditing or maintaining a system that still relies on TCP Wrappers and IPv6 is enabled on that host, explicitly verify IPv6 matching behaves as expected with tcpdmatch rather than assuming your IPv4-oriented rules automatically extend to IPv6 clients — a genuinely common gap that’s led to unintentional IPv6 exposure on hosts where the administrator believed the same access restrictions applied uniformly across both protocol families.

Practical Migration Path for a Mixed Legacy Environment

If you’re maintaining a system where some services still use TCP Wrappers and you’re gradually modernizing, a reasonable interim approach is layering both mechanisms rather than doing a risky big-bang cutover:

# Keep the existing TCP Wrappers rules as a documented, understood baseline
cat /etc/hosts.allow /etc/hosts.deny

# Add equivalent kernel-level rules as the actual enforcement layer
sudo iptables -A INPUT -p tcp --dport 22 -s 192.168.1.0/24 -j ACCEPT
sudo iptables -A INPUT -p tcp --dport 22 -j DROP

# Verify both layers agree before removing the TCP Wrappers rules entirely
tcpdmatch sshd 192.168.1.50
sudo iptables -L INPUT -n -v

Running both simultaneously for a transition period, confirming they produce consistent decisions, then retiring the TCP Wrappers configuration once you’re confident the kernel-level rules fully cover the same access model, is a safer path than assuming a translation is correct without verification.

Troubleshooting

Rule doesn’t seem to apply — confirm the service is actually linked against libwrap in the first place; if ldd shows no libwrap dependency, TCP Wrappers has no effect on that service regardless of what’s in the config files.

Access granted when you expected denial — remember the default-allow behavior; check for a catch-all ALL : ALL in hosts.deny, since without one, anything not matched in either file is permitted.

Hostname-based rules behaving inconsistently — reverse DNS lookups can fail, time out, or resolve differently than expected; prefer IP/netmask-based rules for anything security-critical rather than relying on DNS.

Changes don’t take effect immediately — TCP Wrappers rules are read per-connection by the daemon (or by tcpd), so no service restart is typically required, but confirm with tcpdmatch rather than assuming.

Summary

TCP Wrappers offers simple, application-layer host-based access control via /etc/hosts.allow and /etc/hosts.deny, checked in that order, with default-allow behavior for anything unmatched. It’s largely superseded by kernel-level packet filtering (iptables, nftables, firewalld) on modern systems, both because fewer services support libwrap at all and because kernel-level filtering is strictly more capable and applies uniformly. Worth knowing for legacy systems and historical context; not something to build new security architecture around today.

References

Total
4
Shares

Leave a Reply

Previous Post
how to configure internet super server in linux

How to Configure Internet Super Server (xinetd) in Linux: Complete Service Management Guide

Next Post
how to use Secure Shell for remote logins in linux

How to Use Secure Shell (SSH) for Remote Logins in Linux: Complete Connection and Security Guide

Related Posts