tcp_wrapper Macro Basics
The following information supplements the article, "Enhancing Network Security With tcp_wrapper" by Christopher Bush appearing in the May, 1999 issue of Sys Admin. This material is not meant as a standalone article.
tcp_wrapper Macro Basics
By default, tcpd will be compiled to log through syslogd(8) using the LOG_MAIL facility, used by most Sendmail daemons, and a severity of LOG_INFO. I'll assume a reasonable level of familiarity with syslog, as a detailed discussion of syslog is not the purpose of this article. The macros in the Makefile that control this are FACILITY and SEVERITY, respectively. If you heavily customize your syslog configuration, you may choose to change these macros to tailor your tcp_wrapper installation to the rest of your environment. You may also choose to change these so that you are logging tcpd messages to a completely separate log file, making it easier to scan for potential attacks.
You could make this change by adjusting the SEVERITY, or perhaps setting the FACILITY to one of LOCAL0-LOCAL7 - the facilities reserved for localized configurations and applications. One interesting "feature" I discovered is that the SEVERITY macro in the Makefile only controls the logging of accepted connections. The severity setting for logging refused connections is hard-coded into tcpd.c. Refused connections by default are logged with a severity of LOG_WARNING. Refused connections use the value of FACILITY as do accepted connections. Normally, this would not be a problem, and would log to the same place as accepted connections, since LOG_WARNING is of higher priority than LOG_INFO. Most syslog configurations will contain something like this in /etc/syslog.conf:
This would result in both LOG_INFO and LOG_WARNING messages being logged to /usr/adm/messages. On my Slackware Linux 3.5 distribution, however, the default syslog.conf file has:
*.=info; *.=notice /usr/adm/messages
Instead of logging anything of severity LOG_INFO and higher to usr/adm/messages, this logs only messages of severity equal to LOG_INFO or LOG_NOTICE. This is something to watch out for. If you don't see refused connection messages from tcpd in your log, check your /etc/syslog.conf carefully, especially if you are going to modify the FACILITY and SEVERITY macros in the Makefile.
Another important macro in the Makefile is ACCESS, which by default is set to -DHOSTS_ACCESS. This tells tcpd whether or not it will be performing access control checks or not. Even if you don't intend to use the tcp_wrapper for access control, there is a better way to do this than at compile time. Leave this set to the default of enabling access control. You can then disable access control by having empty rule files. If you choose to use access control in the future, you won't have to recompile.
The TABLES macro defines the location of the access control rule files. The default locations are /etc/hosts.allow and /etc/hosts.deny. These files contain your access control rules. I'll discuss their use later. If you really want to disguise your use of tcp_wrapper (and have used the "easy" installation method), you might want to compile the software to look somewhere besides /etc for these two files. It will be difficult for future administrators of your systems to recognize right away that you are using tcp_wrapper.
The PARANOID macro, default value -DPARANOID, specifies whether to use the paranoid mode or not. The default value turns paranoid mode on. The paranoid mode means that connection requests where the host name does not match the IP address of that host are automatically rejected. If the host name cannot be verified, the connection will be rejected as well. This adds a level of security against host name or address spoofing. You can get more control over being paranoid by disabling this at compile time, and using the PARANOID keyword in your access control rules, which will be discussed shortly. I leave PARANOID set in the Makefile. I don't think everyone's out to get me; I know they are.
There are other optional macros as well. Get used to using tcp_wrapper, then spend some time studying the Makefile; after that, you can decide if you need to tailor your installation any further. Let's move on to configuring and using tcp_wrapper.
A Rule Example
Here's an example of a very simple rule:
in.telnetd : 10.1.15.37
If this rule were in hosts.allow, it would say that connections to the telnet server from 10.1.15.37 are accepted. If they were in hosts.deny, they would be refused. It is important to realize that once a match is found, the search stops. Putting a rule like the above in hosts.deny does no good if you mistakenly put a broader rule in hosts.allow that would also match the request. Broader rules are possible through the use of patterns and wildcard keywords in the access control language.
There are six wildcard keywords; ALL, LOCAL, UNKNOWN, KNOWN, PARANOID, and EXCEPT. ALL and EXCEPT can be used in both the daemon_list and the client_list. The remainder apply just to the client_list. The ALL wildcard does just what you'd think; it matches everything. The EXCEPT keyword is sort of a qualifier to ALL and LOCAL. It allows you to specify exceptions to these more broadly inclusive wildcards. As an example, let's say you want to allow telnet connections from all hosts, but ftp connections only from one specific host. In /etc/hosts.allow, make two rules that look like these:
in.telnetd : ALL
in.ftpd : 10.1.15.37
To prevent other ftp connections that don't match the above rule, you'll need to add a rule to /etc/hosts.deny that prevents them. You could simply deny all connections of any type by putting this in hosts.deny:
ALL : ALL
You could also specifically deny just ftp connections with:
in.ftpd : ALL
The language for forming access control rules is very flexible and easy if you remember the search order, and keep in mind what happens when a connection request doesn't match a rule.
The wildcards KNOWN and UNKNOWN apply to the client_list, and will cause a match when the host name or address is known or unknown, respectively. They can also be used to match known or unknown user names. Be careful when using these wildcards. A host name may be unavailable if there are intermittent DNS failures. You don't want to refuse otherwise valid connections just because of this. Similarly, relying on detecting known usernames is troublesome because this depends on the client host running the ident daemon, identd, or some other RFC 931 compliant service. Here are a few examples using the KNOWN and UNKNOWN wildcards.
ALL : cebush@ALL
would match connections to all daemons from user "cebush" at all hosts.
in.ftpd : UNKNOWN@ALL
matches all ftp connections from unidentified users on all hosts.
ALL : ALL
ALL : UNKNOWN
would match connections to all daemons from known hosts, and unknown hosts, respectively.
Suppose you want to set up your Web server so that telnet and ftp connections are only allowed from your user ID on your workstation. In /etc/hosts.allow, you might have a rule like this:
in.telnetd in.ftpd : jreader@yourhost
and in /etc/hosts.deny:
ALL : ALL
to prevent connections to all daemons that didn't match in the hosts.allow file.
I mentioned previously the EXCEPT keyword. Let's say you want to allow all hosts except mine to connect. You could write the following rule in /etc/hosts.allow:
ALL : 'ALL EXCEPT syrinx'
where "syrinx" is the host name of my workstation. It is important to put the single quotes around this as shown; otherwise, ALL, EXCEPT, and syrinx would be treated as separate client elements in the client_list, instead of as one element.
The PARANOID wildcard works like the paranoid mode discussed previously in the installation section. Using it in access control rules simply gives you finer grained control over when it is applied.
The LOCAL wildcard will match client hosts that do not have a "." character in their name. This indicates that they are in the local domain. If you trust all local hosts and no external ones, put:
ALL : LOCAL
in hosts.allow, and:
ALL : ALL
in hosts.deny. You can also use the EXCEPT qualifier with LOCAL. For example:
ALL : 'LOCAL EXCEPT syrinx'
which matches all local clients except the host named syrinx.
There are also four important patterns you can use in your client_list. Strings with a "." character at the beginning match if the last components of the name match. For example:
ALL : .samag.com
matches everything from the samag.com domain. Similarly, strings ending with a "." will match if the first numeric fields of a connection client address match. For example:
ALL : 10.1.
matches all IP addresses in 10.1.x.x. This is useful for matching everything from a particular network or subnet. However, if a network is using a netmask that is not on an octet boundary, you can still do subnet matching simply by using an expression of the form:
where the n.n.n.n is a network address, and m.m.m.m is the subnet mask. Consider this rule:
in.fingerd : 10.1.20.0/255.255.252.0
This will match connections to the finger daemon from 10.1.20.0 through 10.1.23.255. This is a subnet scheme that is using 10-bit host numbers.
The final pattern you can use matches hosts in a netgroup, using a leading "@" character. Netgroups are a feature of NIS, which I don't use, so I won't go into that here.
The final piece of your access control rules is the optional shell command. You can use this to trigger a command or script every time a specific rule matches. Perhaps you want to receive an email every time a particular connection request is refused. You might end up with a rule like this in hosts.deny:
in.telnetd : 10.1.15.37 : (echo "Attack!" | /usr/bin/mail root)
The real power of the shell command is through expansion of special macros that are part of the access control language. These macros allow you to include information about the connection request within your shell command. The hosts_access(5) man page, included with the distribution, provides a complete reference for all of these macros, so I won't go into all of them in detail here. Instead, I'll show another version of the example above. In that example, the information that is emailed to root is extremely limited. I'd prefer to know what address or host originated the connection, and the user id if available. Consider this rule:
ALL : 10.1.15.37 : (echo "Attack against %d from %u on %a" | \
/usr/bin/mail -s 'tcpd alert' root)
The %d expands to the daemon name of the server (i.e., in.telnetd). The %u expands to the client user name, or "unknown" if it cannot be determined. Finally, %a expands to the client address. Other macros expand to the host name of the client or server, daemon process id, and more. This can greatly enhance the information you can relay when rules match, allowing you to perform other forms of logging, send descriptive email messages, or even send informative text pager alerts.
There is also an optional, extended language for access control rules. This is described in hosts_options(5). To use this extension, you have to recompile tcpd, after uncommenting the STYLE macro in the Makefile. By default, this macro (which would take the value of -DPROCESS_OPTIONS if you wanted the extensions) is commented out. You could also add -DPROCESS_OPTIONS to your make command if you don't wish to modify Makefile. I don't use these options, but you may eventually choose to. One noteworthy feature includes the ability to display banner messages to connecting clients for each service. You can also select the syslog facility and severity on a per-rule basis, giving you finer control of logging.
Another useful feature is the existence of ALLOW and DENY keywords, which make it possible to put all of your access control rules in one file. These extensions could be a powerful addition to your configuration and are worth exploring.
Four other tools, besides tcpd, are included with the tcp_wrapper software. Two of them are useful for checking and verifying your tcpd configuration. It's important to discuss them, because although tcpd is not particularly complicated to configure in most cases, the consequences of getting it wrong could be embarrassing at best and disastrous at worst. These tools can help you avoid deploying a tcpd configuration that doesn't behave as you expected.
The first utility is called tcpdmatch(8). A man page for tcpdmatch is included with the tcp_wrapper distribution. Briefly, tcpdmatch is used to confirm how tcpd will respond to a request for a specific service, from a specific client host. The command uses the following syntax:
tcpdmatch [-d] [-i inet_conf] daemon[@server] [user@]client
For example, to test how the local server's tcpd would respond to a telnet request from 10.1.15.37, you could issue the command:
% tcpdmatch in.telnetd 10.1.15.37
This, however, would test how the current "live" configuration would handle the request. Ideally, you want to perform this kind of testing before you deploy a configuration. That's where the -d and -i switches come in. The -d switch tells the tool to examine the hosts.allow and hosts.deny files in the current directory, rather than their default location (usually /etc). The -i switch takes as an argument the path to an alternate inetd.conf file, other than the default (/etc/inetd.conf in most cases). Suppose you've copied your inetd.conf file to a working directory, /usr/tmp/security, and added the configuration changes you think you need to wrap everything up with tcpd. Then, in that same directory you create a hosts.allow and hosts.deny file with your desired access controls. While in this working directory, issue the command:
% tcpdmatch -d -i ./inetd.conf in.fingerd 10.1.15.37
substituting for the daemon and client as necessary. You'll want to do this for each service you've wrapped, and for a variety of client addresses and host names, depending upon the access control rules you've configured. The tcpdmatch utility will report something like this:
client: hostname rocinante.bushnet.net
client: address 10.1.15.37
server: process in.fingerd
which shows whether the requested connection would be accepted or rejected.
Another included utility, tcpdchk(8), performs an examination of your tcpd configuration and presents a report of any problems it finds. A man page for tcpdchk(8) is also included in the distribution. The syntax for tcpdchk is:
tcpdchk [-a] [-d] [-i inet_conf] [-v]
Like tcpdmatch, tcpdchk has the -i and -d switches, which behave exactly the same as described above. There is also a -v switch, which presents a pretty-printed report of each access control rule. The -a switch is used to report on rules that allow access without the ALLOW keyword, which is a feature of the extended access control language described in hosts_options(5). I won't go into particulars of using -a here.
If tcpdchk (with no command line switches) finds no errors in your configuration, it prints nothing. Below is a sample output from running tcpdchk with the -v switch:
rocinante:/tmp# /usr/local/etc/tcpdchk -d -i ./inetd.conf -v
Using network configuration file: ./inetd.conf
>>> Rule hosts.deny line 14:
The try_from utility is useful in determining whether your protected host can properly perform host and username lookups against connecting systems. You can test this by calling try_from on your protected host from another system. For example, while logged in on a different host, run the command:
rocinante:/home/cebush% rsh syrinx /usr/local/etc/try_from
client address (%a): 10.1.15.37
client hostname (%n): rocinante.bushnet.net
client username (%u): cebush
client info (%c): firstname.lastname@example.org
server address (%A): 10.1.15.43
server hostname (%N): syrinx.bushnet.net
server process (%d): try-from
server info (%s): email@example.com
Be sure that remote shell connections are allowed by the current tcpd configuration on the protected host ("syrinx" in my example) before you try this. The output from try_from should tell you which host you connected from, and potentially the username. This will be helpful in deciding whether you can set up the protected host with access control rules based on the username. If running try_from as above does not report a username, you probably won't be able to use user id lookups in your access control rules.
The safe_finger utility is provided for use in the optional shell command section of the access control rules. If you want to run a finger command back to a connecting host to get further information, use safe_finger instead of your vendor-supplied finger command. Your vendor-supplied finger command may subject you to security problems through responses to your finger probe. For example, you may have in your /etc/hosts.deny file:
in.telnetd : ALL : (/usr/local/etc/safe_finger -l @%h |
/usr/bin/mail -s "%d %h" root) &
This will send an email to root with the output of safe_finger run against the connecting host, substituted in place of the %h in the command.
Using tcp_wrapper on Web Sites
Implementing this type of policy with tcp_wrapper is easy. Your /etc/hosts.deny file will simply have one line:
ALL : ALL
with any optional shell commands added on as previously discussed. Then, in /etc/hosts.allow, you simply list the services that are allowed to connect, and only those IP addresses or host names permitted. For example:
in.telnetd : 10.1.15.124
where 10.1.15.124 would be the IP address from which the Webmaster connects. Remember that tcpd looks for a match first in /etc/hosts.allow, which was configured with very minimal information. You need to create the universally restrictive /etc/hosts.deny file as described above to match everything else and deny access. If /etc/hosts.deny were left empty, connection requests from hosts other than the Webmaster's would fail to match in hosts.allow, but would also fail to match in hosts.deny, and the connection would be accepted. This is not what you want.
This example raises a particularly thorny issue. As a Webmaster, I may want to administer my site from my dial-up ISP, while working at home or while travelling. In this case, the only logical solution is to arrange for a static IP address from my ISP. Many ISPs can't or won't do this, and insist on handing out dynamically assigned addresses through DHCP. Other ISPs may accommodate you, but at a higher cost. The other option would be to expand the range of allowed connections in hosts.allow to allow connections from the entire pool of IP addresses from your ISP. For example:
in.telnetd : 10.0.
in.ftpd : 10.0.
Now, anyone who uses the same ISP as you can potentially connect to those services and exploit them to attack your Web site. You might as well not have tcpd running at all.
The other policy extreme is based on the statement, "That which is not explicitly denied is allowed." This may be used when you have a very open, general purpose system, but there are specific people or services you do not allow. In this case, you would have an empty /etc/hosts.allow file, or no file at all. The reason for this lies in the order in which access control matches are searched. Because /etc/hosts.allow is consulted first, if it were configured with:
ALL : ALL
all connections from all hosts would match this rule and /etc/hosts.deny would be ignored. But having /etc/hosts.allow empty provides more of an implicit permission, and /etc/hosts.deny is then consulted for matches to determine what specific connections to refuse.
Let's say your organization has just hired John Q. Hacker as a contractor, and you don't trust him. You could set up your systems, which may ordinarily allow all connections, with an empty /etc/hosts.allow file, and configure /etc/hosts.deny with:
ALL : 10.1.15.37
where 10.1.15.37 is the IP address of John Q. Hacker's workstation. You are denying connection requests from that address for all services. You might also add to this list other hosts that you know John has access to, depending upon whether they are multi-user systems or not. If John's workstation, or the other hosts he uses, support the IDENT service or some other similar descendant of RFC 931, you may attempt to match with:
ALL : jhacker@ALL 10.1.15.37
in /etc/hosts.deny. This will block any connection from hosts that tcp_wrapper can retrieve the user id from and match it to jhacker. It will also block all connections from 10.1.15.37, regardless of the user id.
This is obviously a contrived example, and not a foolproof one. It would be more sensible not to hire John Q. Hacker in the first place - but maybe he's a COBOL wizard, and with Y2K right around the corner . . . well, you get the picture.
More likely, your policy dictates some middle ground between these extremes when it comes to network service access. You have to look at each server individually, and configure each appropriately. The first thing you should do is turn off all network services you don't need. If you're not sure of one, turn it off and wait for the phone to ring. It's guerrilla systems administration to be sure, but it is effective.
I always find it easier to get forgiveness than permission for changes like that, and when it's done for security reasons, forgiveness is pretty easy to get. You can always turn the service back on if it turns out that someone uses it, and wrap tcpd around it for logging and access control. If you're squeamish about turning the service off so unceremoniously, wrap it with tcpd and no access control, and include a shell command in the /etc/hosts.allow file that will log connections to that service to a separate file. This will make it easier to track who, if anyone, has used it over a period of time. For example, before turning off the telnet daemon, you might put the following in /etc/hosts.allow:
in.telnetd : ALL : (echo "'date': %u connected from %h >> \
Keep an eye on /usr/adm/telnet_log for 30 days or so. If nobody connects, turn off telnet altogether. If someone does, find out who, and why, before wrapping additional security around the service. Remember, it's your responsibility to administer the system appropriately. You should know for what purposes it is being accessed. For the services you know are needed, apply the most restrictive access control you can. You can always adjust it later, and that's easier than recovering compromised data from backups.
The tcp_wrapper software is not necessarily part of a firewall implementation. It is possible though, that it could be used to intercept and hand off connection requests to proxy services, like a telnet proxy. It's conceivable that only certain people in your organization are authorized to telnet to hosts outside your network. Your Webmaster might be one, especially if the Web site is hosted externally. In this case, tcpd may be wrapped around the firewall's telnet proxy, and access control configured appropriately. There are many firewall implementations available, which may offer other ways of providing this level of control to proxy services. Firewalls that use a circuit-level proxy like SOCKS would not likely be a good place to use tcpd. However, just because you have a firewall protecting your servers against attack from the Internet, that doesn't mean that a solution like tcp_wrapper isn't a good idea.
There are other ways an intruder can access your servers, like walking in the front door, the way John Q. Hacker did, or through dial-up connections. That's why you need to set up your tcp_wrapper configuration to protect against internal systems or networks. You may need to either deny or allow access to one person, or an entire subnet, or a building with multiple subnets. Perhaps you have an ftp server that should only be accessed by a select few PCs in your sales and marketing department. From this server, you may be providing files with sensitive marketing data or new product information to the people in that department to share. The tcp_wrapper software, in conjunction with solid user level and file system security, can help provide a strong level of inter-departmental security for your systems and data.
About the Author
Chris Bush is a Programmer Analyst for a major mid-west bank in Cleveland, Ohio. He does Web site management, Web development, Internet/Intranet architecture, UNIX systems administration, and dabbles with Linux at home in his spare time. He can be reached at firstname.lastname@example.org.