OS X 下使用证书认证

最近安全界爆出了一个 Docker/Swarm 2375 端口无认证的漏洞,甚至乌云里也有人搞批量扫描上报。事实上这个 2375 是 Docker Remote API 的端口,有了这个端口有什么用呢?任何能连上这个端口的人都能通过这个 API 在你的机器上部署容器,甚至是一个 privileged 的容器,那样几乎可以直接操作宿主机。但是这个端口其实本来就是干这事儿的,不过直接开放在公网上肯定危险,Docker 官方事实上一直推荐在生产环境中使用客户端证书的方式对该端口作认证。

一般在企业内部使用证书认证时,我们会拿到这么几个文件:

  • client-cert.pem 证书文件
  • client-key.pem 用户私钥
  • ca-cert.pem CA 的证书

事实上用户私钥按照道理来讲的话,不应该是认证平台下发的,而是用户本地生成后,再通过创建包含了公钥的 CSR 向 CA 请求签名后的证书,但是很多时候大家都没这么讲究,所以直接把全家桶丢给你了。

还有那个 CA 的证书实际上是因为企业内部出于成本考虑不会去将向公开的 CA 请求一个 SubCA 证书。一个是考虑成本,另外一个是因为也没啥必要,企业内部都是员工自己访问的东西,证书里只有公钥,就算泄漏了也不会造成什么影响。

因此在最最理想的情况下,认证平台可能直接就给你一个 client-cert.pem 就足够了。

假设我现在已经配置好了 Docker Remote API 的证书认证,我在 Linux 下使用 curl 请求下试试:

# curl --key key.pem --cert cert.pem --cacert ca.pem --insecure https://127.0.0.1:2376/info
{"ID":"P66H:DEYX:PEQ3:QKAR:BC7D:S7KV:5RXY:57PK:SHRS:DGTI:EBDI:V3YM","Containers":0,"ContainersRunning":0,"ContainersPaused":0,"ContainersStopped":0,"Images":0,"Driver":"aufs","DriverStatus":[["Root Dir","/var/lib/docker/aufs"],["Backing Filesystem","extfs"],["Dirs","0"],["Dirperm1 Supported","true"]],"SystemStatus":null,"Plugins":{"Volume":["local"],"Network":["bridge","null","host"],"Authorization":null},"MemoryLimit":true,"SwapLimit":false,"KernelMemory":true,"CpuCfsPeriod":true,"CpuCfsQuota":true,"CPUShares":true,"CPUSet":true,"IPv4Forwarding":true,"BridgeNfIptables":true,"BridgeNfIp6tables":true,"Debug":false,"NFd":13,"OomKillDisable":true,"NGoroutines":30,"SystemTime":"2016-05-29T02:16:06.783224329+08:00","ExecutionDriver":"","LoggingDriver":"json-file","CgroupDriver":"cgroupfs","NEventsListener":0,"KernelVersion":"3.16.0-60-generic","OperatingSystem":"Ubuntu 14.04.4 LTS","OSType":"linux","Architecture":"x86_64","IndexServerAddress":"https://index.docker.io/v1/","RegistryConfig":{"InsecureRegistryCIDRs":["127.0.0.0/8"],"IndexConfigs":{"docker.io":{"Name":"docker.io","Mirrors":null,"Secure":true,"Official":true}},"Mirrors":null},"NCPU":2,"MemTotal":1041883136,"DockerRootDir":"/var/lib/docker","HttpProxy":"","HttpsProxy":"","NoProxy":"","Name":"163-44-149-20","Labels":null,"ExperimentalBuild":false,"ServerVersion":"1.11.1","ClusterStore":"","ClusterAdvertise":""}

一切看起来很正常,我就开开心心的像在 OS X 上也尝试下这么访问,结果 OS X 上却爆出了这么的错误:

$ curl -v --cert cert.pem --key key.pem --cacert ca.pem https://163-44-149-20:2376/
*   Trying 163.44.149.20...
* Connected to 163-44-149-20 (163.44.149.20) port 2376 (#0)
* WARNING: SSL: CURLOPT_SSLKEY is ignored by Secure Transport. The private key must be in the Keychain.
* WARNING: SSL: Certificate type not set, assuming PKCS#12 format.
* SSL: Can't load the certificate "cert.pem" and its private key: OSStatus -25299
* Closing connection 0
curl: (58) SSL: Can't load the certificate "cert.pem" and its private key: OSStatus -25299

Google 了好久,百思不得其解。

我想了想,要不我用浏览器试试。然后把证书导入了 Keychain Access 中,奇怪,居然不找我要私钥还能导入成功……然后打开浏览器,果然还是打不开:

但是我想倒入那个私钥时却告诉我类型不支持?

之后回头注意到了那两个 WARNING:

* WARNING: SSL: CURLOPT_SSLKEY is ignored by Secure Transport. The private key must be in the Keychain.
* WARNING: SSL: Certificate type not set, assuming PKCS#12 format.

难道是因为 OS X 上的 curl 有什么幺蛾子?然后 which curl 看了下果然不是 Homebrew 安装的 curl 而是 /usr/bin/curl。之后我用 brew info curl 看了下,貌似是因为 OS X 现在弃用了 OpenSSL 的缘故导致 curl 时几个证书格式解析方式也不同了,于是用 brew install curl --with-openssl 安装了个看起来比较和谐的 curl,之后尝试了下就没问题了。

那么在 OS X 下使用那个妖怪版的 curl 怎么做客户端证书认证呢?注意到这个格式 PKCS#12,这个格式是 OS X curl 的 --cert 参数默认读取的格式,我们可以通过这么一条命令将我们的证书和私钥打包成一个 pkcs12 文件:

openssl pkcs12 -export -clcerts -in client-cert.pem -inkey client-key.pem -out client-bundle.p12

然后再导入 Keychain Access 的时候就看起来合理多了。用浏览器打开页面时也会问你需要使用那个证书啦。

那么在 curl 中如何使用这个证书呢?我们可以这么做:

 $ curl -H 'Host: 163-44-149-20' --cacert ./auth-ca.pem --cert client-bundle.p12:dangerous https://163-44-149-20:2376/info

之后就能获取到正确的结果啦~