|
楼主 |
发表于 2010-9-10 10:08:16
|
显示全部楼层
用SELinux加强网络安全
[ SELinux(Security-Enhanced Linux) 是美国国家安全局(NAS)对于强制访问控制的实现,是 Linux® 上最杰出的新安全子系统。NSA是在Linux社区的帮助下开发了一种访问控制体系,在这种访问控制体系的限制下,进程只能访问那些在他的任务中所需要文件。SELinux 默认安装在 Fedora 和 Red Hat Enterprise Linux 上,也可以作为其他发行版上容易安装的包得到。本文是一篇翻译稿件,主要介绍了怎样通过SELinux从而加强网络安全,原文地址如下:http://securityblog.org/brindle/ ... rking-with-selinux/
在过去的一年中付出了相当多的努力后使得SELinux对网络支持得更好,感谢伟大的SELinux社区,随着支持的进一步发展,它变得对人们越来越有用,人们愿意尝试使用它并提出反馈意见,最终将变得更加有用,受众也更加广泛,因为在SELinux中网络支持还在不断发展(已经有一部分其它想法正在讨论中,可能今后会被包括进来),我将保持本文的更新,请随时关注。
在SELinux中的网络支持包括很多事情,过去SELinux通过对象类的方式实现细粒度的网络访问控制,这些对象类是网络接口,网络地址和一些套接字对象类(如tcp_socket,udp_socket,rawip_socket等)。
SELinux策略直接标记出了不同种类的网络对象包括接口(使用netifcon),互联网节点(使用nodecon)和端口(使用portcon),策略的语法结构看起来象下面这样:
netifcon eth0 system_ubject_r:external_netif_t system_ubject_r:
external_packet_t
nodecon 192.168.1.1 255.255.255.255 system_ubject_r:external_node_t;
portcon tcp 21 system_ubject_r:ftp_port_t ;
这些被标记了的对象使用正常的SELinux规则就可以访问了:
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 };
策略和规则结合起来实际上指定了ftpd_t绑定到端口21,在端口21上发送和接收tcp消息,在eth0(它的ip地址是192.168.1.1)网卡接口上发送和接收tcp消息,这些规则工作得很好,唯一的问题是它们不能在一条规则中进行描述,因此它们不是连接在一起的。这就意味着如果有另外一套规则指出ftpd_t只能在内部接口上发送和接收dns(名称解析服务)数据包,依靠上面的规则他们也能在外部接口上发送和接收dns数据包,注意这些访问控制也可以用在我稍后要介绍的在SELinux中通过echo 1 > linux/compat_net使用的compat_net选项上。这些对象的标签最后都将增加到libsemanage中,使用semanage命令可以改变它们,但这个命令不能修改策略。
在2006年顶级SELinux开发者研讨会期间,我们讨论了如何使网络标记变得更容易、更多地支持典型的网络约束和大部分重要的精密地绑定接口、地址和端口的方法,幸运地是在Linux中netfilter已经支持这些内容以及一些额外的好处,例如网络连接跟踪可以更精确地限制诸如使用动态端口的ftp这些事情。
可能会想到向iptables中增加一条域分类符来控制更容易一些,因此可能会写一条规则和SELinux域一样定义端口、网络地址、网络接口实现仅允许ftp数据包到达ftpd_t。这可能会用到分布式SELinux策略,但是,可以从SELinux安全服务器移动一部分策略到iptables策略和子系统中去,但我们并不欢迎这样做,因为我们喜欢单一的,集中的,可分析的SELinux策略,实际上让我们使用netfilter和iptables来标记数据包,然后用SELinux规则描述同意或拒绝访问,让我们看看这都是如何使用的:
iptables -A INPUT -t mangle -p tcp --dport 21 -j SECMARK --selctx system_ubject_r:ftp_packet_t
这条语句标记了经过端口21的数据包为ftp_ packet_t,SELinux规则只是简单地允许ftpd_t发送和接收这种类型的数据包:
allow ftpd_t ftp_packet_t : packet { send recv };
但是这相对于传统的网络控制而言并没有做出更新颖的事情来,让我们再来看看更有趣的内容:
iptables -A INPUT -t mangle -p tcp --dport 21 -i eth0 -s 192.168.0.1/24 -j SECMARK –selctx system_ubject_r:ftp_packet_t
这条语句标记了在eth0上,来自192.168.0.1/24,端口21的数据包为ftp_packet_t,因此这就体现了netfilter支持集中处理的好处,另一个iptables规则如下:
iptables -A INPUT -t mangle -m state --state RELATED,ESTABLISHED -j CONNSECMARK –restore
它将为与此相关的数据包进行拷贝,因此当ftp客户端尝试通过相关端口(传输时端口是动态指定的)传输一个文件时将接收到相同的标签,它将运用netfilter的连接跟踪特性来允许ftp仅仅接收与其连接有关的数据包,但这并不需要新增加策略规则或对策略进行修改,因为我们正在对标签进行修改。
这将转变netfilter已经存在的功能,使其能够更精确地指定哪个域能访问哪个数据包,因此它允许你相对于进程而不是机器控制防火墙的功能,如果你在同一台机器上运行有一些不同安全属性的服务那它就有用了,例如:允许你有一个内部的Apache实例访问公司的机密数据,而另外一个外部的Apache实例提供静态的web内容到internet上。
大多数人都在思考在SELinux中为什么不仅仅是提供对网络的支持,而是要在防火墙规则条目中进行网络访问控制,下面的SELinux网络支持就是标记网络通讯。
在SELinux中有两种标记网络通讯的方法:NetLabel,它是CIPSO的实现,另外一种是基于IPSec的标记,我将不讨论NetLabel,因为它仅仅提供了SELinux上下文MLS支持,并且主要是应用于传统的可靠的操作系统如HP-UX和Solaris与SELinux进行协作。基于IPSec的标记条目内容,目前只工作在SELinux系统之间。
因为NetLabel主要为传统的可靠的系统提供MLS支持,我将不讨论它,我将重点介绍基于IPSec的标记。
在这里将涉及到一些IPSec的知识,如果你对它们还不熟悉请参阅IPSec相关文档,尤其要知道什么是SPD和SA,以及它们在IPSec中的角色。
首先你需要准备一个支持IPSec的内核,如果你使用的是redhat的系统就很容易了,可以使用目前Fedora rawhide内核或LSSP内核,LSSP内核可以在
http://people.redhat.com/sgrubb/files/lspp/找到,如果不是redhat的系统,可以升级到2.6.22内核,它包含了IPSec的支持。 你还需要一个ipsec工具用来标记,rawhide或LSSP版本也将在这里用到,一旦你安装好所有需要的东西就可以开始使用IPSec标记了,我们将从简单的标记SA开始。 首先,我有一组程序用来测试并确定所有需要的东西都在工作,它们是一个简单的客户端和连接用的服务端,使用getpeercon()返回网络套接字的上下文。client.c【译者注:内容见附录A】
server.c【译者注:内容见附录B】
编译它们并将其连接到libselinux:
gcc -o client client.c -lselinux
gcc -o server server.c –lselinux 首先尝试在没有IPSec的情况运行它们看会发生什么: 我的测试用机是192.168.147.132(主机名:scarecrow)和192.168.147.130(主机名:poisonivy),很明显,请替换为你的ip地址。 在192.168.147.132上运行服务端和客户端后,在其他机器上的输出类似下面这样:[root@poisonivy ~]# ./client 192.168.147.132
getpeercon: Protocol not available Received: Hello, (null) from (null)
[root@scarecrow ~]# . rver
getsockopt: Protocol not available server: got connection from
192.168.147.130, (null)
getpeercon()返回协议不可用,因为在本次连接中没有标记被启用,你可以根据
错误信息来判断你是否使用了标记网络套接字。 如果我们在这两台机器之间不指定一个上下文而产生一个SA,我们将得到同样的结果:[root@scarecrow ~]# cat dev/ipsec tkey.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 tkey.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; 然后运行:[root@poisonivy ~]# setkey -f dev/ipsec tkey.poisonivy.test
[root@scarecrow ~]# setkey -f dev/ipsec tkey.scarecrow.test 注意:当尝试连接时,要想创建SA两台机器都需要运行racoon。 象之前那样运行服务端和客户端会得到同样的结果,你可以使用setkey –D来查看新创建的SA,注意它是没有标记的SA。 如果我们增加一条-ctx语句到我们的setkey文件中,如:[root@scarecrow ~]# cat dev/ipsec tkey.scarecrow.test
spdflush;
flush;
spdadd 192.168.147.130 192.168.147.132 any
-ctx 1 1 "system_ubject_r:default_t:s0"
-P in ipsec esp/transport//require;
spdadd 192.168.147.132 192.168.147.130 any
-ctx 1 1 "system_ubject_r:default_t:s0"
-P out ipsec esp/transport//require; 你可以使用setkey –DP在每一台机器上查看SPD条目,注意条目的上下文字段。 在其他机器上重复设置setkey文件,注意你不能在标签中使用默认的default_t,它是一个展示发生什么事情的例子,它不是用来创建SA的标签,它将用于一个规则来查看哪个SPD条目与一个域匹配,这将在后面解释。 运行setkey –f收到一个错误的结果:[root@scarecrow ~]# setkey -f dev/ipsec tkey.scarecrow.test
The result of line 8: Permission denied.
The result of line 14: Permission denied. 查看拒绝信息:[root@scarecrow ~]# tail ar/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_ubject_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_ubject_r:default_t:s0 tclass=association 非常好,它告诉我们可以在哪个IPSec SPD上设置标签了,现在在两台机器上设置许可模式并再次运行setkey,将会成功。[root@poisonivy ~]# ./client 192.168.147.132
Received: Hello, root:system_r:unconfined_t:SystemLow-SystemHigh from root:
system_r:unconfined_t:-SystemHigh 你可以使用setkey –D来查看新创建的SA,注意每个条目中上下文的字段。希望下面这张图能传达SA是如何标记的,注意连接的每一边都有2个SA,一个标记入站一个标记出站,出站SA在每个系统上总是本地域,入站SA在每个系统上总是远程域。attachment=9712]
我很感谢Chris Ashworth制作这张图,它比我想象的更简单易懂。 这就论证了当服务端调用getpeercon()时它看到了root:system_r:unconfined_t:SystemLow-SystemHigh,客户端看到了root:system_r:unconfined_t:-SystemHigh,使用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 现在你可以看到服务端调用getpeercon()时的标签是httpd_t,因此现在我们明白了你如何在一个连接的另一端标识进程的上下文了,但是策略如何处理呢?
清除dmesg的内容,设置setenforce 1; setenforce 0,重新开始进程,首先运行setkey,然后运行audit2allow –d,将会显示:allow unconfined_t default_t:association setcontext;
这意味着unconfined_t尝试和default_t一起设置上下文,因为我们在SPD条目中使用了default_t,所以它是预期的结果,运行dmesg –c清除内核缓冲区,并尝试运行服务端和客户端(使用runcon):
移除不相干的规则,在客户端我们可以看到类似下面这样的规则:
allow httpd_t default_t:association polmatch;
这条规则意味着客户端(作为httpd_t运行)企图再次匹配我们之前增加的SPD条目,这个非常有用,因为你可以使用SELinux策略来强制哪个SPD条目与一个域匹配,例如:你想让一些域使用高强度的加密,而另外一些域使用更快速的,低强度的加密,那么你就可以指定哪个SPD类型与哪个域进行匹配。
allow httpd_t self:association sendto;
这条规则指出httpd_t发送数据到标记为httpd_t的团体,意味着它发送到为它创建的团体,注意目前你还不能控制哪个域发送到另一端。
allow httpd_t unconfined_t:association recvfrom;
这是最有效的控制,它告诉我们允许httpd_t接收来自一个标记为unconfined_t的团体的消息,标记为unconfined_t的团体运行在unconfined_t下,这就允许你使用策略来决定其他机器使用哪个域向你本地机器发送数据。
allow unconfined_t default_t:association polmatch;
这是在这台机器上远程域(服务端)再次匹配入站SPD条目,注意有2条SPD条目被增加进来了,一个为出站通讯一个为入站通讯,两个域分别在本地机器上(为出站)和远程机器上(为入站)匹配SPD条目。
可能会在服务端收到类似的拒绝信息,仅仅与之前的颠倒过来了,我不想重提了。使用这个技术你要做到心中有数,假设你控制了两个策略(源和目标),你非常肯定哪个域可以域机器进行通讯,哪个是跨过机器的,与SELinux IPC控制类似,但它是跨网络的。
例如:一个应用了这个技术的应用程序在雇员工作站上有一个内部和外部的浏览器,内部浏览器运行在一个域上,它允许访问公司内部包含了机密的客户信息的web应用服务器,外部浏览器可以访问internet,这就可以减少流氓internet内容泄露你内部数据的风险,这中分离的方式如果没有SELinux高级网络控制支持实现起来将非常困难(甚至是不可能实现的)。
在SELinux网络环境下同时使用IPSec和netfilter技术你将获得惊人的安全,为了更安全,你需要调式更多的信息。
值得特别感谢的是那些为这个技术努力工作的人们,将来他们会使网络架构更安全,这些人包括:James Morris, Venkat Yekkirala, Joy Latten, Paul Moore (实现了NetLabel)等
附录A:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define PORT 3490 // the port client will be connecting to
#define MAXDATASIZE 100 // max number of bytes we can get at once
int main(int argc, char *argv[])
{
int sockfd, numbytes;
char buf[MAXDATASIZE];
struct hostent *he;
struct sockaddr_in their_addr; // connector's address information
security_context_t con = NULL;
if (argc != 2) {
fprintf(stderr,"usage: client hostnamen");
exit(1);
}
if ((he=gethostbyname(argv[1])) == NULL) { // get the host info
herror("gethostbyname");
exit(1);
}
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
their_addr.sin_family = AF_INET; // host byte order
their_addr.sin_port = htons(PORT); // short, network byte order
their_addr.sin_addr = *((struct in_addr *)he->h_addr);
memset(&(their_addr.sin_zero), '', 8); // zero the rest of the struct
if (connect(sockfd, (struct sockaddr *)&their_addr,
sizeof(struct sockaddr)) == -1) {
perror("connect");
exit(1);
}
if ((numbytes=recv(sockfd, buf, MAXDATASIZE-1, 0)) == -1) {
perror("recv");
exit(1);
}
if (getpeercon(sockfd, &con)) {
perror("getpeercon");
}
buf[numbytes] = '';
printf("Received: %s from %sn",buf, con);
close(sockfd);
return 0;
}
附录B:
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MYPORT 3490 // the port users will be connecting to
#define BACKLOG 10 // how many pending connections queue will hold
void sigchld_handler(int s)
{
while(waitpid(-1, NULL, WNOHANG) > 0);
}
int main(void)
{
int sockfd, new_fd; // listen on sock_fd, new connection on new_fd
struct sockaddr_in my_addr; // my address information
struct sockaddr_in their_addr; // connector's address information
socklen_t sin_size;
struct sigaction sa;
int yes=1;
char buf[255];
socklen_t len=254;
security_context_t con = NULL;
if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1) {
perror("socket");
exit(1);
}
if (setsockopt(sockfd,SOL_SOCKET,SO_REUSEADDR,&yes,sizeof(int)) == -1) {
perror("setsockopt");
exit(1);
}
my_addr.sin_family = AF_INET; // host byte order
my_addr.sin_port = htons(MYPORT); // short, network byte order
my_addr.sin_addr.s_addr = INADDR_ANY; // automatically fill with my IP
memset(&(my_addr.sin_zero), '', 8); // zero the rest of the struct
if (bind(sockfd, (struct sockaddr *)&my_addr, sizeof(struct sockaddr))
== -1) {
perror("bind");
exit(1);
}
if (listen(sockfd, BACKLOG) == -1) {
perror("listen");
exit(1);
}
sa.sa_handler = sigchld_handler; // reap all dead processes
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART;
if (sigaction(SIGCHLD, &sa, NULL) == -1) {
perror("sigaction");
exit(1);
}
while(1) { // main accept() loop
sin_size = sizeof(struct sockaddr_in);
if ((new_fd = accept(sockfd, (struct sockaddr *)&their_addr,
&sin_size)) == -1) {
perror("accept");
continue;
}
// if (getsockopt(new_fd, SOL_SOCKET, SO_PEERSEC, con, &len)) {
if (getpeercon(new_fd, &con)) {
perror("getsockopt");
}
printf("server: got connection from %s, %sn",
inet_ntoa(their_addr.sin_addr), con);
if (!fork()) { // this is the child process
close(sockfd); // child doesn't need the listener
sprintf(buf, "Hello, %s", con);
if (send(new_fd, buf, strlen(buf), 0) == -1)
perror("send");
close(new_fd);
exit(0);
}
close(new_fd); // parent doesn't need this
}
return 0; |
5#
2010-9-10 10:08:16
回复(0)
收起回复
|