Secure Networking with SELinux

During the last year quite a bit of effort has gone into improving SELinux’ networking support, thanks to the great SELinux community. While this support is still evolving it will be very beneficial for people to try it out and give feedback so the final result is useful to more users and meets the security needs of a wider audience. As the network support in SELinux continues to evolve (there are already other ideas being discussed for possible inclusion) I’ll try to keep this post updated so that people who find it will have the latest information available.

Network support in SELinux means many things. In the old days (“old days” is very relative since it was only a few kernel releases back at this point) SELinux had fine grained network access control by way of many object classes. These object classes were netif (network interfaces), node (network addresses) and a handful of socket object classes (such as tcp_socket, udp_socket, rawip_socket and so on).

The SELinux policy directly labeled several kinds of network objects including interfaces (using netifcon), internet nodes (using nodecon) and ports (using portcon). The policy statements looked something like these:

netifcon eth0 system_u:object_r:external_netif_t system_u:object_r:external_packet_t

nodecon 192.168.1.1 255.255.255.255 system_u:object_r:external_node_t; portcon tcp 21 system_u:object_r:ftp_port_t ;

These labeled objects could then be given access to with normal SELinux rules:

allow ftpd_t external_netif_t : netif {tcp_send tcp_recv };

allow ftpd_t external_node_t : node { tcp_send tcp_recv };

allow ftpd_t ftp_port_t : tcp_socket { send_msg recv_msg name_bind };

These basically gave ftpd_t the ability to bind to port 21, send and receive tcp messages on port 21 and send and receive tcp messages on eth0 (whose ip was 192.168.1.1). These worked fine, the only problem is that they weren’t specified in a single rule so they weren’t coupled. This means if there was another set of rules that let ftpd_t send and receive dns packets (for name resolution) only on the internal interface they could also send and receive dns packets on the external interface by virtue of the rules above. Note that these access controls can still be used en lieu of the ones I’m going to talk about next by setting the compat_net option in SELinux with echo 1 > /selinux/compat_net.

The labeling of these objects was eventually added to libsemanage and could be changed on end systems using the semanage command without modifying policies.

During the 2006 SELinux Symposium Developer Summit we discussed ideas on making the network labeling easier to use, more able to support typical network restrictions and most importantly to closely bind the interface, address and port. Fortunately for us netfilter in Linux already supports all these things as well as some additional benefits like connection tracking that can be used to more precisely restrict things that use dynamic ports such as ftp.

One may be thinking that the easiest way to handle this is to add a domain specifier to iptables so that one could write a rule that matched the SELinux domain as well as the ports, network interface and addresses to allow only ftp packets to reach ftpd_t. This would have decentralized the SELinux policy, however, by moving part of the decision making out of the SELinux security server and into the iptables policy and subsystem. This is generally undesirable as we’d like to keep a single, centralized, analyzable SELinux policy. That essentially left us with using netfilter and iptables to label packets, which would have SELinux allows rules written to allow or deny access. Lets look at how this might be used:

iptables -A INPUT -t mangle -p tcp --dport 21 -j SECMARK --selctx system_u:object_r:ftp_packet_t

This labels packets destined for port 21 as ftp_ packet_t. The SELinux rule that allows ftpd_t send and receive packets of this type is simply:

allow ftpd_t ftp_packet_t : packet { send recv };

But this doesn’t do anything more than the old network controls did, lets look at something more interesting:

iptables -A INPUT -t mangle -p tcp --dport 21 -i eth0 -s 192.168.0.1/24 -j SECMARK --selctx system_u:object_r:ftp_packet_t

This labels only packets on eth0, coming from 192.168.0.124 and on tcp port 21 as ftp_packet_t. So this has the advantage of being able to couple anything that netfilter supports together. Another iptables rule such as:

iptables -A INPUT -t mangle -m state --state RELATED,ESTABLISHED -j CONNSECMARK -restore

will copy the label for related packets so when an ftp client attempts a file transfer the related port (which is dynamically assigned at the time of the transfer) will receive the same label. This takes advantage of netfilter’s connection tracking features to allow ftp to only receive packets related to its connections. This requires no additional policy rules or policy modifications of any kind since the labels are all we are changing.

What this brings over netfilter’s existing functionality is the ability to specify precisely which domains can access which packets, so it allows you to bring the firewall functionality all the way to the processes rather than just to the machine, which is significant if you have several services of differing security properties running on the same machine. This would, for example, allow you to have an internal Apache instance with access to company confidential data to only be accessible internally and another external Apache instance that serves static web content to the internet.

