强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 8 章:安全配置

第 8 章:安全配置

本章介绍 dqlite 的安全机制,包括 TLS 加密通信、认证配置、访问控制和安全最佳实践。


8.1 安全威胁模型

dqlite 面临的主要安全威胁:

威胁类型风险等级说明
网络窃听节点间和客户端-节点通信可被监听
中间人攻击攻击者可篡改通信内容
未授权访问任何人都可连接并操作数据库
数据泄露数据目录文件可被直接读取
拒绝服务恶意连接可耗尽资源
威胁示意图:

  攻击者 ◈──────────────窃听──────────────◈ Node 1
    │                                         │
    │         ┌──────────────┐                │
    └─────────│ 中间人代理   │────────────────│
              └──────┬───────┘                │
                     │                        │
  Client ◈───────────┘──────────────────────◈ Node 2
                     │
                     └──▶ 篡改数据 / 窃取凭证

8.2 TLS 加密通信

8.2.1 TLS 概述

dqlite 支持 TLS(Transport Layer Security)加密所有通信:

通信路径TLS 保护说明
节点 ↔ 节点✅ 支持Raft 复制通信
客户端 ↔ 节点✅ 支持SQL 操作通信

8.2.2 生成 TLS 证书

#!/bin/bash
# generate-certs.sh - 生成 dqlite 集群 TLS 证书

CERT_DIR="/etc/dqlite/certs"
mkdir -p "$CERT_DIR"

# 生成 CA 证书
openssl genrsa -out "$CERT_DIR/ca.key" 4096
openssl req -new -x509 -key "$CERT_DIR/ca.key" \
    -out "$CERT_DIR/ca.crt" \
    -days 3650 \
    -subj "/C=CN/ST=Beijing/O=MyOrg/CN=dqlite-ca"

# 为每个节点生成证书
for NODE_ID in 1 2 3; do
    NODE_DIR="$CERT_DIR/node${NODE_ID}"
    mkdir -p "$NODE_DIR"

    # 生成私钥
    openssl genrsa -out "$NODE_DIR/key.pem" 2048

    # 生成证书签名请求(CSR)
    openssl req -new -key "$NODE_DIR/key.pem" \
        -out "$NODE_DIR/csr.pem" \
        -subj "/C=CN/ST=Beijing/O=MyOrg/CN=dqlite-node${NODE_ID}"

    # 配置 SAN(Subject Alternative Name)
    cat > "$NODE_DIR/ext.cnf" <<EOF
[v3_req]
subjectAltName = @alt_names

[alt_names]
DNS.1 = dqlite-node${NODE_ID}
DNS.2 = localhost
IP.1 = 127.0.0.1
IP.2 = 192.168.1.10${NODE_ID}
EOF

    # 使用 CA 签发证书
    openssl x509 -req -in "$NODE_DIR/csr.pem" \
        -CA "$CERT_DIR/ca.crt" \
        -CAkey "$CERT_DIR/ca.key" \
        -CAcreateserial \
        -out "$NODE_DIR/cert.pem" \
        -days 365 \
        -extfile "$NODE_DIR/ext.cnf" \
        -extensions v3_req

    # 验证证书
    openssl x509 -in "$NODE_DIR/cert.pem" -text -noout | grep -E "(Subject:|DNS:|IP:)"

    echo "Node ${NODE_ID} certificates generated in ${NODE_DIR}"
done

# 设置权限
chmod 600 "$CERT_DIR"/*/key.pem
chmod 644 "$CERT_DIR"/*/cert.pem "$CERT_DIR/ca.crt"

echo "All certificates generated in $CERT_DIR"

8.2.3 C API 配置 TLS

