安全地远程调用Docker API

引言

你也许知道如何在一台安装了 docker 的机器上创建、停止、删除容器。但是如果发号施令的程序和 docker 不在同一台机器上该怎么办?

本文将介绍如何开启 docker api 远程调用、使用 http api 或者 java sdk 操作另一台机器上的 docker、以及如何确保 docker api 不被他人调用。

开启 docker api 远程调用(简单,不鉴权,快速体验)

默认 docker 只允许本机使用docker命令进行操作,我们现在来开启 http api 调用,使得其他机器可以通过 http 请求来操作 docker。

注意:这种方法不对调用者权限进行校验,也就是说互联网上任何一个人都可以控制你的 docker。我们这里为了快速体验临时使用这种方法,长期使用请务必使用 tls 来鉴权,具体方法下文会介绍。

vim /lib/systemd/system/docker.service 编辑 docker 启动命令,找到 service 模块下的 ExecStart=xxx 这一行,在其末尾添加`-H=tcp://0.0.0.0:2375

其中,0.0.0.0 意为允许任何来源的访问,docker 将会在 2375 端口监听 http 请求。(记得开放安全组或者防火墙)

重启 docker:

systemctl daemon-reload
sudo service docker restart

然后你就可以通过 http 请求远程操控 docker 了,例如通过curl http://ip:2375/images/json 来获取镜像列表。同样你也可以通过各种编程语言的 sdk 来发起请求,详见:https://docs.docker.com/engine/api/sdk/

接下来我们用 java sdk 举例

使用 java sdk 远程调用 docker api

我们使用 star 最多的 sdk:https://github.com/docker-java/docker-java

我的使用的 maven 依赖如下,其他版本可能会报错:

<dependency>
    <groupId>com.github.docker-java</groupId>
    <artifactId>docker-java</artifactId>
    <version>3.3.4</version>
</dependency>
<dependency>
    <groupId>com.github.docker-java</groupId>
    <artifactId>docker-java-transport-httpclient5</artifactId>
    <version>3.3.4</version>
</dependency>
<dependency>
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.2.1</version>
</dependency>

假设我有一台安装了 docker 的 ubuntu 服务器,下面这段代码将初始化一个 dockerClient,然后通过远程调用获取 docker 的镜像列表。

DockerClientConfig config = DefaultDockerClientConfig
        .createDefaultConfigBuilder()
        .withDockerHost("tcp://服务端 ip 地址:2375")
        .withDockerTlsVerify(false)
        .build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
        .dockerHost(config.getDockerHost())
        .sslConfig(config.getSSLConfig())
        .maxConnections(100)
        .connectionTimeout(Duration.ofSeconds(30))
        .responseTimeout(Duration.ofSeconds(45))
        .build();
dockerClient = DockerClientImpl.getInstance(config, httpClient);
dockerClient.listImagesCmd().exec().forEach(System.out::println);

开启鉴权,防止 docker api 被他人调用

现在我们对 docker api 远程调用有了一个基本认识,接下来我们将使用 tls 自签证书为 docker api 开启鉴权,只有持有配对证书的客户端和服务端才可以通信,防止被他人调用。

服务端

找一个目录存放等下要生成的证书文件,本文以/root/docker/cert/ 为例。

进入这个目录,逐一运行以下命令:

# 这里要你设定一个密码,找个地方存下来,等下运行其他命令还要再输入这个密码
openssl genrsa -aes256 -out ca-key.pem 4096

# 这一步的问题可以不回答,一路回车即可
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem

openssl genrsa -out server-key.pem 4096

# 把$HOST替换成运行着 docker 的服务器的 ip 或者域名
openssl req -subj "/CN=$HOST" -sha256 -new -key server-key.pem -out server.csr

# 把$HOST替换成运行着 docker 的服务器的域名,把$IP替换成 ip。二者至少填一个,不填的就删掉,总之你最终要通过 ip 远程调用就填 ip,通过域名调用就填域名
echo subjectAltName = DNS:$HOST,IP:$IP >> extfile.cnf

echo extendedKeyUsage = serverAuth >> extfile.cnf

openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out server-cert.pem -extfile extfile.cnf

openssl genrsa -out key.pem 4096

openssl req -subj '/CN=client' -new -key key.pem -out client.csr

echo extendedKeyUsage = clientAuth > extfile-client.cnf

openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
  -CAcreateserial -out cert.pem -extfile extfile-client.cnf

# 可选,删除以后没用的配置文件
rm -v client.csr server.csr extfile.cnf extfile-client.cnf

现在这个目录里应该会有一些 .pem 文件,其中,ca.pem, server-cert.pem, server-key.pem 是服务端要用的,ca.pem, cert.pem, key.pem 是客户端要用的。

vim /lib/systemd/system/docker.service 编辑 docker 启动命令,找到我们之前编辑过的 service 模块下的ExecStart=xxx 这一行,把之前的-H tcp://0.0.0.0:2375改成-H tcp://0.0.0.0:2376 ,(2376是 docker 在启用 https 情况下的惯用端口),然后再在后面追加--tlsverify --tlscacert=/path/to/ca.pem tlscert=/path/to/server-cert.pem --tlskey=/path/to/server-key.pem ,以指定启用 ssl 及其用到的证书文件路径(根据自己情况修改)

下面的代码示例指定了服务端所需要的证书文件,并监听 2376 端口接收远程调用,允许所有 ip 来源(但是客户端必须持有配对的证书)

ExecStart=/usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock -H tcp://0.0.0.0:2376 --tlsverify --tlscacert=/root/docker/cert/ca.pem --tlscert=/root/docker/cert/server-cert.pem --tlskey=/root/docker/cert/server-key.pem

重启 docker:

systemctl daemon-reload
sudo service docker restart

客户端

我们从服务端把ca.pem, cert.pem, key.pem这三个文件拷贝到客户端的一个目录里放下,然后修改客户端的代码:

DockerClientConfig config = DefaultDockerClientConfig
        .createDefaultConfigBuilder()
        .withDockerHost("tcp://服务端 ip 地址:2376")
        .withDockerTlsVerify(true)
        .withDockerCertPath("你存放客户端证书的目录路径")
        .build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder()
        .dockerHost(config.getDockerHost())
        .sslConfig(config.getSSLConfig())
        .maxConnections(100)
        .connectionTimeout(Duration.ofSeconds(30))
        .responseTimeout(Duration.ofSeconds(45))
        .build();
dockerClient = DockerClientImpl.getInstance(config, httpClient);
dockerClient.listImagesCmd().exec().forEach(System.out::println);

如果你能够正常获取服务端的 docker 镜像列表,说明你成功地实现了安全远程调用 docker api

结论

本文介绍了如何开启 docker api 远程调用,并用 java sdk 控制远程 docker 服务器,以及如何通过自签 tls 证书来确保 docker api 不被他人调用

参考资料

https://docs.docker.com/engine/security/protect-access/#use-tls-https-to-protect-the-docker-daemon-socket

https://www.youtube.com/watch?v=cjm_NqteLLA&ab_channel=CodeWithRajRanjan

文章作者: 白烛魁
本文链接:
版权声明: 本站所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 白烛魁的小站
Docker
喜欢就支持一下吧