While most people think of network access controls in the terms of firewalls alone this is not the only network support in SELinux. The next kind of SELinux network support is labeled networking.

There are two methods of labeling network traffic in SELinux: NetLabel, which is an implementation of CIPSO, and IPSec based labeling. I’m not going to talk about NetLabel because it only supports the MLS portion of the SELinux context and is primarily useful for making SELinux cooperate with legacy trusted operating systems like Trusted HP-UX and Trusted Solaris. IPSec based labeling sends the entire context but currently only works between SELinux systems.

As NetLabel is primarily for supporting MLS and legacy trusted systems I’m not going to talk about it, lets instead move to IPSec based labeling.

I’m going to assume some knowledge of IPSec here so if you are unfamiliar with some of the terms please consult IPSec documentation. In particular you need to know what SPD’s and SA’s are and what their role in IPSec is.
The first thing you need is a kernel that supports IPSec labeling. The easiest way (if you are using Red Hat) is to use the current Fedora rawhide kernel or the LSPP kernel. The LSPP kernel is available at http://people.redhat.com/sgrubb/files/lspp/. For the non-Red Hat users in the audience labeled IPSec is also available in the released version of linux-2.6.22.

You’ll also need an ipsec-tools that is capable of labeling, rawhides or the LSPP version will do here as well. Once you have everything you need installed you can start using labeled IPSec. We’ll start with simple labeled SA’s.

First I have a couple programs that will help test and ensure that everything is working. They are simple a simple client and server that connect, use getpeercon() to return what the context of the network socket is.

client.c

server.c

Build these by linking them against libselinux:

gcc -o client client.c -lselinux

gcc -o server server.c -lselinux

First lets try running them without IPSec to see what happens:

My test machines are 192.168.147.132 (scarecrow) and 192.168.147.130 (poisonivy), obviously replace with your addresses.

After running server on 192.168.147.132 and running client 192.168.147.132 on the other machine the output should look like this:

[root@poisonivy ~]# ./client 192.168.147.132

getpeercon: Protocol not available Received: Hello, (null) from (null)

[root@scarecrow ~]# ./server

getsockopt: Protocol not available server: got connection from 192.168.147.130, (null)

getpeercon() returns protocol not available because no labeling was enabled on this connection, you can use the error to determine if you are using a labeled network socket or not.

If we make an SA between these 2 machines without specifying a context we’ll get the same results:

[root@scarecrow ~]# cat dev/ipsec/setkey.scarecrow.test

spdflush;

flush;

spdadd 192.168.147.130 192.168.147.132 any

-P in ipsec esp/transport//require;

spdadd 192.168.147.132 192.168.147.130 any

-P out ipsec esp/transport//require;

[root@poisonivy ~]# cat dev/ipsec/setkey.poisonivy.test

spdflush;

flush;

spdadd 192.168.147.132 192.168.147.130 any

-P in ipsec esp/transport//require;

spdadd 192.168.147.130 192.168.147.132 any

-P out ipsec esp/transport//require;

Then run:

[root@poisonivy ~]# setkey -f dev/ipsec/setkey.poisonivy.test

[root@scarecrow ~]# setkey -f dev/ipsec/setkey.scarecrow.test

NOTE: both machines will need to be running racoon in order to create SA’s when the connections are attempted.

Running server and client the same as before will give the exact same results. You can use setkey -D to see the newly created SA’s. Notice that the SA’s are not be labeled.

If we add a -ctx statement to our setkey files such that:

[root@scarecrow ~]# cat dev/ipsec/setkey.scarecrow.test

spdflush;

flush;

spdadd 192.168.147.130 192.168.147.132 any

-ctx 1 1 "system_u:object_r:default_t:s0"

-P in ipsec esp/transport//require;

spdadd 192.168.147.132 192.168.147.130 any

-ctx 1 1 "system_u:object_r:default_t:s0"

-P out ipsec esp/transport//require;

You can use setkey -DP to see the SPD entries on each machine, note the context field in the entries.
Repeat this with the other machines setkey file. Note that you’d never use default_t in a label, this is an example to show what happens. This is _NOT_ the label that will be used to create SA’s, this label will be used in a ‘polmatch’ rule to see which SPD entries a domain can match to. This will be explained later.

Running setkey -f results in an error now:

[root@scarecrow ~]# setkey -f dev/ipsec/setkey.scarecrow.test

The result of line 8: Permission denied.

The result of line 14: Permission denied.

Looking for the denial:

[root@scarecrow ~]# tail /var/log/audit/audit.log | grep AVC