/* tls_node.c - 启用 TLS 的 dqlite 节点 */
#include <dqlite.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[]) {
    dqlite_node *node;
    int rc;

    uint64_t id = 1;
    const char *address = "127.0.0.1:9001";
    const char *data_dir = "/var/lib/dqlite/node1";
    const char *cert_file = "/etc/dqlite/certs/node1/cert.pem";
    const char *key_file = "/etc/dqlite/certs/node1/key.pem";
    const char *ca_file = "/etc/dqlite/certs/ca.crt";

    /* 创建节点 */
    rc = dqlite_node_create(id, data_dir, address, &node);
    if (rc != 0) {
        fprintf(stderr, "Failed to create node\n");
        return EXIT_FAILURE;
    }

    /* 配置 TLS */
    /* 注意:以下 API 为示意,具体函数签名请参考 dqlite.h */
    /*
    rc = dqlite_node_set_tls(node, cert_file, key_file, ca_file);
    if (rc != 0) {
        fprintf(stderr, "Failed to configure TLS: %s\n", dqlite_node_errmsg(node));
        dqlite_node_destroy(node);
        return EXIT_FAILURE;
    }
    */

    /* 启动节点 */
    rc = dqlite_node_start(node);
    if (rc != 0) {
        fprintf(stderr, "Failed to start node: %s\n", dqlite_node_errmsg(node));
        dqlite_node_destroy(node);
        return EXIT_FAILURE;
    }

    printf("TLS-enabled dqlite node started on %s\n", address);

    /* 事件循环 */
    getchar();

    dqlite_node_stop(node);
    dqlite_node_destroy(node);
    return EXIT_SUCCESS;
}

8.2.4 Go 配置 TLS

package main

import (
    "crypto/tls"
    "crypto/x509"
    "fmt"
    "log"
    "os"

    dqlite "github.com/canonical/go-dqlite/v2"
    "github.com/canonical/go-dqlite/v2/driver"
)

func createTLSConfig(certFile, keyFile, caFile string) (*tls.Config, error) {
    // 加载节点证书
    cert, err := tls.LoadX509KeyPair(certFile, keyFile)
    if err != nil {
        return nil, fmt.Errorf("load cert: %w", err)
    }

    // 加载 CA 证书
    caCert, err := os.ReadFile(caFile)
    if err != nil {
        return nil, fmt.Errorf("read CA: %w", err)
    }

    caCertPool := x509.NewCertPool()
    if !caCertPool.AppendCertsFromPEM(caCert) {
        return nil, fmt.Errorf("parse CA cert failed")
    }

    return &tls.Config{
        Certificates: []tls.Certificate{cert},
        RootCAs:      caCertPool,
        ClientCAs:    caCertPool,
        ClientAuth:   tls.RequireAndVerifyClientCert,
        MinVersion:   tls.VersionTLS12,
    }, nil
}

