## 前言
- 最近公司内大举开始docker部署的实践,从本地连接测试服务器的docker服务不可避免。但心塞的是docker默认的tcp连接2375开启之后,服务器被矿机攻击的概率实在是太高。一个星期之内居然被攻击了三次,后续经过排查发现可以通过更改2375为其他端口以减轻被攻击的频率,但这明显治标不治本,于是开启研究docker内的tsl连接,之后便有了下文。
## 准备工作
- 在服务器内创建一个文件夹,进入该文件夹,后续所有密钥文件全部存入该文件夹
> 示例:
>
> ```bash
> mkdir -p /mnt/docker/tsl
> cd /mnt/docker/tsl
> ```
- 默认情况下,Docker通过非联网的Unix套接字运行。它还可以选择使用HTTP套接字进行通信。这里有三个角色,**Docker服务端**和**Docker客户端**和**CA签名的证书**
- Docker服务端:对应运行Docker守护程序的主机
- 通过开放端口(一般为2376)支持远程连接
- 通过指定**tlsverify**标志并将Docker的**tlscacert**标志指向**可信的CA证书**来启用**TLS**以实现安全访问(仅允许由该CA签名的证书进行身份验证的客户端连接)
- Docker客户端:即默认情况下的Docker主机
- 当使用证书连接时,仅能连接到具有该CA签名的证书的服务器
- CA签名的证书
- 作用:服务端和客户端证书都只对应一份信任列表,信任列表里是服务端的信息(如ip或域名等等),服务端持有服务端证书,仅接受持有客户端证书的主机访问(这里可以再加上其他的限制,详情见下文)
> 注意:
>
> - Docker服务端也可以不开启TLS验证,不过这样子很不安全,生产环境下应当尽量避免.如果只是试验性的,可以指定关闭TLS验证,但只对特定主机开放,方法见后文`/etc/docker/daemon.json`配置相关
>
> - 如果Docker服务端没有开启TLS验证,则Docker客户端不需要使用证书连接.但如果客户端不使用证书连接开启了TLS验证的服务端,则会报错,如下:
>
> ```basic
> Get http://远程主机ip:2376/v1.38/version: net/http: HTTP/1.x
> transport connection broken: malformed HTTP response
> "\x15\x03\x01\x00\x02\x02".
> * Are you trying to connect to a TLS-enabled daemon without
> TLS?
> ```
>
> - 如果Docker客户端连接时使用的证书内不含目的主机的信息,则会提示对方主机不在证书信任列表内,访问失败
## 生成密钥对
### 使用OpenSSL创建CA和服务端密钥key
> 注意:将以下示例中的所有`$HOST`实例替换为Docker守护程序主机的域名(DNS name)(博主使用的是域名,如果是使用ip则将此处替换为自己的ip地址,有影响的地方我会再做提醒)
以下步骤在**Docker服务端**进行:
#### 1. 生成CA私钥ca-key.pem
> 说明:ca-key.pem是一个临时文件,最后可以删除。
- 指令:
```basic
openssl genrsa -aes256 -out ca-key.pem 4096
```
> 如下示例,需要设置密码并验证
>
> ```basic
> $ openssl genrsa -aes256 -out ca-key.pem 4096
> Generating RSA private key, 4096 bit long modulus
> ............................................................................................................................................................................................++
> ........++
> e is 65537 (0x10001)
> Enter pass phrase for ca-key.pem:
> Verifying - Enter pass phrase for ca-key.pem:
> ```
#### 2. 使用CA私钥生成自签名CA证书ca.pem
> 说明:生成证书时,通过-days 365设置证书的有效期。单位为天,默认情况下为30天。有了CA证书后,就可以创建服务器密钥和证书签名请求(CSR)了
- 指令:
```basic
openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
```
> 如下示例,需要验证密码并输入信息,注意"Common Name"设置为服务器所在主机的域名(我只填了Country Name 为CN,Province Name为Guangdong,后面直接回车也没关系,反正也只是自己用的证书)
>
> ```basic
> $ openssl req -new -x509 -days 365 -key ca-key.pem -sha256 -out ca.pem
> Enter pass phrase for ca-key.pem:
> You are about to be asked to enter information that will be incorporated
> into your certificate request.
> What you are about to enter is what is called a Distinguished Name or a DN.
> There are quite a few fields but you can leave some blank
> For some fields there will be a default value,
> If you enter '.', the field will be left blank.
> -----
> Country Name (2 letter code) [AU]:CN
> State or Province Name (full name) [Some-State]:Guangdong
> Locality Name (eg, city) []:ShenZhen
> Organization Name (eg, company) [Internet Widgits Pty Ltd]:Docker Inc
> Organizational Unit Name (eg, section) []:Sales
> Common Name (e.g. server FQDN or YOUR name) []:$HOST(冒号后改为自己的域名或ip)
> Email Address []:
> ```
#### 3. 生成服务器私钥server-key.pem和证书签名请求server-csr
> 说明:CSR:Certificate Signing Request,证书签名请求,server-csr是一个临时文件,生成server-cert.pem以后,可以删除。这里分两小步:
>
> - 第一步生成服务器私钥server-key.pem
> - 第二步使用服务器私钥另加CN信息生成证书签名请求server-csr.pem
- 指令1:
```basic
openssl genrsa -out server-key.pem 4096
```
- 指令2:
```basic
openssl req -subj "/CN=$HOST(等于号后面改为自己的域名或ip)" -sha256 -new -key server-key.pem -out server.csr
```
> 如下示例(注意这里的CN信息对应的是服务器所在主机域名,如果没有的话也没关系,可以通过下一步的`extfile.cnf`配置IP地址连接):
>
> ```basic
> $ openssl genrsa -out server-key.pem 4096
> Generating RSA private key, 4096 bit long modulus
> .....................................................................++
> .................................................................................................++
> e is 65537 (0x10001)
>
> $ openssl req -subj "/CN=$HOST(等于号后面改为自己的域名或ip)" -sha256 -new -key server-key.pem -out server.csr
> ```
#### 4. 编写extfile.cnf(重要)
> 说明:这个文件用于指定下一步生成签名证书的一些属性配置,这里我们主要用到两个属性,如果要其他要求(如限制指定ip范围的客户端才能连接)的可以看[OpenSSL x509v3_config文档](1)
>
> - subjectAltName
>
> 主题备选名称,是有点像上一步生成server.csr时所用的选项-subj "/CN=$HOST"的东西,这个更像一个说明补充,这里可以填信任的DNS域名和主机IP等等。
> 另外,需要特别注意,这里对应生成的是一份信任列表,这里所说的信任是对服务端的信任,所以填的是服务端的信息(如域名,IP),我之前看到有文章说这里的列表是客户端的列表,只有在列表中的客户端才能访问服务器,这种说法是错误,在使用证书连接到服务器时,会报错说服务器IP不在信任列表中( 如远程主机ip为ip3,证书的信任列表为ip1和ip2时,若使用该证书访问远程主机,则会报错x509: certificate is valid for ip1, ip2, not ip3)
>
> - extendedKeyUsage
>
> 扩展密钥用法,此扩展包含一个用法列表,用于指示证书公钥可用于的目的
- 指令1:
```basic
echo extendedKeyUsage = serverAuth >> extfile.cnf
```
- 指令2:
```basic
echo subjectAltName = DNS:$HOST(冒号后面改为自己的域名),IP:10.10.10.20,IP:127.0.0.1,IP:公网ip(若是拥有域名则可不使用公网ip连接,此处可省略) >> extfile.cnf
```
> 指令解析:
>
> - 将Docker守护程序密钥的扩展使用属性设置为仅用于服务器身份验证
> - 将域名(DNS Name)$HOST和IP为10.10.10.20(私有地址,用于局域网登录)和127.0.0.1(本地地址,用于本机登录)和公网ip(用于远程登录)
>
> 注意:事实上只配一个公网IP就行了,其他的视实际需求而定,本机的话一般不那么麻烦,通过docker本身的配置即可使用本机控制,这里配127.0.0.1主要是为了测试是否能连通,如果只配了私有地址则只有本局域网可访问,安全性更高
#### 5. 使用CA证书生成服务器签名证书server-cert.pem
> 注意,上面总共生成了两大模块的文件和一个extfile.cnf配置文件,这三部分之间是彼此独立的,到了这一步,也是最后一步才真正做了整合.
> 这里要用到的有,(1)CA私钥ca-key.pem和CA签名文件ca.pem,(2)证书签名请求server-csr(3)配置文件extfile.cnf
- 指令:
```basic
openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out server-cert.pem -extfile extfile.cnf
```
> 如下示例:
>
> ```basic
> $ openssl x509 -req -days 365 -sha256 -in server.csr -CA ca.pem -CAkey ca-key.pem \
> -CAcreateserial -out server-cert.pem -extfile extfile.cnf
> Signature ok
> subject=/CN=your.host.com
> Getting CA Private Key
> Enter pass phrase for ca-key.pem:
> ```
至此,Docker服务端密钥创建完毕。
### 利用CA创建客户端密钥key
> 注意:生成密钥需要使用CA私钥和签名文件,为简化流程,避免CA文件在服务端和客户端之间的传输,以下步骤仍在**Docker服务端**进行
>
> 创建客户端密钥的过程和服务端类似,CA相关已经创建好了,extfile.cnf配置文件也简单很多,具体如下
#### 1. 生成客户端私钥key.pem和证书签名请求client-csr
- 指令:
```basic
openssl genrsa -out key.pem 4096
openssl req -subj '/CN=client' -new -key key.pem -out client.csr
```
> 如下示例:
>
> ```basic
> $ openssl genrsa -out key.pem 4096
> Generating RSA private key, 4096 bit long modulus
> .........................................................++
> ................++
> e is 65537 (0x10001)
>
> $ openssl req -subj '/CN=client' -new -key key.pem -out client.csr
> ```
#### 2. 编写扩展配置文件extfile.cnf
> 注意,我们依旧是在刚才创建服务器私钥的文件夹下,应该还有原来的extfile.cnf文件,为避免覆写,可以先执行重命名
- 指令1:
```basic
mv extfile.cnf extfile.cnf.old
```
- 指令2: 创建扩展配置文件并使密钥适用于客户端身份验证
```basic
echo extendedKeyUsage = clientAuth >> extfile.cnf
```
#### 3. 生成签名文件cert.pem
- 指令:
```basic
openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem -CAcreateserial -out cert.pem -extfile extfile.cnf
```
> 如下示例:
>
> ```basic
> $ openssl x509 -req -days 365 -sha256 -in client.csr -CA ca.pem -CAkey ca-key.pem \
> -CAcreateserial -out cert.pem -extfile extfile.cnf
> Signature ok
> subject=/CN=client
> Getting CA Private Key
> Enter pass phrase for ca-key.pem:
> ```
至此,Docker客户端密钥key创建完毕
## 修改文件权限
- 指令1:删除两个证书签名请求文件
```basic
rm -v client.csr server.csr
```
- 指令2:修改密钥文件权限为只由所有者读取
```basic
chmod -v 0400 ca-key.pem key.pem server-key.pem
```
- 指令3:修改证书文件权限为只读
```basic
chmod -v 0444 ca.pem server-cert.pem cert.pem
```
## 启动Docker守护进程
- 启动Docker守护进程有两种方法,直接用带参指令或者修改daemon.json配置文件,另外,还有一种方式使用systemctl修改docker.service文件,这种不推荐,这里不作介绍.需要注意的是,不管是哪一种方法,只要对同一属性做了配置,都会导致冲突而启动失败.所以建议只使用一种
- 注意,监听unix:///var/run/docker.sock是为了实现本机docker直接控制,监听tcp://0.0.0.0:2376表示监听2376端口所有连接,又这里开启了TLS验证,则会根据我们给定的TLS文件去做验证
- 服务端需要的TLS文件有CA证书**ca.pem**,服务端证书**server-cert.pem**,服务端密钥**server-key.pem**
- 目录结构`cd /mnt/docker/tsl && tree -cp`
```basic
.
├── [-r--r--r--] ca.pem
├── [-r--r--r--] cert.pem
├── [-r--r--r--] server-cert.pem
├── [-r--------] ca-key.pem
├── [-r--------] key.pem
├── [-r--------] server-key.pem
├── [-rw-r--r--] ca.srl
├── [-rw-r--r--] extfile.cnf
└── [-rw-r--r--] extfile.cnf.old
```
下面的两种方法的介绍,只可二选一!
### 方式1.指令启动
- 指令:
```basic
dockerd --tlsverify=true \
--tlscacert=/mnt/docker/tsl/ca.pem \
--tlscert=/mnt/docker/tsl/server-cert.pem \
--tlskey=/mnt/docker/tsl/server-key.pem \
--host tcp://0.0.0.0:2376 \
--host unix:///var/run/docker.sock
```
### 方式2. daemon.json配置启动
- 配置`/etc/docker/daemon.json`文件如下,注意,镜像地址与本文无关,可不配置,配置只为加速镜像拉取。
```json
{
"tlsverify": true,
"tlscacert": "/mnt/docker/tsl/ca.pem",
"tlscert": "/mnt/docker/tsl/server-cert.pem",
"tlskey": "/mnt/docker/tsl/server-key.pem",
"hosts": ["tcp://0.0.0.0:2376","unix:///var/run/docker.sock"],
"registry-mirrors": ["https://5ehijrnq.mirror.aliyuncs.com"]
}
```
- 然后通过**systemctl**正常启动,指令如下
```basic
systemctl restart docker
```
## 启动失败的处理办法
- 如果在修改 `/etc/docker/daemon.json`后遇到启动失败的问题,如
> ```basic
> Job for docker.service failed because the control process exited with error code. See "systemctl status docker.service" and "journalctl -xe" for details.
> ```
- 首先检查 json 文件的编写有没有错误,比方说是不是加多了一个 “ , ”符号,如果确定json没有错,那通常是json文件的启动项和dockerd指令方法启动时附带的参数冲突了,这个时候建议不要使用dockerd指令启动,如果你只是简单地使用 `systemctl start docker`启动,那这个时候通常只剩下一种可能性--------systemd的`docker.service`文件里面附带了与你`daemon.json`文件重复的配置项。这个时候处理方法如下:
- 打开配置文件
```basic
vim /usr/lib/systemd/system/docker.service
```
- 找到其中`Service`下的内容,将其中的`-H fd:// --containerd=/run/containerd/containerd.sock`后的内容注释掉。如下所示:
```basic
···
[Service]
Type=notify
# the default is not to use systemd for cgroups because the delegate issues still
# exists and systemd currently does not support the cgroup feature set required
# for containers run by docker
ExecStart=/usr/bin/dockerd
# -H fd:// --containerd=/run/containerd/containerd.sock(注释这一行)
ExecReload=/bin/kill -s HUP $MAINPID
TimeoutSec=0
RestartSec=2
Restart=always
···
```
- 重新加载配置
```basic
systemctl daemon-reload
```
- 重新启动
```basic
systemctl restart docker
```
## 验证远程控制
- 客户端需要的TLS文件有**CA证书ca.pem**,**客户端证书cert.pem**,**客户端密钥key.pem**连接的指令格式如下,以**docker version**为例,其中$HOST为远程主机的域名或ip
```basic
docker --tlsverify --tlscacert=ca.pem --tlscert=cert.pem --tlskey=key.pem -H=$HOST:2376 version
```
> 注意:此指令需要在客户端TSL的放置目录执行,即`ca.pem`、`cert.pem`、`key.pem`三个文件在哪个目录,就在哪个目录执行此条指令。
- 若看到docker的版本信息即为TSL配置成功
```basic
Client: Docker Engine - Community
Version: 19.03.5
API version: 1.40
Go version: go1.12.12
Git commit: 633a0ea
Built: Wed Nov 13 07:25:41 2019
OS/Arch: linux/amd64
Experimental: false
Server: Docker Engine - Community
Engine:
Version: 19.03.5
API version: 1.40 (minimum version 1.12)
Go version: go1.12.12
Git commit: 633a0ea
Built: Wed Nov 13 07:24:18 2019
OS/Arch: linux/amd64
Experimental: false
containerd:
Version: 1.2.10
GitCommit: b34a5c8af56e510852c35414db4c1f4fa6172339
runc:
Version: 1.0.0-rc8+dev
GitCommit: 3e425f80a8c931f88e6d94a8c831b9d5aa481657
docker-init:
Version: 0.18.0
GitCommit: fec3683
```
## 本地连接
- 此处以idea为例,如图所示
![][image-1]
> 注意:连接时请将`ca.pem`、`cert.pem`、`key.pem`三文件置于图上的Certificates folder指定的目录下,Engine API URL处需填写http地址,不是tcp,请务必注意这点。
## 后记
文章参考:[鲸临于空](https://blog.csdn.net/alinyua)的[Docker 守护进程+远程连接+安全访问+启动冲突解决办法 (完整收藏版)](<https://blog.csdn.net/alinyua/article/details/81086124>)
感谢前辈的辛苦付出。
[1]: https://www.openssl.org/docs/manmaster/man5/x509v3_config.html
[image-1]: https://blog.chihiro.org.cn/upload/2019/11/idea%E7%9A%84docker%E5%AE%89%E5%85%A8%E8%BF%9E%E6%8E%A5%E9%85%8D%E7%BD%AE-191c26240bd74efcaad579dfd5de4c00.png

docker基于tsl的安全tcp访问