type=AVC msg=audit(1179150979.762:33): avc: denied { setcontext } for pid=21632 comm="setkey" scontext=root:system_r:unconfined_t:s0-s0:c0.c1023 tcontext=system_u:object_r:default_t:s0 tclass=association type=AVC msg=audit(1179150979.895:34): avc: denied { setcontext } for pid=21632 comm="setkey" scontext=root:system_r:unconfined_t:s0-s0:c0.c1023 tcontext=system_u:object_r:default_t:s0 tclass=association

This is good, it means that we can protect who sets the labels on IPSec SPD’s. For now set permissive mode on both machines and run setkey again, it should now succeed.

Run the client and server again and you should see something like this on the client side:

[root@poisonivy ~]# ./client 192.168.147.132

Received: Hello, root:system_r:unconfined_t:SystemLow-SystemHigh from root:system_r:unconfined_t:-SystemHigh

You can use setkey -D to see the newly created SA’s and note the context field in each entry.
Hopefully this image conveys how the SA’s are labeled, note that each side of the connection has 2 SA’s, one incoming and one outgoing with the appropriate labels. The outgoing SA for each system is always the local domain, the incoming SA for each system is always the remote domain.
labeled ipsec
I’d like to thank Chris Ashworth for the graphic, it is much simpler than mine and I think gives a better understanding of the labeled tunnels.

This demonstrates that the server sees root:system_r:unconfined_t:SystemLow-SystemHigh when it calls getpeercon() and the client sees root:system_r:unconfined_t:-SystemHigh. To show the difference more specifically run the client with a different context using runcon:

[root@poisonivy ~]# runcon -t httpd_t ./client 192.168.147.132

Received: Hello, root:system_r:httpd_t:SystemLow-SystemHigh from root:system_r:unconfined_t:-SystemHigh

Now you can see the server sees httpd_t when it calls getpeercon(). So now we’ve seen how you can identify the process context on the other side of a connection, but what about the policy?

Lets clear out dmesg and setenforce 1; setenforce 0; to clear the AVC and start the process over.

First running setkey and then audit2allow -d should show:

allow unconfined_t default_t:association setcontext;

This means that unconfined_t (what your shell normally runs as) tried to set the context of an association to default_t. This is expected since we used default_t in our SPD entry. Run dmesg -c to clear the kernel buffer and try to run the server and client (with runcon):

Removing the irrelevant rules we should see something like this on the client side:

allow httpd_t default_t:association polmatch;

This rule means that the client (running as httpd_t) attempted to match against the SPD entry we added earlier. This is useful because you can use SELinux policy to enforce which SPD entries a domain may match against. For example, if you wanted to have some domains use high quality encryption and let the others use a faster, lower quality encryption you can do that by specifying which SPD types a domain may polmatch against.

allow httpd_t self:association sendto;

This is httpd_t sending data to an association labeled httpd_t. This basically just means that it is sending to the association created for it. Note that with the current controls you can not control which domains you are sending to on the other side.

allow httpd_t unconfined_t:association recvfrom;

This is the most significant control; it says that we are allowing httpd_t to receive from an association labeled unconfined_t, which is the result of the server running in unconfined_t. This lets you use policy to determine which domains on another machine can send data to which domains on the local machine.

allow unconfined_t default_t:association polmatch;

And this is the remote domain (the server) matching against the income SPD entry on this machine. Note that there were 2 SPD entries added, one for outgoing communication and one for incoming communication, both domains have to polmatch the SPD’s on the local machine (for outgoing) and the remote machine (for incoming).

There should be similar denials on the server side, only reversed so I won’t go over those.

As you can see you can do a significant amount with this technology, assuming that you control both policies (source and destination) you can be very certain which domains are talking to which domains across machines, similar to the SELinux IPC controls but across a network!

For example, one possible application of this technology is to have an ‘internal’ and ‘external’ browser on employee workstations. The internal browser would run in a domain that is allowed to access internal company web application servers that contain confidential customer information while the external browser can access the internet. This reduces the risk that rogue internet content can compromise your internal data. This kind of separation would be much more difficult (or impossible) without SELinux’ advanced networking controls.

Using both labeled IPSec and netfilter secmark you can do some pretty amazing things with SELinux networking, and its only going to get better, stay tuned for more information.

A special thanks goes out to all the people that worked hard on getting this technology where it is now and the same ones trying to push it even further to have the best secure networking infrastructure around. These people include (in no particular order): James Morris, Venkat Yekkirala, Joy Latten, Paul Moore (who implemented NetLabel), and many others.