func main() {
    tlsConfig, err := createTLSConfig(
        "/etc/dqlite/certs/node1/cert.pem",
        "/etc/dqlite/certs/node1/key.pem",
        "/etc/dqlite/certs/ca.crt",
    )
    if err != nil {
        log.Fatalf("TLS config failed: %v", err)
    }

    // 创建带 TLS 的 dqlite 节点
    node, err := dqlite.New(1, "127.0.0.1:9001", "/var/lib/dqlite/node1", nil,
        dqlite.WithTLS(tlsConfig),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer node.Close()

    // 创建带 TLS 的驱动
    nodeStore := driver.NewInmemNodeStore()
    nodeStore.Set(context.Background(), []driver.NodeInfo{
        {ID: 1, Address: "127.0.0.1:9001"},
        {ID: 2, Address: "127.0.0.1:9002"},
        {ID: 3, Address: "127.0.0.1:9003"},
    })

    drv, err := driver.New(nodeStore,
        driver.WithTLS(tlsConfig),
    )
    if err != nil {
        log.Fatal(err)
    }
    defer drv.Close()

    fmt.Println("TLS-enabled dqlite cluster started")
}

8.2.5 TLS 配置参数

参数说明推荐值
最低 TLS 版本协议版本TLS 1.2
密码套件加密算法TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
客户端认证双向 TLSRequireAndVerifyClientCert
证书轮换证书有效期90 天(自动化轮换)

8.3 认证机制

8.3.1 认证方式对比

方式安全性复杂度说明
无认证最低开发测试用
TLS 客户端证书推荐生产使用
共享密钥简单场景

8.3.2 TLS 双向认证(Mutual TLS)

双向 TLS 握手流程:

  Client                              Server (dqlite node)
    │                                      │
    │──── ClientHello ─────────────────── ▶│
    │                                      │
    │◀─── ServerHello + ServerCert ────────│
    │     + CertificateRequest              │
    │                                      │
    │──── ClientCert + ClientKeyExchange ─ ▶│
    │     + CertificateVerify               │
    │                                      │
    │     [Server 验证 Client 证书]         │
    │     [Client 验证 Server 证书]         │
    │                                      │
    │◀─── Finished ───────────────────────│
    │                                      │
    │     加密通道建立完成                    │

8.3.3 证书验证策略

// 严格证书验证
tlsConfig := &tls.Config{
    Certificates: []tls.Certificate{cert},
    RootCAs:      caCertPool,
    ClientCAs:    caCertPool,
    ClientAuth:   tls.RequireAndVerifyClientCert,
    MinVersion:   tls.VersionTLS12,

    // 自定义验证逻辑
    VerifyPeerCertificate: func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
        for _, chain := range verifiedChains {
            leaf := chain[0]

            // 检查证书是否过期
            if leaf.NotAfter.Before(time.Now()) {
                return fmt.Errorf("certificate expired: %s", leaf.Subject.CommonName)
            }

            // 检查证书用途
            if leaf.KeyUsage&x509.KeyUsageDigitalSignature == 0 {
                return fmt.Errorf("certificate not valid for digital signature")
            }

            // 检查节点标识(可选)
            // 可以从 CN 或 SAN 中提取节点 ID 并验证
        }
        return nil
    },
}

8.4 访问控制

8.4.1 网络层访问控制

# 使用 iptables 限制 dqlite 端口访问

# 只允许集群节点之间通信
iptables -A INPUT -p tcp --dport 9001 -s 192.168.1.101 -j ACCEPT
iptables -A INPUT -p tcp --dport 9001 -s 192.168.1.102 -j ACCEPT
iptables -A INPUT -p tcp --dport 9001 -s 192.168.1.103 -j ACCEPT
iptables -A INPUT -p tcp --dport 9001 -j DROP

# 只允许应用服务器访问
iptables -A INPUT -p tcp --dport 9001 -s 10.0.0.0/24 -j ACCEPT

8.4.2 systemd 服务安全

# /etc/systemd/system/dqlite.service
[Unit]
Description=dqlite Node
After=network-online.target

[Service]
Type=simple
User=dqlite
Group=dqlite
ExecStart=/usr/local/bin/dqlite-node

# 文件系统安全
ProtectSystem=strict
ReadWritePaths=/var/lib/dqlite
PrivateTmp=true
ProtectHome=true

# 网络安全
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

# 权限限制
NoNewPrivileges=true
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectControlGroups=true

# 资源限制
LimitNOFILE=65536
MemoryMax=512M
CPUQuota=80%

[Install]
WantedBy=multi-user.target

8.4.3 文件系统权限

# 创建专用用户
sudo useradd -r -s /bin/false -d /var/lib/dqlite dqlite

# 设置数据目录权限
sudo mkdir -p /var/lib/dqlite
sudo chown dqlite:dqlite /var/lib/dqlite
sudo chmod 750 /var/lib/dqlite

# 设置证书权限
sudo chown -R dqlite:dqlite /etc/dqlite/certs
sudo chmod 600 /etc/dqlite/certs/*/key.pem
sudo chmod 644 /etc/dqlite/certs/*/cert.pem

8.5 安全最佳实践

8.5.1 安全检查清单

项目检查内容状态
TLS 加密所有通信使用 TLS 1.2+
双向认证节点间和客户端使用 mTLS
证书管理自动化证书轮换
网络隔离防火墙限制端口访问
用户隔离使用专用系统用户运行
文件权限数据目录和证书权限正确
日志审计记录关键操作日志
定期备份备份策略已实施
版本更新使用最新稳定版本
监控告警异常行为告警

