docker 可以让我们很方便地安装本地服务。但同时,默认的 docker 的设置使这些端口可以很轻松地从 remote 访问。 理想的做法是利用 nginx 把 docker 默认打开的端口反向代理到别的端口,然后对新的端口进行用户验证保护。
屏蔽外部端口访问我们很自然而然就想到了使用 iptables。
我们在运行某个 container 的时候,使用了端口映射,例如
$ docker docker run --name myservice -p 5000:5000 some_image_name
我们想要仅允许在服务器上通过 localhost:5000 访问 docker container 的服务,而屏蔽掉外部通过 myhostname:5000 来访问服务的 request。
错误的尝试
$ sudo iptables -A INPUT -p tcp -m tcp --dport 5000 -j ACCEPT
尝试在浏览器输入 myhostname:5000,发现依然可以访问到 5000 端口。
这个是为什么呢?
选择正确的 chain
第一次通过简单搜索谷歌,复制粘贴的方法失败了。看来还得静下来看一下 iptables 这个东西。
这篇文章https://securitynik.blogspot.jp/2016/12/docker-networking-internals-how-docker_16.html
很好解释了 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。
正确的做法是
$ sudo iptables -I FORWARD -p tcp -m tcp --dport 5000 -j ACCEPT
但是注意,docker 每次重启之后都会把自定义的 DOCKER chain 插到 FORWARD chain 的第一个。所以,我们不如把这个规则写到 DOCKER chain 中。
$ sudo iptables -I DOCKER -p tcp -m tcp --dport 5000 -j ACCEPT
为什么 iptables 知道特定的外部 reqeust 需要走的是 FORWARD 而不是 INPUT 的 chain? 这个是因为在 filter table 之前由 NAT table 替换了走向 docker 的 request 的 destination。并不是所有到本机的请求都是走 INPUT chain 的,想象一下如果本机是 NAT 的 gateway,那么很显然,大部分 package 需要转发到下面去。
为什么经常有两条一摸一样的规则? 光用 iptables -L 的话会看到几乎完全一样的两行,我们查看具体内容需要 iptables -v,这样可以看到 in out 两个参数,这两个参数分别为 in 的网卡端口,和 out 的网卡端口。