9.安全与访问控制
TLS 传输安全配置
在生产环境中,数据在传输过程中的安全性至关重要。etcd 默认以明文方式通信,这意味着网络中的任何监听者都可能截获敏感数据。为了保护数据在客户端与服务器之间、以及集群节点之间的传输安全,etcd 提供了基于 TLS(Transport Layer Security)的加密通信机制。
TLS 协议通过非对称加密交换密钥,再通过对称加密保护后续通信,同时利用数字证书验证通信双方的身份。在 etcd 中,TLS 配置分为两个层面:客户端与服务器之间的通信(client-to-server)以及集群节点之间的内部通信(peer-to-peer)。
证书与密钥准备
要启用 TLS,首先需要一套可信的证书体系。通常,我们会创建一个私有的证书颁发机构(CA),然后用这个 CA 签发服务器证书和客户端证书。
- CA 证书 (CA Certificate): 信任链的根,用于验证其他证书是否由该机构签发。
- 服务器证书 (Server Certificate): 部署在 etcd 服务器端,用于向客户端证明自己的身份。
- 服务器私钥 (Server Private Key): 与服务器证书配对的私钥,用于解密客户端发来的加密数据或进行签名。
- 客户端证书 (Client Certificate): 部署在客户端,用于向服务器证明自己的身份(可选,取决于是否开启客户端证书认证)。
- 客户端私钥 (Client Private Key): 与客户端证书配对的私钥。
etcd 通过命令行参数或环境变量来指定这些文件的路径。
服务器端配置
客户端到服务器的通信
要让 etcd 监听 HTTPS 请求,需要配置以下参数:
--cert-file: 服务器的 TLS 证书路径。--key-file: 服务器的 TLS 私钥路径。--listen-client-urls: etcd 监听客户端请求的地址,应使用https协议。--advertise-client-urls: 客户端访问该节点的地址,应使用https协议。
一个基础的配置示例如下:
etcd --name infra0 \
--data-dir /var/lib/etcd/infra0 \
--cert-file=/etc/etcd/certs/server.crt \
--key-file=/etc/etcd/certs/server.key \
--listen-client-urls=https://0.0.0.0:2379 \
--advertise-client-urls=https://127.0.0.1:2379
启动后,客户端可以使用 CA 证书来验证服务器并建立加密连接。
集群节点间的通信
除了客户端通信,集群节点之间的数据同步也需要保护。这被称为 peer 通信。
--peer-cert-file: peer 通信使用的证书。--peer-key-file: peer 通信使用的私钥。--listen-peer-urls: 监听其他节点请求的地址,使用https。--advertise-peer-urls: 其他节点访问该节点的地址,使用https。
配置示例:
etcd --name infra0 \
--data-dir /var/lib/etcd/infra0 \
--peer-cert-file=/etc/etcd/certs/server.crt \
--peer-key-file=/etc/etcd/certs/server.key \
--listen-peer-urls=https://0.0.0.0:2380 \
--advertise-peer-urls=https://127.0.0.1:2380
客户端配置
当服务器启用了 TLS,客户端也需要相应的配置才能连接。
使用 etcdctl
etcdctl 是 etcd 官方提供的命令行客户端工具。连接启用了 TLS 的集群时,需要指定 CA 证书以及可选的客户端证书。
# 仅验证服务器证书(单向认证)
etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/etcd/certs/ca.crt \
endpoint health
# 同时验证服务器和客户端(双向认证)
etcdctl --endpoints=https://127.0.0.1:2379 \
--cacert=/etc/etcd/certs/ca.crt \
--cert=/etc/etcd/certs/client.crt \
--key=/etc/etcd/certs/client.key \
put foo bar
使用 Go 客户端
在 Go 程序中,通常使用 crypto/tls 包来配置 TLS。需要加载 CA 证书、客户端证书和私钥,构建 tls.Config 对象,并将其传递给 gRPC 的 Dial 选项。
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io/ioutil"
"log"
"time"
"go.etcd.io/etcd/client/v3"
"google.golang.org/grpc/credentials"
)
func main() {
// 加载 CA 证书
caCert, err := ioutil.ReadFile("/etc/etcd/certs/ca.crt")
if err != nil {
log.Fatal(err)
}
caCertPool := x509.NewCertPool()
caCertPool.AppendCertsFromPEM(caCert)
// 加载客户端证书和私钥
clientCert, err := tls.LoadX509KeyPair("/etc/etcd/certs/client.crt", "/etc/etcd/certs/client.key")
if err != nil {
log.Fatal(err)
}
// 构建 TLS 配置
tlsConfig := &tls.Config{
RootCAs: caCertPool,
Certificates: []tls.Certificate{clientCert},
}
// 创建 etcd 客户端
cli, err := clientv3.New(clientv3.Config{
Endpoints: []string{"https://127.0.0.1:2379"},
DialTimeout: 5 * time.Second,
// 通过 grpc.Creds 传递 TLS 凭证
DialOptions: []grpc.DialOption{
grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)),
},
})
if err != nil {
log.Fatal(err)
}
defer cli.Close()
fmt.Println("Successfully connected to etcd with TLS")
}
这段代码演示了如何手动构建一个安全的 etcd 客户端连接。它首先读取 CA 证书并创建一个证书池,然后加载客户端的证书和私钥。最后,将这些信息放入 tls.Config,并通过 grpc.WithTransportCredentials 将其应用到 gRPC 连接中。
自动 TLS
etcd 还提供了一个简化选项 --auto-tls。开启后,etcd 会自动生成自签名的证书用于 TLS 连接。这对于快速测试或内部开发环境非常方便,但由于证书不是由可信 CA 签发的,生产环境中不推荐使用,因为客户端无法验证服务器的真实性,容易受到中间人攻击。
客户端证书认证
在 TLS 的基础上,etcd 可以进一步利用客户端证书中的信息进行身份认证,这被称为“双向 TLS 认证”(Mutual TLS, mTLS)。
启用客户端证书认证
在 etcd 服务器端,通过设置 --client-cert-auth=true 参数来强制要求所有客户端必须提供有效的证书。
etcd --name infra0 \
--cert-file=/etc/etcd/certs/server.crt \
--key-file=/etc/etcd/certs/server.key \
--client-cert-auth=true \
--trusted-ca-file=/etc/etcd/certs/ca.crt \
--listen-client-urls=https://0.0.0.0:2379 \
--advertise-client-urls=https://127.0.0.1:2379
注意 --trusted-ca-file 参数,它指定了可信的 CA 证书。etcd 会用这个 CA 来验证客户端证书的合法性。只有由该 CA 签发的客户端证书才会被接受。
证书与用户映射
当 --client-cert-auth 开启后,etcd 会从客户端证书的 Common Name (CN) 字段提取用户名。这个用户名将用于后续的 RBAC(基于角色的访问控制)权限检查。
例如,如果客户端证书的 CN 是 user01,那么 etcd 会认为这个请求是由用户 user01 发起的。因此,在配置客户端证书时,必须确保 CN 字段被正确设置为 etcd 中已存在的用户名。
优先级与注意事项
- 认证优先级:如果一个请求同时提供了用户名/密码和客户端证书,etcd 会优先使用用户名/密码认证。
- 无密码用户:可以创建一个没有密码的用户,专门用于证书认证。这在自动化场景中很常见,因为密码不适合硬编码在程序里。
- 代理限制:此功能不适用于 gRPC 代理(gRPC-proxy)和 gRPC 网关(gRPC-gateway)。因为代理会终止 TLS 连接,它与后端 etcd 服务器之间建立新的连接,导致原始客户端的证书信息丢失。如果代理配置了 TLS,它会用自己的证书与客户端通信,所有客户端看到的都是代理的证书,CN 信息无法传递。
用户认证机制
有了 TLS 提供的加密通道和身份验证基础,etcd 还内置了一套独立的用户认证系统。这套系统允许创建多个用户,并为每个用户设置密码。
认证流程
etcd 的认证是基于用户名和密码的。客户端在发起数据操作请求前,需要先通过认证接口获取一个 Token(令牌)。后续的所有请求都必须携带这个 Token,etcd 服务器会验证 Token 的有效性以及其关联的用户身份。
管理用户
etcdctl user 命令用于管理用户。
1. 创建用户
创建一个新用户会提示输入密码。
$ etcdctl user add user01
User: user01
Password:
Type password of user01:
Type password of user01 again:
User user01 created
也可以非交互式地创建用户,或者创建一个无密码用户(用于证书认证)。
# 非交互式提供密码
$ etcdctl user add user02 --interactive=false --new-user-password='my-secret-password'
# 创建无密码用户
$ etcdctl user add user03 --no-password
2. 查看用户信息
$ etcdctl user get user01
User: user01
Roles:
3. 修改密码
$ etcdctl user passwd user01
4. 删除用户
$ etcdctl user delete user01
5. 列出所有用户
$ etcdctl user list
特殊用户:root
root 是一个特殊的内置用户。在启用认证之前,必须先创建 root 用户。root 用户拥有对整个 etcd 集群的完全访问权限,可以执行任何操作,包括管理其他用户和角色。
RBAC 角色管理
RBAC(Role-Based Access Control,基于角色的访问控制)是 etcd 权限系统的核心。它的思想是将权限打包成“角色”,然后将角色“授予”用户。
核心概念
- Role (角色):一个权限的集合。角色本身没有密码,它只定义了可以访问哪些键(key)以及什么样的操作(读、写)。
- Permission (权限):定义了对一个键或一个键范围(range)的访问能力。权限分为三种:
Read: 读取键值。Write: 写入或删除键值。Readwrite: 同时拥有读和写权限。
管理角色
etcdctl role 命令用于管理角色。
1. 创建角色
$ etcdctl role add reader
Role reader created
2. 授予权限
权限可以授予给单个键,也可以授予给一个键的前缀(prefix)或一个键的范围(range)。
# 授予对 /app/config/ 下所有键的读权限 (使用前缀)
$ etcdctl role grant-permission reader read /app/config/
# 授予对 /app/secrets/db 的读写权限 (单个键)
$ etcdctl role grant-permission reader readwrite /app/secrets/db
# 授予对 [key1, key5) 范围的读写权限 (范围)
$ etcdctl role grant-permission reader readwrite key1 key5
3. 查看角色权限
$ etcdctl role get reader
Role reader
KV Read:
/app/config/, /app/config0 (prefix /app/config/)
/app/secrets/db
KV Write:
/app/secrets/db
4. 撤销权限
$ etcdctl role revoke-permission reader /app/secrets/db
5. 删除角色
$ etcdctl role delete reader
角色与用户关联
创建了角色并授予了权限后,需要将角色授予给用户,这样用户才能获得相应的权限。
# 将 reader 角色授予 user01
$ etcdctl user grant-role user01 reader
特殊角色:root
root 也是一个特殊角色。拥有 root 角色的用户,等同于 root 用户,拥有全局的读写权限,并且可以管理认证配置、集群成员等。任何需要超级管理员权限的用户都应该被授予 root 角色。
权限控制策略
在了解了用户、角色和权限之后,我们来看如何组合它们来实现灵活的访问控制策略。
启用认证
当用户和角色配置完毕后,可以通过以下命令启用整个集群的认证:
$ etcdctl auth enable
启用认证后,所有对数据的操作(put, get, delete 等)都必须经过认证。未认证的请求将被拒绝。
# 未认证的请求会失败
$ etcdctl --endpoints=... get foo
# Error: etcdserver: user name is empty
# 认证后的请求成功
$ etcdctl --endpoints=... --user=user01:password get foo
禁用认证
如果需要,可以随时禁用认证。
$ etcdctl --user root:rootpassword auth disable
策略示例
假设我们有一个微服务系统,需要实现以下访问控制:
- 运维团队:需要完全控制所有配置。
- 支付服务:只能读写自己的配置
/services/payment/。 - 日志服务:只能读取所有服务的配置
/services/,用于收集日志配置。
实现步骤:
创建角色
# 运维角色,拥有全局读写权限 etcdctl role add admin etcdctl role grant-permission admin readwrite "" # 支付服务角色 etcdctl role add role-payment etcdctl role grant-permission role-payment readwrite /services/payment/ # 日志服务角色 etcdctl role add role-logger etcdctl role grant-permission role-logger read /services/创建用户
etcdctl user add ops-team etcdctl user add svc-payment --no-password etcdctl user add svc-logger --no-password授予角色
etcdctl user grant-role ops-team admin etcdctl user grant-role svc-payment role-payment etcdctl user grant-role svc-logger role-logger启用认证
etcdctl auth enable
现在,运维团队可以使用密码登录并管理所有配置;支付服务使用其 TLS 证书(CN 为 svc-payment)登录,只能操作 /services/payment/ 下的键;日志服务同理,只能读取 /services/ 下的配置。
安全最佳实践
将上述安全特性组合起来,可以构建一个健壮的 etcd 安全体系。以下是一些在生产环境中应遵循的最佳实践:
- 始终启用 TLS:无论是客户端通信还是节点间通信,都应强制使用 TLS。这不仅加密了数据,还防止了未授权的节点加入集群。
- 强制双向 TLS 认证:在
etcd服务器端设置--client-cert-auth=true和--peer-client-cert-auth=true。这确保了只有持有有效证书的客户端和节点才能连接。 - 使用独立的 CA:为 etcd 集群创建一个专用的内部 CA,不要使用公共 CA 或其他服务的 CA。这可以隔离风险。
- 最小权限原则:为每个用户或服务授予完成其工作所需的最小权限。不要轻易授予
root角色。例如,只读服务就只给读权限。 - 证书轮换:定期更换证书和密钥,以降低密钥泄露的风险。建立自动化的证书管理流程。
- 保护 root 凭证:
root用户的密码和证书必须严格保管,仅限最高权限的管理员访问。 - 审计日志:虽然本章未详述,但应开启 etcd 的审计日志,记录所有 API 调用,以便追踪异常行为。
- 网络隔离:通过防火墙策略,限制只有应用服务器和 etcd 节点可以访问 etcd 的客户端端口(2379)和 peer 端口(2380)。
- 避免使用
--auto-tls:自动生成的证书无法提供身份验证,容易受到中间人攻击。生产环境必须使用由可信 CA 签发的证书。
通过遵循这些实践,可以确保 etcd 集群中的数据在传输和访问时都得到充分的保护,为上层应用提供一个安全、可靠的分布式协调服务。