作者:唐银@涂鸦智能安全实验室
一、etcd简介
etcd是一个具有强一致性的分布式 key-value 存储组件。采用类似目录结构的方式对数据进行存储,仅在叶子结点上存储数据,叶子结点的父节点为目录,不能存储数据。
“etcd”这个名字源自两个想法:unix “/etc” 目录和 “d” istributed 分布式系统。“/etc” 目录是用于存储单个系统的配置数据的位置,而 etcd 用于存储大规模分布式的配置信息。因此,加了 “d” 的 “/etc” 就是 “etcd”。
etcd使用比较多的场景包括服务注册与发现、键值对存储、消息发布订阅等。
在kubernetes中,etcd存储集群状态和配置信息,以用于服务发现和集群管理。
二、测试环境搭建
测试环境说明:
- etcdctl在本机运行;
- etcd集群部署在虚拟机中的docker下;
- 虚拟机环境:CentOS 7;
- 虚拟机ip:192.168.126.143
首先拉取etcd镜像
docker pull quay.io/coreos/etcd:v3.3.1
# 查看镜像
docker images
创建自定义网络
docker network create --driver bridge --subnet=172.16.1.0/16 --gateway=172.16.1.1 mynet
# 查看网络
docker network ls
创建etcd节点
节点1:
docker run -d -p 23791:2379 -p 23801:2380 \
--name etcdnode1 \
--network=mynet \
--ip 172.16.2.1 \
quay.io/coreos/etcd:v3.3.1 \
etcd -name etcdnode1 \
-advertise-client-urls http://172.16.2.1:2379 \
-initial-advertise-peer-urls http://172.16.2.1:2380 \
-listen-client-urls http://0.0.0.0:2379 \
-listen-peer-urls http://0.0.0.0:2380 \
-initial-cluster-token etcd-cluster \
-initial-cluster "etcdnode1=http://172.16.2.1:2380,etcdnode2=http://172.16.2.2:2380,etcdnode3=http://172.16.2.3:2380" \
-initial-cluster-state new
节点2
docker run -d -p 23792:2379 -p 23802:2380 \
--name etcdnode2 \
--network=mynet \
--ip 172.16.2.2 \
quay.io/coreos/etcd:v3.3.1 \
etcd -name etcdnode2 \
-advertise-client-urls http://172.16.2.2:2379 \
-initial-advertise-peer-urls http://172.16.2.2:2380 \
-listen-client-urls http://0.0.0.0:2379 \
-listen-peer-urls http://0.0.0.0:2380 \
-initial-cluster-token etcd-cluster \
-initial-cluster "etcdnode1=http://172.16.2.1:2380,etcdnode2=http://172.16.2.2:2380,etcdnode3=http://172.16.2.3:2380" \
-initial-cluster-state new
节点3:
docker run -d -p 23793:2379 -p 23803:2380 \
--name etcdnode3 \
--network=mynet \
--ip 172.16.2.3 \
quay.io/coreos/etcd:v3.3.1 \
etcd -name etcdnode3 \
-advertise-client-urls http://172.16.2.3:2379 \
-initial-advertise-peer-urls http://172.16.2.3:2380 \
-listen-client-urls http://0.0.0.0:2379 \
-listen-peer-urls http://0.0.0.0:2380 \
-initial-cluster-token etcd-cluster \
-initial-cluster "etcdnode1=http://172.16.2.1:2380,etcdnode2=http://172.16.2.2:2380,etcdnode3=http://172.16.2.3:2380" \
-initial-cluster-state new
参数说明:
参数项 | 说明 |
---|---|
-name | etcd集群中的节点名,各节点可区分不重复即可。 |
-advertise-client-urls | 客户端(etcdctl/curl等)与当前节点通信的URL。 |
-initial-advertise-peer-urls | 其他节点与当前节点通信的URL。 |
-listen-client-urls | 当前节点监听的URL,用于跟客户端通信。 |
-listen-peer-urls | 当前节点监听的URL,用于其他节点与当前节点通信,集群内部将通过这些URL进行数据交互(如选举,数据同步等)。 |
-initial-cluster-token | 启动集群的时候指定集群token,只有token相同的节点才能加入到同一集群。当使用相同配置再启动一个集群时,只要该 token 值不一样,etcd 集群就不会相互影响。 |
-initial-cluster | 所有集群节点的url列表。 |
-initial-cluster-state | 初始化集群状态,默认为new,也可以指定为existing表示要加入到一个已有集群。 |
# 查看docker进程
docker ps
更多的安装和部署方式可参考:
http://blueskykong.com/2020/05/27/etcd-2/
http://blueskykong.com/2020/06/06/etcd-3/
三、未授权访问利用
刚刚我们搭建好的etcd环境,没有经过特殊配置,默认是未经授权即可访问的。
使用官方提供的etcdctl直接用命令行即可访问etcd,无需去了解每个http api。
下载etcd:https://github.com/etcd-io/etcd/releases
解压后在命令行中进入etcd目录下。
etcdctl api版本切换:
export ETCDCTL_API=2
export ETCDCTL_API=3
切换版本后可以执行etcdctl -h
命令查看帮助。
目前网上的公开文章大部分都是在讲v2版本api的利用,比如:
直接访问http://ip:2379/v2/keys/?recursive=true ,可以看到所有的key-value值。
或者使用etcdctl:
etcdctl --endpoints="http://ip:2379" ls
etcd v3版本的api和v2版本完全不同,所以访问上面的url不会看到任何数据。这里主要简单介绍一下v3版本api的使用。
搭建好上面的测试环境后,可以执行以下命令,向etcd中插入几条测试数据:
etcdctl --endpoints=192.168.126.143:23791 put /testdir/testkey1 "Hello world1"
etcdctl --endpoints=192.168.126.143:23791 put /testdir/testkey2 "Hello world2"
etcdctl --endpoints=192.168.126.143:23791 put /testdir/testkey3 "Hello world3"
查看指定key的值:
etcdctl --endpoints=192.168.126.143:23791 get /testdir/testkey1
执行下面命令即可读取etcd中存储的所有数据:
etcdctl --endpoints=192.168.126.143:23791 get / --prefix
--prefix
用来指定前缀,上述命令的意思就是获取所有“/”作为前缀的key value值
如果结果过多,还可以通过--limit
选项限制数量:
etcdctl --endpoints=192.168.126.143:23791 get / --prefix --limit=2
下面命令可用于列出当前目标所属同一集群的所有节点:
etcdctl --endpoints=192.168.126.143:23791 member list
更多etcdctl使用示例可以在压缩包中的:README-etcdctl.md、READMEv2-etcdctl.md文档里查看,分别对应v3、v2版本api。
四、未授权访问可能产生的风险
kubernetes的master会安装etcd v3用来存储数据,如果管理员进行了错误的配置,导致etcd未授权访问的情况,那么攻击者就可以从etcd中拿到kubernetes的认证鉴权token,从而接管集群。
在真实的场景中,还有一些应用使用etcd来存储各种服务的账号密码、公私钥等敏感数据。而很多etcd服务的使用者完全没有考虑过其安全风险,这种情况和redis的使用情况差不多,在企业内网比较普遍,甚至也有少部分人会将其开放到公网。
更多关于etcd未授权访问风险的描述可参考:https://gcollazo.com/the-security-footgun-in-etcd/
五、如何安全的使用etcd(修复方案)
etcd目前支持两种安全方案,分别解决了不同问题。
1、basic认证(基于角色的访问控制)
这种安全方案解决了用户认证和权限管理的问题。
etcd在2.1版本之前,是一个完全开放的系统,任何人都可以通过rest api对etcd数据进行增删改查。2.1版本之后,引入了用户认证功能,并且支持权限管理。但为了向前兼容,默认并未开启,需要手动启用。
etcd 2.x版本开启basic认证的相关命令和etcd 3.x版本有所区别,可以参考:https://blog.csdn.net/ucmir183/article/details/84454506
此处主要讲解etcd 3.x版本开启basic认证的过程。首先创建root用户:
etcdctl --endpoints=192.168.126.143:23791 user add root
如图,输入密码,重复输入并确认密码后创建成功:
接下来执行下面命令启用认证:
etcdctl --endpoints=192.168.126.143:23791 auth enable
启用认证后会自动为root账号创建一个root角色,该角色拥有全部etcd数据的读写权限。接下来访问etcd就必须要带着账号密码了。
例如:
查看所有角色:
etcdctl --endpoints=192.168.126.143:23791 --user root:password role list
查看所有用户:
etcdctl --endpoints=192.168.126.143:23791 --user root:password user list
创建一个新的角色:
etcdctl --endpoints=192.168.126.143:23791 --user root:password role add staff
授予staff角色/testdir/testkey1只读权限:
etcdctl --endpoints=192.168.126.143:23791 --user root:password role grant-permission staff read /testdir/testkey1
授予staff角色/pub/作为key前缀的所有数据读写权限:
etcdctl --endpoints=192.168.126.143:23791 --user root:password role grant-permission staff --prefix=true readwrite /pub/
查看staff角色权限:
etcdctl --endpoints=192.168.126.143:23791 --user root:password role get staff
结果如图:
创建一个新用户:
etcdctl --endpoints=192.168.126.143:23791 --user root:password user add staffuser1
同样需要输入要创建用户的密码。
授予staffuser1用户staff角色权限:
etcdctl --endpoints=192.168.126.143:23791 --user root:password user grant-role staffuser1 staff
创建后的staffuser1用户将拥有我们之前配置的staff角色的数据访问权限。
更多访问控制相关命令可参考官方文档:https://etcd.io/docs/v3.4/op-guide/authentication/
2、基于TLS的身份验证和数据传输
互联网中所有明文传输数据的方式,都面临三个风险:窃听、篡改和冒充。SSL/TLS协议的出现解决了这三个问题。
基于TLS的身份验证方式既解决了传输安全的问题,也可以用来解决未授权访问的问题。
TLS协议的原理不在这里赘述,如果不了解可以自行查阅相关资料。接下来主要讲etcd如何使用TLS进行身份验证和数据传输的实践。
首先我们需要下载cfssl:https://github.com/cloudflare/cfssl/releases
cfssl 是 CloudFlare 的 PKI证书管理工具。
下载cfssl-certinfo_1.5.0_linux_amd64、cfssljson_1.5.0_linux_amd64、cfssl_1.5.0_linux_amd64这三个文件,下载后全部移动到/usr/local/bin/目录下。
[root@localhost Downloads]# mv cfssl_1.5.0_linux_amd64 /usr/local/bin/cfssl
[root@localhost Downloads]# mv cfssljson_1.5.0_linux_amd64 /usr/local/bin/cfssljson
[root@localhost Downloads]# mv cfssl-certinfo_1.5.0_linux_amd64 /usr/local/bin/cfssl-certinfo
[root@localhost Downloads]# ls /usr/local/bin/cfssl*
/usr/local/bin/cfssl /usr/local/bin/cfssl-certinfo /usr/local/bin/cfssljson
创建将要存放PKI配置和证书的目录,并进入目录下:
[root@localhost /]# mkdir /etc/etcd/pki -p
[root@localhost /]# cd /etc/etcd/pki/
2.1 创建CA根证书
[root@localhost pki]# vi ca-csr.json
填入下面内容:
{
"CN": "ETCD Root CA",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Shanghai",
"ST": "Shanghai"
}
]
}
生成根证书和key
[root@localhost pki]# cfssl gencert -initca ca-csr.json | cfssljson -bare ca
[root@localhost pki]# ls
ca.csr ca-csr.json ca-key.pem ca.pem
2.2 签发证书配置文件
我们需要签发三种证书,创建ca-config.json文件,定义三个profile:
{
"signing": {
"default": {
"expiry": "168h"
},
"profiles": {
"server": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"server auth"
]
},
"client": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"client auth"
]
},
"peer": {
"expiry": "8760h",
"usages": [
"signing",
"key encipherment",
"server auth",
"client auth"
]
}
}
}
}
其中,server作为服务端与客户端通信时的服务端证书,client作为服务端与客户端通信时的客户端证书,peer作为集群节点之间的通信证书。
2.3 生成服务端证书
创建etcd-server.json文件
{
"CN": "etcd server",
"hosts": [
"172.16.2.1",
"172.16.2.2",
"172.16.2.3",
"192.168.126.143"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Shanghai",
"ST": "Shanghai"
}
]
}
生成服务端证书:
[root@localhost pki]# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=server etcd-server.json | cfssljson -bare server
2021/03/31 07:24:58 [INFO] generate received request
2021/03/31 07:24:58 [INFO] received CSR
2021/03/31 07:24:58 [INFO] generating key: rsa-2048
2021/03/31 07:24:58 [INFO] encoded CSR
2021/03/31 07:24:58 [INFO] signed certificate with serial number 545742070794152469099370572346380711975550497369
[root@localhost pki]# ls
ca-config.json ca-csr.json ca.pem server.csr server.pem
ca.csr ca-key.pem etcd-server.json server-key.pem
上面这种方式,把所有节点的ip都写到了hosts中,集群成员使用统一的服务端证书。生产环境一般把hosts写成统一的对外域名。也可以分开创建三个配置文件,每个配置文件里面填写一个ip,不公用,这样方便后面扩容。
2.4 生成客户端证书
创建etcd-client.json文件,因为客户端证书仅用于签发验证客户端身份,因此不需要hosts字段。
{
"CN": "etcd client",
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Shanghai",
"ST": "Shanghai"
}
]
}
生成客户端证书:
[root@localhost pki]# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=client etcd-client.json | cfssljson -bare client
由于没有填写hosts字段,因此会有“[WARNING] This certificate lacks a “hosts” field. ”的警告,忽略就好。
2.5 生成peer节点通信证书
创建etcd-peer.json文件:
{
"CN": "etcd peer",
"hosts": [
"172.16.2.1",
"172.16.2.2",
"172.16.2.3"
],
"key": {
"algo": "rsa",
"size": 2048
},
"names": [
{
"C": "CN",
"L": "Shanghai",
"ST": "Shanghai"
}
]
}
生成peer节点通信证书:
[root@localhost pki]# cfssl gencert -ca=ca.pem -ca-key=ca-key.pem -config=ca-config.json -profile=peer etcd-peer.json | cfssljson -bare peer
2.6 重新创建带TLS传输认证的etcd集群
首先停止运行之前创建的etcd集群,并将其删除。
[root@localhost w4ter0]# docker stop $(docker ps -a -q)
9cc6afc100ad
56148a0ebae2
43ef4286ca34
[root@localhost w4ter0]# docker rm $(docker ps -a -q)
9cc6afc100ad
56148a0ebae2
43ef4286ca34
执行下面命令创建新的etcd集群。
节点1:
docker run -d -p 23791:2379 -p 23801:2380 \
-v /etc/etcd/pki:/pki \
--name etcdnode1 \
--network=mynet \
--ip 172.16.2.1 \
quay.io/coreos/etcd:v3.3.1 \
etcd -name etcdnode1 \
-advertise-client-urls https://172.16.2.1:2379 \
-initial-advertise-peer-urls https://172.16.2.1:2380 \
-listen-client-urls https://0.0.0.0:2379 \
-listen-peer-urls https://0.0.0.0:2380 \
-initial-cluster-token etcd-cluster \
-initial-cluster "etcdnode1=https://172.16.2.1:2380,etcdnode2=https://172.16.2.2:2380,etcdnode3=https://172.16.2.3:2380" \
-initial-cluster-state new \
-cert-file=/pki/server.pem \
-key-file=/pki/server-key.pem \
-client-cert-auth \
-trusted-ca-file=/pki/ca.pem \
-peer-client-cert-auth \
-peer-cert-file=/pki/peer.pem \
-peer-key-file=/pki/peer-key.pem \
-peer-trusted-ca-file=/pki/ca.pem
节点2:
docker run -d -p 23792:2379 -p 23802:2380 \
-v /etc/etcd/pki:/pki \
--name etcdnode2 \
--network=mynet \
--ip 172.16.2.2 \
quay.io/coreos/etcd:v3.3.1 \
etcd -name etcdnode2 \
-advertise-client-urls https://172.16.2.2:2379 \
-initial-advertise-peer-urls https://172.16.2.2:2380 \
-listen-client-urls https://0.0.0.0:2379 \
-listen-peer-urls https://0.0.0.0:2380 \
-initial-cluster-token etcd-cluster \
-initial-cluster "etcdnode1=https://172.16.2.1:2380,etcdnode2=https://172.16.2.2:2380,etcdnode3=https://172.16.2.3:2380" \
-initial-cluster-state new \
-cert-file=/pki/server.pem \
-key-file=/pki/server-key.pem \
-client-cert-auth \
-trusted-ca-file=/pki/ca.pem \
-peer-client-cert-auth \
-peer-cert-file=/pki/peer.pem \
-peer-key-file=/pki/peer-key.pem \
-peer-trusted-ca-file=/pki/ca.pem
节点3:
docker run -d -p 23793:2379 -p 23803:2380 \
-v /etc/etcd/pki:/pki \
--name etcdnode3 \
--network=mynet \
--ip 172.16.2.3 \
quay.io/coreos/etcd:v3.3.1 \
etcd -name etcdnode3 \
-advertise-client-urls https://172.16.2.3:2379 \
-initial-advertise-peer-urls https://172.16.2.3:2380 \
-listen-client-urls https://0.0.0.0:2379 \
-listen-peer-urls https://0.0.0.0:2380 \
-initial-cluster-token etcd-cluster \
-initial-cluster "etcdnode1=https://172.16.2.1:2380,etcdnode2=https://172.16.2.2:2380,etcdnode3=https://172.16.2.3:2380" \
-initial-cluster-state new \
-cert-file=/pki/server.pem \
-key-file=/pki/server-key.pem \
-client-cert-auth \
-trusted-ca-file=/pki/ca.pem \
-peer-client-cert-auth \
-peer-cert-file=/pki/peer.pem \
-peer-key-file=/pki/peer-key.pem \
-peer-trusted-ca-file=/pki/ca.pem
再次访问新创建的etcd集群,直接访问
etcdctl --endpoints="https://192.168.126.143:23791" member list
会报如下错误:
浏览器中访问报错如下:
需要携带客户端的证书和密钥访问。将签发的客户端证书、密钥和ca证书copy到本机etcdctl同级目录下,指定对应参数即可正常访问:
etcdctl --endpoints="https://192.168.126.143:23791" --cacert=ca.pem --cert=client.pem --key=client-key.pem member list
至此,我们完成了基于TLS的身份验证和数据传输配置。
虽然上面两个方案都能解决etcd未授权访问的问题,但是为保证安全,实际使用时强烈建议两种方案同时上,既实现了权限管控,又保障了传输安全。
参考资料:
https://etcd.io/docs/v3.4/op-guide/
https://blog.csdn.net/u011508407/article/details/108549703
https://blog.csdn.net/weixin_30788731/article/details/97545042
https://www.jianshu.com/p/7bbef1ca9733
https://blog.csdn.net/ucmir183/article/details/84454506
https://juejin.cn/post/6844903678269210632
https://www.cnblogs.com/effortsing/p/10332492.html
http://blueskykong.com/categories/etcd/
https://gcollazo.com/the-security-footgun-in-etcd/
漏洞悬赏计划:涂鸦智能安全响应中心( https://src.tuya.com )欢迎白帽子来探索。
招聘内推计划:涵盖安全开发、安全测试、代码审计、安全合规等所有方面的岗位,简历投递sec#tuya.com,请注明来源。