Moby: docker swarm模式:127.0.0.1上的端口暴露于0.0.0.0

创建于 2017-04-02  ·  53评论  ·  资料来源: moby/moby

描述

在docker群模式下,将端口绑定到127.0.0.1会导致端口也在0.0.0.0上打开。 这可能是一个严重的安全问题,应在文档中进行说明。

重现此问题的步骤:

  1. 在docker-compose.swarm.yml文件中创建一个服务(例如MongoDB),并将端口27017发布到localhost:
  mongodb:
    image: mongo:3.2
    volumes:
      - ./persistent-data/mongodb:/data
      - ./persistent-data/mongodb/db:/data/db
    networks:
      data:
        aliases:
          - mongo.docker
    logging:
      driver: syslog
      options:
        syslog-address: "udp://10.129.26.80:5514"
        tag: "docker[mongodb]"
    ports:
      - "127.0.0.1:27017:27017"
    deploy:
      placement:
        constraints: [node.labels.purpose == main-data]
  1. 部署群
  2. 使用netcat检查端口是否从集群外部打开

描述您收到的结果:

nc -vz PUBLIC_NODE_IP 27017
found 0 associations
found 1 connections:
[...]
Connection to PUBLIC_NODE_IP port 27017 [tcp/*] succeeded!

描述您期望的结果:
该端口在127.0.0.1上可用,至少在运行此服务的群集节点中可用。

您认为重要的其他信息(例如,问题仅偶尔发生):

docker version

Docker version 17.03.1-ce, build c6d412e

docker info

群管理器的docker信息:

Containers: 0
 Running: 0
 Paused: 0
 Stopped: 0
Images: 1
Server Version: 17.03.1-ce
Storage Driver: aufs
 Root Dir: /var/lib/docker/aufs
 Backing Filesystem: extfs
 Dirs: 3
 Dirperm1 Supported: true
Logging Driver: json-file
Cgroup Driver: cgroupfs
Plugins: 
 Volume: local
 Network: bridge host macvlan null overlay
Swarm: active
 NodeID: pk7ulemi0z0chgtsg0azfrjz5
 Is Manager: true
 ClusterID: 27etomlyjvtmygrm6rcdgr2ni
 Managers: 1
 Nodes: 6
 Orchestration:
  Task History Retention Limit: 5
 Raft:
  Snapshot Interval: 10000
  Number of Old Snapshots to Retain: 0
  Heartbeat Tick: 1
  Election Tick: 3
 Dispatcher:
  Heartbeat Period: 5 seconds
 CA Configuration:
  Expiry Duration: 3 months
 Node Address: 10.129.26.165
 Manager Addresses:
  10.129.26.165:2377
Runtimes: runc
Default Runtime: runc
Init Binary: docker-init
containerd version: 4ab9917febca54791c5f071a9d1f404867857fcc
runc version: 54296cf40ad8143b62dbcaa1d90e520a2136ddfe
init version: 949e6fa
Security Options:
 apparmor
 seccomp
  Profile: default
Kernel Version: 4.4.0-64-generic
Operating System: Ubuntu 16.04.2 LTS
OSType: linux
Architecture: x86_64
CPUs: 1
Total Memory: 992.4 MiB
Name: <HIDDEN>
ID: IMOK:QIR7:WU5Y:WTPP:EPRQ:F77G:ULGE:WOG4:O7S7:6AFE:V7QG:2XEK
Docker Root Dir: /var/lib/docker
Debug Mode (client): false
Debug Mode (server): false
Username: <HIDDEN>
Registry: https://index.docker.io/v1/
WARNING: No swap limit support
Experimental: false
Insecure Registries:
 127.0.0.0/8
Live Restore Enabled: false

其他环境详细信息(AWS,VirtualBox,物理等):
在Digital Ocean的飞沫上进行了测试。

areswarm kinbug

最有用的评论

令我感到非常失望的是,将近两年的某些Docker开发人员在必须具备该功能的情况下忽略了一个有效且非常有用的用例:当您要将包含关系的数据库作为群服务绑定到本地接口以便通过SSH隧道安全地访问它时。 目前,这种情况是不可能做到的。

所有53条评论

是的,这应该会输出错误; 服务(默认情况下)使用“进入”网络“发布”,并且不支持指定IP地址,因为无法预测它们最终指向哪个_node_(因此不知道可用的IP地址-尽管127.0 .0.1可能)。 此问题正在跟踪该功能https://github.com/docker/docker/issues/26696 (并且此“史诗”跟踪服务尚未(但)支持的其他选项https://github.com/docker/docker/issues / 25303)

这里的错误是docker应该产生一个错误,而不是默默地忽略该选项。 使用此最小的docker-compose文件可重现;

version: "3.2"
services:
  mongodb:
    image: nginx:alpine
    ports:
      - "127.0.0.1:27017:80"

ping @dnephin @vdemeester

@ fer2d2在群体模式下,如果发布某些内容( ports表示stack deploy ),则该内容会发布在ingress网络上,因此是公开的。 有几种解决方法,但是在上面加上kind/bug ,因为使用带有该符号的端口执行stack deploy时,我们至少应该警告人们有关此的信息(即host:port:port )。

要变通解决此问题,有几种方法:

  • 首先,仅当您希望将其公开时才应发布mongo端口,否则,它可以通过docker中的名称发现捆绑包使用(同一网络上的另一个容器/服务将能够通过mongo来访问它)
  • 如果要在主机中而不是在ingress发布它(因此,请不要使用集群模式,而不是在没有运行集群的情况下将它们公开发布,而是在不运行集群的情况下公开),则需要使用端口扩展语法
    ports:
      - mode: host
        target: 80
        published: 9005

它的作用与docker run -p 80:9005 …相同,因此它将绑定到0.0.0.0 ,但仅限于主机。

但是正如@thaJeztah所说,“这里的错误是

/ cc @mavenugo @aboch ,看看是否有一种方法可以将其绑定到特定的ip? (因为节点的ip会有所不同,所以很难实现)。

@vdemeester我可以使用这种表示法将localhost指定为主机目标吗?

    ports:
      - mode: host
        target: 127.0.0.1:80
        published: 9005

由于它是端口配置的扩展格式,因此应该可以正常工作。

提前致谢

长目标语法似乎将目标和发布都强制为整数类型

如果您通过SSH隧道连接到某些服务,我认为这不是理想的行为。 例如,如果要在127.0.0.1上安装MySQL或MongoDB服务器并通过SSH隧道进行连接,则必须使用Docker Swarm在0.0.0.0上公开数据库端口或在内部运行SSH的情况下创建自定义数据库容器(以及两个选项)非常不安全)。

由于这种限制(特定的接口绑定),许多使用SSH隧道的数据库客户端(例如SQL Workbench或Robomongo)无法使用。

我们公司中的问题与@ fer2d2相同,试图通过ssh隧道将Mongobooster与docker swarm连接。 我们发现的唯一解决方案是打开27017端口并使用用户名和密码保护数据库。

任何新闻?

+1

+1

允许ip_ address:port对进行长格式端口映射的另一种用例是用于任意播地址或任何其他可能与回送相关的地址。 这些地址类似于127.0.0.1地址,因为它们仅在环回网络上可见。 限于具有此属性的节点的服务可能希望仅在任意播地址上公开端口,以避免端口冲突,同时避免iptables规则进行端口转换。

当您指定时,它可能是一个选项吗?

placement:
        constraints:
          - node.id ==

干杯

+1

+1

+1

我自己解决了这个问题,所以:

iptables -I DOCKER-USER -i eth0 -j DROP
iptables -I DOCKER-USER -m state --state RELATED,ESTABLISHED -j ACCEPT

泊坞窗不触及这些规则。 只是添加自己的
-A DOCKER-USER -j RETURN
结果,尽管该端口侦听0.0.0.0,但无法从外部接口eth0访问

此行为违反了“默认情况下是安全的”,并且在文档中添加注释还不够好。 目前,它应该导致错误。

它还与模式有关:入口/主机(这两个问题在讨论中似乎混淆了)。 关于入口模式,没有什么应该阻止服务绑定到所有节点上的本地地址,而不是绑定到外部地址。 因此,应允许使用127.xxx。 (在非群模式下(使用docker run),我绑定到127.0.0.2:80和127.0.0.3:80等。以在开发中本地测试多台服务器。)

另一个问题是进入模式是默认模式。 这是意外的,并且还导致安全问题。 我只是试图在受限于网络私有部分的节点上启动服务,端口绑定到127.0.0.3:80。 然后,它也绑定到公共节点的公共接口。 (那是无声地忽略IP地址,而无声地使用入口模式,并且我的数据是公开的)。

用例

  • 影响我的用例(绝对真实)

    • 1绑定到特定节点的端口,因为其他节点正在使用该端口。 您可以为此使用主机模式,但是默认设置是令人惊讶的。

    • 2绑定到特定节点的端口,因为其他节点具有公共接口。 您可以为此使用主机模式,但是默认设置违反了“默认安全”

    • 3在本地绑定,因为您不希望它对其他主机可见,默认情况下违反了“默认安全”

    • 4绑定到127.0.0.3,因为您的开发机器上有很多东西,并且此端口正在使用127.0.0.1。 并使用/etc/hosts ,以便将每个域名发送到不同的容器。 这适用于docker run ,但不适用于compose。

  • 其他用例

    • 绑定到特定接口,例如192.168.0.x,因为这是一个内部网络。 默认违反“默认安全”

    • 绑定到特定的特定节点,但不限制服务在该节点上运行。 这与1或2类似,但没有使用约束。 交通将通过群路由。

所以总结

  • 忽略IP地址并将其绑定到0.0.0.0以及默认的进入模式都违反了“默认安全”。 如果指定了IP地址并更新了文档,则应该发出错误消息。 如果未指定模式(无默认值)并且文档已更新,则应该发出错误。 (这解决了模式问题,并消除了意外的安全问题。)
  • 然后可以在主机模式下添加对IP地址的支持。
  • 可以添加对Ingress模式下IP地址的支持(仅限本地地址127.xxx)。 (不同的本地地址,例如127.0.0.2和127.0.0.3,应视为不同的地址(仅传递给OS))。

允许绑定到本地地址对于受约束的节点很有用。 允许绑定到特定的地址,适用于受约束的节点,或者通过群集路由到节点之一上的地址之一(可能仅是入口模式)。 此路由已完成

@ richard-delorenzi Moby当前甚至不接受主机IP。 因此,在功能请求之外,这听起来像是一个客户端问题……具体来说就是如何在Docker CLI中翻译compose yaml。

入口的工作方式已被很好地记录下来,但是同意这是CLI上的不良行为。

+1

+1

+1

我有一种使用的解决方法。 我运行独立的容器,并将它们连接到名为“核心”的网络,该网络被一群内部运行的所有后端服务(mongo,elasticsearch,influxdb等)使用。

我看不到如何在撰写文件中执行此操作,因此我们只在运行独立容器,如下所示:

docker run --name kibana --rm -d -v /var/lib/kibana:/usr/share/kibana/config -p 127.0.0.1:5601:5601 --network core docker.elastic.co/kibana/kibana:6.1.2

docker run --name chronograf --rm -d -v /var/lib/chronograf:/var/lib/chronograf -p 127.0.0.1:8888:8888 --network core chronograf:1.4 chronograf --influxdb-url=http://influxdb:8086

启动这些容器后,docker ps显示新容器已绑定到127.0.0.1。 阿们然后,我可以从本地工作站隧道连接到Docker主机以进行安全访问,如下所示:

ssh -i my_ssh_key.pem [email protected]  -L 8888:localhost:8888  -L 5601:localhost:5601 -N

然后,可以从浏览器连接到http:// localhost :8888或http:// localhost :5601

为我工作。

如果UNIX套接字可以代替127.0.0.1 TCP / IP套接字,则可以在此处获得我为流利的位实现的一种可能的解决方法。

也许在mode添加另一个选项可能会有所帮助。 除了hostingress之外,还类似于local ingress

请删除“可用的安全性:Moby提供安全的默认设置而不损害可用性的措辞”。 在Moby自述文件上。 这绝对是虚假广告,请参阅@ richard-delorenzi的评论。

服务默认情况下不发布端口,因此除非您指定它们应发布端口,否则将无法访问这些服务。 当前不支持绑定到特定的IP地址。 如果您的服务不可访问,请不要发布端口,并使用内部(覆盖)网络连接到该服务。

https://github.com/moby/moby/issues/26696中讨论了添加对绑定到IP地址的支持,但是实现起来并不容易(考虑到非“ localhost” IP地址)

部署堆栈时添加了警告;

docker stack deploy -c- test <<'EOF'
version: '3'
services:
  web:
    image: nginx:alpine
    ports:
      - "127.0.0.1:8080:80"
EOF

WARN[0000] ignoring IP-address (127.0.0.1:8080:80/tcp) service will listen on '0.0.0.0' 
Creating network test_default
Creating service test_web

并且,当尝试使用指定的IP地址部署服务时,它将无法部署,并会显示错误消息。

docker service create -p 127.0.0.1:9090:80 nginx:alpine
invalid argument "127.0.0.1:9090:80" for "-p, --publish" flag: hostip is not supported
See 'docker service create --help'.

@dalu,如果您的系统暴露在互联网上,并且您告诉Docker在群集中暴露服务,我不确定为什么会有其他期望。

毫无疑问,这种构成格式的开发工作很困难,实际部署中存在一些重大妥协。

@ cpuguy83

如果您的系统暴露在互联网上,并且您告诉Docker在群集上暴露服务,我不确定为什么会有其他期望。

不。 如果有人将其绑定到非公共IP(例如127.0.0.1或10.0.0.0),为什么它应该是公共的? 实际上,这是正确的答案:

当前不支持绑定到特定的IP地址

@dalu

但是应该可以访问,但不能公开访问。 这就是这里的全部内容。
默认情况下,您是不安全的,并且逃避了带有语义的修复。
该问题已经开放了将近2年,而没有适当的解决方案。

我正在从集群过渡到kubernetes,因为集群无法使用。 我对这个决定感到非常满意,即使这种过渡非常昂贵。

@Bessonv随便告诉您“我忽略了这个”

问题在于compose格式是为开发环境设计的,已被推送以支持集群部署。 “ docker stack”应该只是出错了,但是后来人们希望能够使用一个撰写文件来将它们全部规则化,因此存在混乱。

@ cpuguy83
我不确定我对这个描述是否满意。 最后,撰写格式只是所需状态的描述。 在单台计算机(组成)和集群(群集)之间具有一些差异是完全可以的。 从我的角度来看,完全没有必要支持撰写。 特别是因为启用群体模式非常容易。 但这需要修复群。

问题根本就不存在于集群中,而是100%构成格式+ docker cli中的实现。
请注意,栈当前是100%的客户端实现。

我们发现,在堆栈内部,您不必显式公开内部服务(例如数据库,redis等)的任何端口。只需省略内部服务

堆栈内部的示例数据库服务

services:
  db:
    image: postgres:11-alpine
  networks:
    - backend

...可以由Django app服务默认端口使用,如下所示:

DATABASES = {
    'default': env.db(default='postgres://user:pass<strong i="13">@db</strong>:5432/catalog'),
}

因此,在这种情况下,当您仅公开显示公共服务时,默认情况下看起来像

问题根本就不存在于集群中,而是100%构成格式+ docker cli中的实现。
请注意,栈当前是100%的客户端实现。

无论如何:我停止使用堆栈(由于此问题),并且不再在意。 怪图书馆,怪码头工人,怪我猫。

直接使用docker或使用compose时,我没有看到此问题。

看起来这种方法可以提供帮助(应该在swarm中的每个节点上执行):

  1. 离开群
  2. 删除网络docker_gwbridge
  3. 使用附加选项com.docker.network.bridge.host_binding_ipv4 = IP重新创建网络docker_gwbridge
  4. 加入群聚
    适用于以“主机”模式发布的端口。 如果没有模式“主机”入口网络,则与其他驱动程序和作用域“群”一起使用。

可怕的解决方案:

$ mv /usr/bin/docker-proxy /usr/bin/docker-proxy-original
$ cat << 'EOF' > /usr/bin/docker-proxy
#!/bin/sh
exec /usr/bin/docker-proxy-original `echo $* | sed s/0.0.0.0/127.0.0.1/g`
EOF
$ chmod 755 /usr/bin/docker-proxy
$ service docker restart

@jsmouret我什至找不到最新的

看起来要看...

$ apt-file search docker-proxy
docker-ce: /usr/bin/docker-proxy
docker.io: /usr/sbin/docker-proxy

应该以某种方式在文档中记录此行为。
当前,它只是从短端口映射中忽略主机。 并且默默地行不通。

另一个奇怪的事情是您不能在长语法模式中设置主机。

此行为应在文档中以某种方式进行记录。

我同意; 我以为它在该页面的某处提到过,但是找不到。 随时在docs资料库中打开问题; https://github.com/docker/docker.github.io/issues

当前,它只是从短端口映射中忽略主机。 并且默默地行不通。

您正在使用哪个版本的Docker? 它应该打印警告(使用docker stack deploy )或_error_(使用docker service create ); 参见https://github.com/moby/moby/issues/32299#issuecomment -472793444

您正在使用哪个版本的Docker? 它应该打印警告(使用docker stack deploy时)或错误(使用docker service create时);

gh,看来是我的错。 当我尝试从控制台部署堆栈时,它确实做到了。
以前,我是通过portainer UI进行操作的,它没有显示任何错误或警告。

令我感到非常失望的是,将近两年的某些Docker开发人员在必须具备该功能的情况下忽略了一个有效且非常有用的用例:当您要将包含关系的数据库作为群服务绑定到本地接口以便通过SSH隧道安全地访问它时。 目前,这种情况是不可能做到的。

一个可行的,干净的解决方案正在另一个容器中运行SSH服务器,该容器与数据库连接到相同的docker网络。 然后可以将SSH端口发布到主机上(当然,端口也要不同于22),因此您可以通过SSH容器转发到数据库。

@nartamonov我看不到除非协议本身是安全的,否则如何从入口安全地完成此操作。
安全地访问它的方法是通过加密的数据平面(对于过度的网络为--opt encrypted ),并使用与该网络连接所需的任何工具来旋转容器。

这可能还有其他无关的副作用,但是在/etc/docker/daemon.json中设置"iptables": false /etc/docker/daemon.json也可以解决此问题。 不太麻烦的解决方案是只添加自定义规则,例如

无论哪种方式,我都希望在3年后看到更多对此的支持。

看起来这种方法可以提供帮助(应该在swarm中的每个节点上执行):

1. leave swarm

2. remove network docker_gwbridge

3. recreate network docker_gwbridge with additional option com.docker.network.bridge.host_binding_ipv4=IP

4. join swarm back
   Works for ports published in mode "host". Without mode "host" ingress network is used with other driver and scope "swarm".

@ienovytskyi
如果我没记错的话,这会使所有已发布的端口绑定到给定的默认IP地址? 需要明确的是,如果您只想为某些服务的某些端口限制绑定接口,则这不是可用的解决方法。

我想报告我的解决方法。

用例:
群中的某些服务需要在所有接口上侦听,或者至少在公共接口上侦听-在我的示例中,此容器是反向代理
在这些群集节点中,每个节点上还有一个数据库实例,它们使用定义为的群集网络:

docker network create --scope swarm NETWORK_NAME --attachable -d overlay

当然,需要数据库连接的Web服务需要加入该NETWORK_NAME

出于管理目的,有时有必要直接连接到数据库

解:
只有需要在所有网络上公开的服务(在我的示例中为反向代理)才能在其服务定义中包含ports: ['SOMEPORT:ANOTHERPORT']

所有其他服务都需要在主机上具有配对的docker non-swarm容器。
该非群集容器会将NETWORK_NAME/nodeXYZ:port上存在的端口桥接到localhost

mongodb的示例:

docker run --rm -it --net=NETWORK_NAME -d --name expose-mongo -p 127.0.0.1:27017:47017 alpine/socat tcp-listen:47017,fork,reuseaddr tcp-connect:mongo01:27017

缺点:每个群集节点都应有一个非群集容器,因此,除非采用ansible / heavy脚本,否则许多节点确实很无聊

@ fer2d2提到的“如果您通过SSH隧道连接到某些服务”的问题,我的解决方法是使用

FROM alpine

RUN apk add --no-cache openssh
RUN mkdir ~/.ssh
RUN ssh-keygen -A
RUN echo "root:root" | chpasswd
RUN echo 'PasswordAuthentication no' >> /etc/ssh/sshd_config
RUN echo 'Port 22' >> /etc/ssh/sshd_config
RUN echo -e " \
Match User root \n\
  AllowTcpForwarding yes\n\
  X11Forwarding no\n\
  AllowAgentForwarding no\n\
  ForceCommand /bin/false\n\
" >> /etc/ssh/sshd_config

EXPOSE 22
CMD /usr/sbin/sshd -D -e "$@"

然后在docker-compose.yml中:

...
  db:
    image: mysql:5.6
    environment:
      MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:?err}
      MYSQL_ROOT_HOST: '%'
    volumes:
      - "./mysql:/var/lib/mysql"
    deploy:
      placement:
        constraints: [node.role == manager]

  sshd:
    image: maxisme/sshd:latest
    volumes:
      - "~/.ssh:/root/.ssh"
    ports:
      - "2223:22"
    deploy:
      placement:
        constraints: [node.role == manager]

这允许我将我的authorized_keys添加到~/.ssh文件夹,然后ssh代理使用db主机名通过端口2223跳转到我的数据库。

一个可行的,干净的解决方案正在另一个容器中运行SSH服务器,该容器与数据库连接到相同的docker网络。 然后可以将SSH端口发布到主机上(当然,端口也要不同于22),因此您可以通过SSH容器转发到数据库。

有效

为什么此功能很重要的另一个示例。
我有一台安装了plesk的服务器,plesk已经有了它的配置,但是我可以添加另一个配置只是指向docker swarm服务。 该plesk服务器是一个群集节点。
我想使用plesk将proxy_pass传递到端口。 应该发布此端口,因为容器位于覆盖网络中,但是它需要一个外部端口才能与外界通信。

因此proxypass应该指向一个本地接口,例如127.0.0.1:someport
并且群集中的容器应仅将端口发布到localhost。

这样,只能通过proxypass而不是直接从世界访问容器端口

我喜欢您的解决方法@maxisme ,但是如何管理authorized_keys所有权? 在OS X上,它适合我(挂载属于root ),但是在生产Linux机器上,我得到:

Authentication refused: bad ownership or modes for file /root/.ssh/authorized_keys
Connection closed by authenticating user root 85.145.195.174 port 60535 [preauth]

该卷属于主机用户的UID,不是root ,SSHD拒绝使用它。 解决方法top之上的解决方法是使用configs ,如下所示:

services:
  sshd:
    image: [...]/sshd:${version}
    configs:
      # FIXME: It would be much better to use a bind volume for this, as it
      # would always be in sync with the host configuration. So revoking a key
      # in the host machine would automatically revoke it in the container. But
      # I can't figure out how to give the volume right ownership. It keeps UID
      # from the host which doesn't align with the container user.
      - source: authorized_keys
        target: /root/.ssh/authorized_keys
        mode: 0600

configs:
  authorized_keys:
    file: ~/.ssh/authorized_keys

我了解由于您不知道容器将部署到哪个主机而无法告诉服务绑定到特定主机ip地址。

但是主机通常具有例如北向和南向接口。 您可能希望群端口​​仅绑定到所有群主机上的北向接口。

如果要绑定服务的所有接口的接口名称都相同(例如eth0),则可以提供一个选项来指定将集群端口绑定到的接口名称(在“服务端口”部分)。

    nginx:
      image: nvbeta/swarm_nginx
      networks:
        - demonet1
      ports:
        - "eth0:8088:80"

当eth0在群集节点上不可用时,指定的端口将不会绑定到任何接口。

@ tad-lispy您应该能够将容器用户的uid和gid更改为与主机上的卷所有者相同。
linuxserver映像通过设置环境变量来支持此操作(请参阅https://hub.docker.com/r/linuxserver/openssh-server,$#$ User / Group Identifiers ),

此页面是否有帮助?
0 / 5 - 0 等级