8.5.2 证书轮换自动化

#!/bin/bash
# cert-renew.sh - 自动证书轮换脚本

set -euo pipefail

CERT_DIR="/etc/dqlite/certs"
DAYS_BEFORE_EXPIRY=30

check_and_renew() {
    local cert_file="$1"
    local key_file="$2"
    local ca_file="$3"

    # 检查证书是否即将过期
    local expiry_date
    expiry_date=$(openssl x509 -in "$cert_file" -noout -enddate | cut -d= -f2)
    local expiry_epoch
    expiry_epoch=$(date -d "$expiry_date" +%s)
    local now_epoch
    now_epoch=$(date +%s)
    local days_left=$(( (expiry_epoch - now_epoch) / 86400 ))

    if [ "$days_left" -le "$DAYS_BEFORE_EXPIRY" ]; then
        echo "Certificate expires in $days_left days, renewing..."

        # 备份旧证书
        cp "$cert_file" "${cert_file}.bak.$(date +%Y%m%d)"
        cp "$key_file" "${key_file}.bak.$(date +%Y%m%d)"

        # 生成新密钥和证书(具体逻辑根据你的 PKI 方案)
        openssl genrsa -out "${key_file}.new" 2048
        # ... CSR 签发流程 ...

        # 原子替换
        mv "${key_file}.new" "$key_file"
        # mv "${cert_file}.new" "$cert_file"

        # 重载 dqlite 服务
        systemctl reload dqlite
        echo "Certificate renewed and service reloaded"
    else
        echo "Certificate valid for $days_left more days"
    fi
}

# 检查每个节点的证书
for node_dir in "$CERT_DIR"/node*/; do
    echo "Checking $node_dir..."
    check_and_renew \
        "${node_dir}/cert.pem" \
        "${node_dir}/key.pem" \
        "${CERT_DIR}/ca.crt"
done

8.5.3 安全审计日志

// 审计日志中间件
type AuditLogger struct {
    logger *log.Logger
}

func (a *AuditLogger) LogAccess(remoteAddr, operation, database, query string, success bool) {
    status := "SUCCESS"
    if !success {
        status = "FAILURE"
    }

    a.logger.Printf("[%s] %s | %s | %s | %s | %s",
        time.Now().Format(time.RFC3339),
        status,
        remoteAddr,
        operation,
        database,
        query,
    )
}

// 使用示例
auditLog := AuditLogger{logger: log.New(os.Stdout, "[AUDIT] ", 0)}

func handleQuery(remoteAddr, query string) {
    err := db.Exec(query)
    auditLog.LogAccess(remoteAddr, "EXEC", "mydb", query, err == nil)
}

8.6 安全事件响应

8.6.1 常见安全事件处理

事件响应措施
证书泄露立即吊销证书并重新签发
异常连接阻断来源 IP,检查日志
数据泄露评估影响范围,通知相关方
节点被入侵隔离节点,从安全快照恢复
配置被篡改从备份恢复配置

8.6.2 应急响应流程

发现异常
    │
    ├── 是否数据泄露? ──▶ 是 ──▶ 隔离系统 → 评估影响 → 通知
    │
    ├── 是否未授权访问? ──▶ 是 ──▶ 阻断来源 → 检查日志 → 加固
    │
    ├── 是否服务中断? ──▶ 是 ──▶ 检查集群 → 恢复服务
    │
    └── 否 → 记录日志 → 继续监控

本章小结

要点说明
TLS 加密节点间和客户端通信都应使用 TLS
双向认证生产环境推荐 mTLS
网络隔离防火墙限制 dqlite 端口访问
文件权限使用专用用户、最小权限原则
证书管理自动化轮换,定期审计
审计日志记录所有访问和操作

下一章

第 9 章:Docker 与 Kubernetes 部署 — 学习如何使用 Docker Compose 和 Kubernetes 部署 dqlite 集群。