9. 安全与访问控制

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

策略示例

假设我们有一个微服务系统,需要实现以下访问控制:

  1. 运维团队:需要完全控制所有配置。
  2. 支付服务:只能读写自己的配置 /services/payment/。
  3. 日志服务:只能读取所有服务的配置 /services/,用于收集日志配置。

实现步骤:

  1. 创建角色

    # 运维角色,拥有全局读写权限
    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/
    
  2. 创建用户

    etcdctl user add ops-team
    etcdctl user add svc-payment --no-password
    etcdctl user add svc-logger --no-password
    
  3. 授予角色

    etcdctl user grant-role ops-team admin
    etcdctl user grant-role svc-payment role-payment
    etcdctl user grant-role svc-logger role-logger
    
  4. 启用认证

    etcdctl auth enable
    

现在,运维团队可以使用密码登录并管理所有配置;支付服务使用其 TLS 证书(CN 为 svc-payment)登录,只能操作 /services/payment/ 下的键;日志服务同理,只能读取 /services/ 下的配置。

安全最佳实践

将上述安全特性组合起来,可以构建一个健壮的 etcd 安全体系。以下是一些在生产环境中应遵循的最佳实践:

  1. 始终启用 TLS:无论是客户端通信还是节点间通信,都应强制使用 TLS。这不仅加密了数据,还防止了未授权的节点加入集群。
  2. 强制双向 TLS 认证:在 etcd 服务器端设置 --client-cert-auth=true 和 --peer-client-cert-auth=true。这确保了只有持有有效证书的客户端和节点才能连接。
  3. 使用独立的 CA:为 etcd 集群创建一个专用的内部 CA,不要使用公共 CA 或其他服务的 CA。这可以隔离风险。
  4. 最小权限原则:为每个用户或服务授予完成其工作所需的最小权限。不要轻易授予 root 角色。例如,只读服务就只给读权限。
  5. 证书轮换:定期更换证书和密钥,以降低密钥泄露的风险。建立自动化的证书管理流程。
  6. 保护 root 凭证:root 用户的密码和证书必须严格保管,仅限最高权限的管理员访问。
  7. 审计日志:虽然本章未详述,但应开启 etcd 的审计日志,记录所有 API 调用,以便追踪异常行为。
  8. 网络隔离:通过防火墙策略,限制只有应用服务器和 etcd 节点可以访问 etcd 的客户端端口(2379)和 peer 端口(2380)。
  9. 避免使用 --auto-tls:自动生成的证书无法提供身份验证,容易受到中间人攻击。生产环境必须使用由可信 CA 签发的证书。

通过遵循这些实践,可以确保 etcd 集群中的数据在传输和访问时都得到充分的保护,为上层应用提供一个安全、可靠的分布式协调服务。