TCP wrappers revisited

TCP wrappers — Wietse Venema's tcpd program — date from 1990. They are present on essentially every Unix system shipped in the last decade. I have been using them for years without ever reading the source. This week I read the source. My mental model has improved enough that I want to write about it.

What TCP wrappers do

The traditional Unix way of running a small network service was via inetd, the internet super-daemon. inetd listens on multiple ports; when a connection arrives, it forks a service-specific program (an FTP server, a finger daemon, etc) and hands it the connection. The service program reads the connection from its standard input and writes responses to its standard output.

TCP wrappers insert themselves between inetd and the service program. Instead of inetd directly running the service, it runs tcpd, which checks an access control policy and then either runs the service or refuses the connection.

The access control policy is in two files: /etc/hosts.allow and /etc/hosts.deny. Lines like:

# /etc/hosts.allow
sshd : 192.0.2.0/24
in.fingerd : trusted-friend.example.com

# /etc/hosts.deny
ALL : ALL

The rule order is: check hosts.allow first; if a line matches, allow the connection. Otherwise check hosts.deny; if a line matches, deny. If neither matches, allow by default — except that most modern installations have ALL : ALL in hosts.deny to make the default-deny.

This is a sensible, simple access control mechanism. It has been Unix's standard answer for nearly a decade.

What I had not appreciated

Reading the source revealed several things that I had been getting slightly wrong.

It only works for inetd-launched services. TCP wrappers are not a generic firewall. They sit in front of services that inetd launches. Services that listen on their own (Apache, sendmail-as-daemon, BIND) are not affected. If you want to apply the same access policy to those, you need to use the firewall, the daemon's own configuration, or libwrap integration if the daemon supports it.

The match logic is more subtle than I thought. Patterns can be hostnames, IPs, IP ranges, or special tokens like LOCAL (any host without a . in its name) and KNOWN (a host with both forward and reverse DNS that match). Each daemon line in hosts.allow is checked against the connecting host using all of these.

One particular subtlety: hostname matching does forward-and-reverse DNS lookup. If reverse DNS for the connecting IP returns hostname X, and forward DNS for X returns the same IP, that hostname is used. If forward and reverse do not match, the host is treated as PARANOID and may match a PARANOID rule. This logic exists for a reason — preventing DNS-spoofing-based access — but it has the consequence that hostname-based rules are dependent on DNS being healthy at evaluation time.

For production rules, IP-based matching is more predictable. I have moved my hosts.allow to use IPs and IP ranges almost exclusively.

The shell-out feature is a security concern. TCP wrappers can run a shell command on match: sshd : ALL : spawn (echo connection from %h | mail admin). This is intended for logging or notification. It is also a privilege gap — the spawned command runs in the context of tcpd, which has whatever capabilities tcpd has. If tcpd runs as root (which it sometimes does, depending on inetd configuration), the shell-out runs as root.

The spawn feature is rarely used legitimately. I disable it in any environment where I do not specifically need it. The configure-time option is --disable-spawn, or in older versions, removing the spawn keyword from the parser is a one-line patch.

The library, libwrap, is what daemons use directly. The access policy is interpretable from inside any program by linking with libwrap and calling the right functions. Several daemons — including SSH and some versions of sendmail — do this directly, without going through tcpd. This means the same policy file is enforced regardless of how the daemon is launched. This is a good thing; it just was not obvious to me until I read the source.

What it is not

A few things TCP wrappers are not, which the casual user often gets wrong.

Not a firewall. A firewall blocks packets at the kernel level. TCP wrappers operate at the application level, after the connection is accepted. The kernel sees the connection. The daemon process is forked. Then tcpd decides to accept or refuse. This means an attacker can still see that the service is listening, can still complete the TCP handshake, and can still consume small amounts of resources before being denied.

For any host where I really do not want connections from a particular source, I use the firewall at the kernel level and TCP wrappers at the application level. Defence in depth — and the firewall stops the connection before it ever reaches inetd.

Not a substitute for daemon-level configuration. A daemon's own access control (the Apache <Directory> blocks, sendmail's relay control, BIND's allow-recursion) is more granular than what TCP wrappers can express. If a daemon supports its own access control, use that in addition to TCP wrappers, not instead of.

Not encryption. TCP wrappers say who can connect. They do not say what is on the wire afterwards. If you are running a service on an insecure protocol over an untrusted network, TCP wrappers do not help. The fix is the protocol, not the access control.

What my current setup looks like

# /etc/hosts.allow
sshd : 192.0.2.0/24
portmap : 192.0.2.0/24
rlogind : NEVER  # (always denied; here for documentation)

# /etc/hosts.deny
ALL : ALL

The relevant points:

  • IP-based, not hostname-based.
  • Default-deny.
  • Only services I actually run are mentioned.
  • A handful of services are explicitly denied even though they could not connect anyway, as documentation that I have considered them and refuse them.

For any new service I add, I add it to hosts.allow only after thinking about who should be able to connect.

The general principle

TCP wrappers are an early instance of a pattern I think will recur more broadly: a small, standalone access-control layer that sits in front of services and enforces a uniform policy. The pattern works well when it is consistently applied; it fails when it becomes one of three or four overlapping mechanisms, each with its own quirks.

The modern equivalent is probably going to be something like a policy daemon, enforced at the kernel level via LSM hooks or similar. The principle is unchanged: separate access control from the application logic, so that the application code can focus on doing its job and the access policy can be reviewed independently.

This is the architecture TCP wrappers gave us in 1990. We are still working out the next iteration.


Back to all writing