用iptables隐藏docker映射到host上的端口

docker可以让我们很方便地安装本地服务。但同时,默认的docker的设置使这些端口可以很轻松地从remote访问。
理想的做法是利用nginx把docker 默认打开的端口反向代理到别的端口,然后对新的端口进行用户验证保护。

屏蔽外部端口访问我们很自然而然就想到了使用iptables。

目的

我们在运行某个container的时候,使用了端口映射,例如

1
$ docker docker run --name myservice -p 5000:5000 some_image_name

我们想要仅允许在服务器上通过localhost:5000访问docker container的服务,而屏蔽掉外部通过myhostname:5000来访问服务的request。

错误的尝试

1
$ sudo iptables -A INPUT -p tcp -m tcp --dport 5000 -j ACCEPT

尝试在浏览器输入myhostname:5000,发现依然可以访问到5000端口。

这个是为什么呢?

选择正确的chain

第一次通过简单搜索谷歌,复制粘贴的方法失败了。看来还得静下来看一下iptables这个东西。

这篇文章
很好解释了docker中iptables的用法。

然而iptables的概念太多。我们把范围缩小到filter这个table中。

简要来说,我们通常需要处理的是三条chain(INPUT, FORWARD, OUTPUT)。所有的package 通过相应的chain,chain中的规则进行match。如果有match的规则,则执行规则规定的动作,否则继续向后访问规则,最后最后如果没有匹配的规则,则使用chain的默认的policy来处理。 默认的Policy可以是 ACCEPT 或者 DROP。分别表示接受和丢弃该Package。 被Drop的表现通常是,在浏览器上,显示load中但是总是无法出来结果。

当我们启动docker deamon,挂上docker container的时候,docker会在FORWARD chain中追加叫DOCKER和DOCKER-ISOLATION的自定义CHAIN。

由此可见。我们要追加的规则应该追加在FORWARD chain而不是在INPUT chain。

正确的做法是

1
$ sudo iptables -I FORWARD -p tcp -m tcp --dport 5000 -j ACCEPT

但是注意,docker每次重启之后都会把自定义的DOCKER chain 插到FORWARD chain的第一个。所以,我们不如把这个规则写到DOCKER chain中。

1
$ sudo iptables -I DOCKER -p tcp -m tcp --dport 5000 -j ACCEPT

其他疑惑

  1. 为什么iptables知道特定的外部reqeust需要走的是FORWARD而不是INPUT的chain?

这个是因为在filter table之前由NAT table替换了走向docker的request的destination。并不是所有到本机的请求都是走INPUT chain的,想象一下如果本机是NAT的gateway,那么很显然,大部分package需要转发到下面去。

  1. 为什么经常有两条一摸一样的规则?

光用iptables -L的话会看到几乎完全一样的两行,我们查看具体内容需要iptables -v,这样可以看到in out两个参数,这两个参数分别为in的网卡端口,和out的网卡端口。

"highlight plain">
1
2
3
4
5
6
7
8
root@7c7edc22ff14:/# ps aux |grep logstash
logstash 1 10.2 24.7 4197892 507684 ? Ssl 13:36 2:50 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -XX:+UseParNewGC -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=75 -XX:+UseCMSInitiatingOccupancyOnly -XX:+DisableExplicitGC -Djava.awt.headless=true -Dfile.encoding=UTF-8 -XX:+HeapDumpOnOutOfMemoryError -Xmx1g -Xms256m -Xss2048k -Djffi.boot.library.path=/usr/share/logstash/vendor/jruby/lib/jni -Xbootclasspath/a:/usr/share/logstash/vendor/jruby/lib/jruby.jar -classpath : -Djruby.home=/usr/share/logstash/vendor/jruby -Djruby.lib=/usr/share/logstash/vendor/jruby/lib -Djruby.script=jruby -Djruby.shell=/bin/sh org.jruby.Main /usr/share/logstash/lib/bootstrap/environment.rb logstash/runner.rb -f /etc/logstash/conf.d/
root@7c7edc22ff14:/# cd /access_log
root@7c7edc22ff14:/access_log# ls -la
total 24
drwxrwxr-x 2 1000 1000 4096 Feb 13 13:15 .
drwxr-xr-x 66 root root 4096 Feb 13 13:36 ..
-rw-rw---- 2 www-data adm 15198 Feb 13 13:37 access.log

跑logstash的是用户logstash,而logstash用户没有access.log的访问权限。
在docker container中挂在的目录,其用户权限是和host相同的。所以我们必须在host中增加logstash用户的权限。
退出docker container,在host中我们尝试个logstash用户添加access.log的访问权限。

1
2
$ sudo setfacl -m u:logstash:r access.log
setfacl: Option -m: Invalid argument near character 6

host系统中并没有logstash用户,所以map不到相关的uid。那就去container中找一下logstash的uid就是了。

1
2
3
4
5
root@7c7edc22ff14:/# cat /etc/passwd

...
...
logstash:x:999:999:LogStash Service User:/usr/share/logstash:/usr/sbin/nologin

container中logstash uid是999,我们在host中直接用uid设置权限。

1
$sudo setfacl -m u:999:r access.log

重新build一下,然后docker-compose up, 再打开kibana,这次终于看到了期待的结果。

Docker Elasticsearch Logstash Kibana