dqlite 分布式 SQLite 教程 / 第 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 |
| 客户端认证 | 双向 TLS | RequireAndVerifyClientCert |
| 证书轮换 | 证书有效期 | 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 集群。