Latest / Linux Server Admin with Fexingo: Sysadmin, Bash, and Server Engineering / How to Use Linux Server TCP Wrappers for Host-Based Access Control
Transcript
- Lucas: Alright, let's talk about a tool that's been around since the early nineties, still ships with most Linux distributions, and yet a lot of newer sysadmins have never touched it — TCP Wrappers. Luna: TCP Wrappers — that's the /etc/hosts.allow and /etc/hosts.deny mechanism, right? Lucas: Exactly. It's a host-based access control layer that sits between an incoming network connection and the actual service daemon. If the service was compiled against the libwrap library — and many common ones like sshd, vsftpd, and sendmail are — TCP Wrappers checks the connecting IP or hostname against those two files before handing the connection off to the service. Lucas: If today's tech conversation gave you something usable — maybe you manage a server and you're looking for a lightweight way to lock down SSH access — the way these episodes stay ad-free is through listener support. So if you found value here, you can head over to buy me a coffee dot com slash fexingo. That's it — no upsell, just a way to keep this going. Luna: Yeah, it's a small gesture that goes a long way on our end. We really appreciate everyone who's chipped in. Lucas: So, back to TCP Wrappers. The core idea is simple: you define rules in two files. hosts.allow gets checked first — if the client matches a rule there, access is granted immediately. hosts.deny is checked second — if the client matches a rule there, access is denied. If neither file matches, the default action is to allow the connection. Luna: So the default allow behaviour means you need to explicitly deny everything you don't want. That's a bit backwards from how most people think about firewalls today. Lucas: Right. In practice, a common pattern is to put a blanket deny all in hosts.deny — something like ALL: ALL — and then explicitly allow only the services and hosts you need in hosts.allow. That shifts the default to deny, which is much more secure. Lucas: For example, if you want to allow SSH only from your office IP range, you'd put in hosts.allow: sshd: 192.168.1.0/24. And in hosts.deny: ALL: ALL. Now any SSH connection from outside that subnet gets rejected before it ever reaches the SSH daemon. Luna: Does it work with IPv6 as well? Lucas: It does. You can specify IPv6 addresses or CIDR notation. The same rule syntax applies. Some older distributions had quirks, but modern libwrap handles both address families fine. Lucas: One thing I like about TCP Wrappers is that logging is built in. By default, every allowed or denied connection gets logged via syslog — usually to /var/log/secure or /var/log/messages. So you can see exactly who tried to connect and whether they were blocked. Luna: That can be really useful during an incident. You see a bunch of denied entries from some random IP, and you know your wrapper rules are working. Lucas: Exactly. And because the check happens before the service even processes the connection, it's very lightweight. There's almost no performance impact — it's just a string comparison on the IP address. Luna: But isn't TCP Wrappers considered a bit obsolete at this point? I mean, we have nftables, firewalld, SELinux, AppArmor... Lucas: That's a fair question. It is older, and many modern distributions are moving away from it. For example, Fedora and RHEL 9 deprecated the use of TCP Wrappers in favour of nftables and system-level firewalls. But it's still widely used in legacy environments, and even on newer systems it can be a quick, no-fuss way to add a second layer of access control. Lucas: I'd argue that for a simple use case like restricting SSH to a management subnet, TCP Wrappers is actually simpler to configure than writing nftables rules, especially for junior admins. It's declarative and human-readable. Luna: Yeah, I can see that. You don't need to understand chain flow or state tables. It's just allow list, deny list, done. Lucas: Right. Now, there are some important caveats. First, the service must be compiled with libwrap support. You can check with the command 'ldd $ | grep libwrap' — if you see libwrap.so.0, it's supported. If not, TCP Wrappers won't do anything for that service. Luna: So it's not a universal firewall — it only works for services that explicitly support it. Lucas: Exactly. The second caveat: TCP Wrappers operates at the application layer, but it only sees the IP address and the daemon name. It can't inspect packet contents or handle non tcp protocols. And it doesn't work with services that spawn child processes outside the wrapper context — like some FTP servers in active mode. Lucas: Third, and this is a big one for security: hostname-based rules are vulnerable to DNS spoofing. If you use a hostname instead of an IP address, TCP Wrappers performs a reverse DNS lookup, then a forward lookup to verify. But if an attacker compromises your DNS server, they could trick the wrapper into allowing their connection. Luna: So the recommendation is to stick with IP addresses or CIDR notation whenever possible. Lucas: Absolutely. Spare yourself the DNS headache. Use IPs. Luna: Can you give us a real-world example where TCP Wrappers saved the day? Lucas: Sure. I managed a university server that hosted a research database. SSH was open to the internet because faculty needed remote access. One weekend, I saw a flood of failed SSH authentication attempts in the logs — a classic dictionary attack. I already had TCP Wrappers denying everything by default, but I had allowed SSH from the university's VPN IP range. The attacker was coming from a completely different IP, so the wrapper denied every single connection. SSH didn't even see them. The service stayed up, and I didn't have to worry about rate-limiting or fail2ban — it was handled at the wrapper level. Luna: That's a great example. The attacker didn't even get to the authentication stage. So it's like having a bouncer before you even enter the club. Lucas: Exactly. Now, if you want to implement this today, the process is straightforward. Edit /etc/hosts.allow and add your allowed services and clients. Then edit /etc/hosts.deny and add ALL: ALL. Then restart any services you've configured — though some services re-read the files on each connection, so a restart isn't always necessary. Lucas: Let me give you a concrete example. Say you run a web server on port 80 and port 443, but you only want to allow SSH from your internal network 10.0.0.0/8. In hosts.allow you'd write: sshd: 10.0.0.0/8. In hosts.deny: ALL: ALL. Now SSH from outside that range is blocked, but HTTP and HTTPS connections are still allowed because they don't use libwrap — or if they do, the default allow takes effect if there's no explicit rule. Luna: But wait — if you put ALL: ALL in hosts.deny, won't that also block HTTP if it's compiled with libwrap? Some web servers can be compiled with that support. Lucas: Great catch. Yes, that's exactly why you should test your configuration. In my example, I'm assuming Apache or Nginx are not compiled with libwrap. But if they are, you'd need to explicitly allow them in hosts.allow too. So a more robust approach might be: in hosts.allow, list all the services you want to allow from any source — like sshd, httpd, etc. — and then put ALL: ALL in hosts.deny. That way, only the listed services are permitted; everything else is blocked. Luna: So the key is to know which of your services actually use libwrap. You can't just assume. Lucas: Right. Check each daemon with ldd or consult the documentation. Also worth noting: TCP Wrappers doesn't replace your firewall. It's an additional layer. Defense in depth. You should still have nftables or iptables rules in place to block traffic at the network level. TCP Wrappers gives you finer-grained control per service without needing to manage complex firewall chains. Luna: And I imagine it's really useful in container or virtualised environments where you might not have full control over the host firewall. Lucas: Absolutely. Inside a container, you often can't modify iptables rules, but you can edit /etc/hosts.allow and /etc/hosts.deny. So TCP Wrappers works well as a lightweight access control mechanism inside containers, as long as the service supports it. Luna: What about performance? If you have thousands of rules, does it slow down? Lucas: The rule files are parsed sequentially, so performance is O in the number of rules. For most use cases — even a few dozen rules — it's negligible. But if you're trying to manage hundreds of rules, you're better off with a proper firewall. TCP Wrappers is meant for simple, targeted access control, not for complex policy. Luna: So it's a scalpel, not a chainsaw. Lucas: Exactly. Use it where it fits — locking down SSH, restricting access to a management interface, or blocking a specific service from the public internet. And because it's so simple to configure, it's a great first step for someone new to server hardening. Lucas: One more tip: you can use the twist option in hosts.deny to send a custom response back to the connecting client. For example, you could spawn a command that logs the attempt or sends an email alert. The syntax is: ALL: ALL: twist /bin/echo 'Access denied'. Not something you'd use every day, but handy for certain situations. Luna: I didn't know about twist. That could be useful for baiting attackers — you could log their IP and then drop them. Lucas: Exactly. So overall, TCP Wrappers is a mature, battle-tested tool that still has a place in modern server administration. It's not going to solve every access control problem, but for the problems it does solve, it solves them elegantly. Luna: Alright, I'm going to check my servers tonight and see which services are libwrap-enabled. Might add a few deny rules. Lucas: Do it. And if you run into any issues, the logs will tell you exactly what's happening. That's the beauty of it — transparency and simplicity.