SPIRE 系列之四:实战 Lab — 用零信任身份替代数据库密码分发
Posted on 日 26 4月 2026 in Journal
SPIRE 系列之四:实战 Lab — 用零信任身份替代数据库密码分发
SPIRE 系列第 4 篇 — 前三篇讲概念、架构和安全,这一篇动手做个小实验:应用启动时没有数据库密码,只靠 SPIFFE 身份向 Secret Server 证明“我是谁”。纸上得来终觉浅,配置跑一遍,很多概念就落地了。
系列导航: - 01:从 Workload Identity 到 Zero Trust - 02:SPIRE 架构深度解析 - 03:安全性分析与加固清单 - 04:实战 Lab:用零信任身份替代数据库密码分发
1. 问题:密码分发的困境
一个 Python 订单服务需要连接 PostgreSQL,密码怎么给它?这个问题看起来小,其实是很多系统安全债的起点。
传统做法:
环境变量 DB_PASSWORD=secret123 → docker inspect 可看
配置文件 config.yaml → git 泄露风险
Vault + Token → Token 本身又是一个 secret
核心矛盾:要证明“我是合法的服务”才能拿到密码,但证明身份本身又需要一个凭证。这就是 Secret Zero 问题,像鸡生蛋、蛋生鸡,绕不好就全是秘密。
2. SPIRE 的解法:身份来自"你是谁",而非"你知道什么"
┌─────────────────────────────────────────────────────┐
│ 信任链 │
│ │
│ Linux 内核 (UID/PID/cgroup) │
│ ↓ 内核级证据,无法伪造 │
│ SPIRE Agent (节点常驻,已向 Server 证明身份) │
│ ↓ Unix Domain Socket (仅本机进程可访问) │
│ Order Service (UID=1000 的进程) │
│ ↓ 拿到 JWT-SVID (短期身份令牌) │
│ Secret Server (验证 JWT 签名 + SPIFFE ID) │
│ ↓ 验证通过 │
│ 返回数据库密码 (仅在内存中,不落盘) │
└─────────────────────────────────────────────────────┘
关键点:Order Service 启动时不需要任何密码、Token、证书。它的身份由 SPIRE Agent 通过操作系统内核信息自动证明。
3. 完整架构与组件
┌──────────────────────────────────────────────────────────────┐
│ Docker Compose 环境 │
│ │
│ ┌─────────┐ ┌─────────┐ ┌──────────┐ │
│ │ Postgres│ │ SPIRE │───▶│ SPIRE │ │
│ │ :5432 │ │ Server │ │ Agent │ │
│ └────▲────┘ │ :8081 │ │ (node-1) │ │
│ │ └─────────┘ └────┬─────┘ │
│ │ │UDS │
│ │ ┌──────────────────┴────────┐ │
│ │ ▼ ▼ │
│ │ ┌──────────┐ ┌──────────────┐ │
│ │ │ Order │──JWT-SVID─▶│ Secret │ │
│ │ │ Service │◀──密码─────│ Server │ │
│ │ │ UID=1000 │ │ UID=1001 │ │
│ └────│ :8080 │ │ :8443 │ │
│ └──────────┘ └──────────────┘ │
└──────────────────────────────────────────────────────────────┘
4. Provisioning:服务如何注册到 SPIRE
这一节最容易被忽略。SPIRE 不是看见进程就发证,它需要你先告诉 Server:“哪些工作负载应该获得什么身份”。登记错了,后面验证再严也白搭。
4.1 注册流程全景
Provisioning 三步曲:
Step 1: 注册 Node(节点)
管理员 → spire-server entry create → "Agent 节点 node-1 是可信的"
Step 2: 注册 Workload(工作负载)
管理员 → spire-server entry create → "node-1 上 UID=1000 的进程是 order-service"
管理员 → spire-server entry create → "node-1 上 UID=1001 的进程是 secret-server"
Step 3: 运行时自动颁发身份
Agent 观察到 UID=1000 的进程连接 → 自动颁发 JWT-SVID
无需应用做任何配置
4.2 Node Attestation — Agent 如何向 Server 证明自己
Agent 首次启动时,必须向 Server 证明"我是合法节点":
# Agent 启动时,使用 join_token 方式向 Server 注册
# 生产环境通常用 AWS IID、GCP IIT、K8s PSAT 等自动方式
# Step 1: Server 端生成一次性 join token
docker exec spire-server \
spire-server token generate -spiffeID spiffe://example.org/agent/node-1
# 输出: Token: 3a7b9c...(一次性,用后即焚)
# Step 2: Agent 启动时使用此 token
# agent.conf 中配置:
# plugins {
# NodeAttestor "join_token" {
# plugin_data {}
# }
# }
# 启动命令带上 -joinToken 3a7b9c...
安全性:Join Token 是一次性的,使用后立即失效。Agent 获得节点级 SVID 后,后续通过证书轮换保持身份,不再需要 token。
4.3 Workload Registration — 告诉 SPIRE 谁是谁
# === 注册 Order Service ===
# 含义:"在 node-1 上,UID 为 1000 的进程,身份是 order-service"
docker exec spire-server \
spire-server entry create \
-parentID spiffe://example.org/agent/node-1 \
-spiffeID spiffe://example.org/service/order-service \
-selector unix:uid:1000 \
-dns order-service \
-ttl 300
# === 注册 Secret Server ===
# 含义:"在同一个演示节点 node-1 上,UID 为 1001 的进程,身份是 secret-server"
docker exec spire-server \
spire-server entry create \
-parentID spiffe://example.org/agent/node-1 \
-spiffeID spiffe://example.org/service/secret-server \
-selector unix:uid:1001 \
-dns secret-server \
-ttl 300
4.4 Selector 详解 — SPIRE 如何识别工作负载
Selector 是 SPIRE 识别"谁是谁"的核心机制:
| Selector 类型 | 示例 | 识别依据 | 安全强度 |
|---|---|---|---|
unix:uid |
unix:uid:1000 |
进程的 Linux UID | ⭐⭐⭐ |
unix:gid |
unix:gid:1000 |
进程的 Linux GID | ⭐⭐⭐ |
unix:path |
unix:path:/usr/bin/app |
可执行文件路径 | ⭐⭐⭐⭐ |
unix:sha256 |
unix:sha256:abc123... |
二进制文件哈希 | ⭐⭐⭐⭐⭐ |
k8s:pod-label |
k8s:pod-label:app:order |
K8s Pod 标签 | ⭐⭐⭐ |
k8s:sa |
k8s:sa:order-sa |
K8s ServiceAccount | ⭐⭐⭐⭐ |
docker:label |
docker:label:com.app:order |
Docker 容器标签 | ⭐⭐⭐ |
多 Selector 组合(AND 逻辑,更安全):
spire-server entry create \
-parentID spiffe://example.org/agent/node-1 \
-spiffeID spiffe://example.org/service/order-service \
-selector unix:uid:1000 \
-selector unix:path:/app/order_service.py \
-selector unix:sha256:$(sha256sum /app/order_service.py | cut -d' ' -f1)
4.5 运行时身份颁发 — 自动发生,应用无感
注册完成后,当 Order Service 进程连接 SPIRE Agent 的 Unix Domain Socket 时:
Order Service (PID=42, UID=1000)
│
│ connect(/run/spire/agent.sock)
▼
SPIRE Agent
│ 1. 通过 SO_PEERCRED 获取 PID=42
│ 2. 读取 /proc/42/status → UID=1000
│ 3. 读取 /proc/42/exe → /app/order_service.py
│ 4. 匹配注册条目:unix:uid:1000 → order-service ✓
│ 5. 签发 JWT-SVID(或从缓存返回)
▼
返回 JWT-SVID:
sub: spiffe://example.org/service/order-service
aud: ["secret-server"]
exp: 1700000300 (5分钟后)
整个过程应用代码只需一行:
jwt_svid = workload_client.fetch_jwt_svid(audiences=["secret-server"])
4.6 Provisioning 在不同环境的实现
| 环境 | Node Attestation | Workload Selector | 自动化方式 |
|---|---|---|---|
| 开发/Docker | Join Token | unix:uid |
setup.sh 脚本,通常一个演示 Agent 承载多个进程 |
| AWS EC2 | AWS IID (自动) | unix:uid + unix:path |
Terraform |
| Kubernetes | K8s PSAT (自动) | k8s:sa + k8s:ns |
Helm + CRD (SPIRE Controller Manager) |
| 裸金属 | X.509 证书 | unix:sha256 |
Ansible |
K8s 生产环境示例(使用 CRD 自动注册):
# 部署了 SPIRE Controller Manager 后,只需创建 CRD
apiVersion: spire.spiffe.io/v1alpha1
kind: ClusterSPIFFEID
metadata:
name: order-service
spec:
spiffeIDTemplate: "spiffe://{{ .TrustDomain }}/ns/{{ .PodMeta.Namespace }}/sa/{{ .PodSpec.ServiceAccountName }}"
podSelector:
matchLabels:
app: order-service
namespaceSelector:
matchLabels:
environment: production
这样新 Pod 部署时 自动注册身份,无需手动执行 spire-server entry create。
5. 可运行代码
5.1 docker-compose.yaml
version: "3.8"
services:
# --- 数据库 ---
postgres:
image: postgres:15-alpine
environment:
POSTGRES_DB: orders
POSTGRES_USER: order_user
# 这个密码只有 postgres 和 secret-server 知道
# order-service 永远不会直接接触到它
POSTGRES_PASSWORD: "super-secret-db-pass-2024"
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U order_user -d orders"]
interval: 5s
retries: 5
# --- SPIRE Server ---
spire-server:
image: ghcr.io/spiffe/spire-server:1.9.1
command: ["-config", "/opt/spire/conf/server.conf"]
volumes:
- ./server/server.conf:/opt/spire/conf/server.conf:ro
- spire-server-data:/opt/spire/data
ports: ["8081:8081"]
healthcheck:
test: ["CMD", "spire-server", "healthcheck"]
interval: 5s
retries: 10
# --- SPIRE Agent ---
spire-agent:
image: ghcr.io/spiffe/spire-agent:1.9.1
command: ["-config", "/opt/spire/conf/agent.conf"]
volumes:
- ./agent/agent.conf:/opt/spire/conf/agent.conf:ro
- spire-agent-socket:/run/spire
- /proc:/proc:ro # 读取进程信息用于 workload attestation
depends_on:
spire-server:
condition: service_healthy
privileged: true # 需要读取 /proc/<pid>/status
# --- Secret Server (UID=1001) ---
secret-server:
build:
context: ./apps
args:
APP_UID: 1001
command: ["python", "secret_server.py"]
user: "1001"
environment:
# Secret Server 是唯一知道数据库密码的服务
DB_PASSWORD: "super-secret-db-pass-2024"
SPIRE_AGENT_SOCKET: "/run/spire/agent.sock"
volumes:
- spire-agent-socket:/run/spire:ro
ports: ["8443:8443"]
depends_on: [spire-agent]
# --- Order Service (UID=1000) ---
# 注意:这里没有任何密码相关的环境变量!
order-service:
build:
context: ./apps
args:
APP_UID: 1000
command: ["python", "order_service.py"]
user: "1000"
environment:
SPIRE_AGENT_SOCKET: "/run/spire/agent.sock"
SECRET_SERVER_URL: "http://secret-server:8443"
volumes:
- spire-agent-socket:/run/spire:ro
ports: ["8080:8080"]
depends_on: [spire-agent, postgres]
volumes:
spire-server-data:
spire-agent-socket:
5.2 SPIRE Server 配置
# server/server.conf
server {
bind_address = "0.0.0.0"
bind_port = "8081"
trust_domain = "example.org"
data_dir = "/opt/spire/data/server"
log_level = "INFO"
ca_ttl = "24h"
default_jwt_svid_ttl = "5m"
}
plugins {
DataStore "sql" {
plugin_data {
database_type = "sqlite3"
connection_string = "/opt/spire/data/server/datastore.sqlite3"
}
}
KeyManager "memory" {
plugin_data {}
}
NodeAttestor "join_token" {
plugin_data {}
}
}
5.3 SPIRE Agent 配置
# agent/agent.conf
agent {
data_dir = "/opt/spire/data/agent"
log_level = "INFO"
server_address = "spire-server"
server_port = "8081"
socket_path = "/run/spire/agent.sock"
trust_domain = "example.org"
insecure_bootstrap = true # 仅用于演示,生产环境应使用 trust_bundle_path
}
plugins {
KeyManager "memory" {
plugin_data {}
}
NodeAttestor "join_token" {
plugin_data {}
}
WorkloadAttestor "unix" {
plugin_data {
discover_workload_path = true
}
}
}
5.4 应用 Dockerfile
# apps/Dockerfile
FROM python:3.11-slim
ARG APP_UID=1000
RUN useradd -u ${APP_UID} -m appuser
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY *.py .
USER ${APP_UID}
5.5 依赖文件
# apps/requirements.txt
PyJWT[crypto]==2.8.0
cryptography==42.0.0
requests==2.31.0
psycopg2-binary==2.9.9
flask==3.0.0
5.6 Secret Server — 验证身份后才给密码
# apps/secret_server.py
"""
Secret Server: 持有数据库密码,仅在验证调用方 SPIFFE 身份后才返回。
"""
import os
import json
import logging
import urllib.request
from flask import Flask, request, jsonify
import jwt
app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("secret-server")
DB_PASSWORD = os.environ["DB_PASSWORD"]
TRUST_DOMAIN = "example.org"
# 允许获取密码的 SPIFFE ID 白名单
ALLOWED_SPIFFE_IDS = {
f"spiffe://{TRUST_DOMAIN}/service/order-service",
}
def get_jwks():
"""从 SPIRE Agent 获取 JWKS 公钥(用于验证 JWT-SVID 签名)"""
# 生产环境从 SPIRE Agent Workload API 获取
# 演示环境从 SPIRE Server 的 bundle endpoint 获取
url = "http://spire-server:8081/keys"
resp = urllib.request.urlopen(url)
return json.loads(resp.read())
@app.route("/secret/db-password", methods=["GET"])
def get_db_password():
# --- Step 1: 提取 JWT-SVID ---
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
log.warning("请求缺少 Bearer token")
return jsonify({"error": "missing token"}), 401
token = auth_header.split(" ", 1)[1]
# --- Step 2: 验证 JWT 签名 ---
try:
jwks = get_jwks()
# 解码 JWT header 获取 kid
header = jwt.get_unverified_header(token)
kid = header.get("kid")
# 从 JWKS 中找到对应的公钥
public_key = None
for key_data in jwks.get("keys", []):
if key_data.get("kid") == kid:
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(json.dumps(key_data))
break
if not public_key:
log.warning(f"未找到 kid={kid} 对应的公钥")
return jsonify({"error": "unknown signing key"}), 401
# 验证签名、过期时间、audience
claims = jwt.decode(
token,
public_key,
algorithms=["RS256", "ES256"],
audience="secret-server",
)
except jwt.ExpiredSignatureError:
log.warning("JWT-SVID 已过期")
return jsonify({"error": "token expired"}), 401
except jwt.InvalidTokenError as e:
log.warning(f"JWT 验证失败: {e}")
return jsonify({"error": "invalid token"}), 401
# --- Step 3: 检查 SPIFFE ID 白名单 ---
spiffe_id = claims.get("sub", "")
if spiffe_id not in ALLOWED_SPIFFE_IDS:
log.warning(f"SPIFFE ID 不在白名单中: {spiffe_id}")
return jsonify({"error": "unauthorized identity"}), 403
# --- Step 4: 身份验证通过,返回密码 ---
log.info(f"✅ 身份验证通过: {spiffe_id} → 返回数据库密码")
return jsonify({
"db_host": "postgres",
"db_port": 5432,
"db_name": "orders",
"db_user": "order_user",
"db_password": DB_PASSWORD,
"ttl_seconds": 300, # 建议客户端 5 分钟后重新获取
})
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok"})
if __name__ == "__main__":
log.info("Secret Server 启动,监听 :8443")
app.run(host="0.0.0.0", port=8443)
5.7 Order Service — 零密码启动
# apps/order_service.py
"""
Order Service: 启动时没有任何密码。
通过 SPIRE 获取身份,再用身份从 Secret Server 获取数据库密码。
"""
import os
import time
import json
import socket
import logging
import threading
import psycopg2
import requests
from flask import Flask, jsonify
app = Flask(__name__)
logging.basicConfig(level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s")
log = logging.getLogger("order-service")
SECRET_SERVER_URL = os.environ.get("SECRET_SERVER_URL", "http://secret-server:8443")
SPIRE_AGENT_SOCKET = os.environ.get("SPIRE_AGENT_SOCKET", "/run/spire/agent.sock")
# 数据库连接(运行时动态获取密码后建立)
db_conn = None
db_config = None
def fetch_jwt_svid_from_agent(audience="secret-server"):
"""
通过 Unix Domain Socket 从 SPIRE Agent 获取 JWT-SVID。
这是 SPIFFE Workload API 的核心调用。
Agent 会:
1. 通过 SO_PEERCRED 获取调用进程的 PID
2. 读取 /proc/<PID>/status 获取 UID
3. 匹配注册条目
4. 签发或返回缓存的 JWT-SVID
"""
import urllib.request
import urllib.parse
# SPIFFE Workload API 通过 Unix Domain Socket 通信
# 使用 HTTP over UDS
params = urllib.parse.urlencode({"audience": audience})
url = f"http://localhost/v1/auth/jwt-svids?{params}"
# 创建 Unix Socket 连接
class UDSHandler(urllib.request.HTTPHandler):
def http_open(self, req):
return self.do_open(UDSConnection, req)
class UDSConnection:
def __init__(self, host, timeout=10):
self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
self.sock.connect(SPIRE_AGENT_SOCKET)
self.sock.settimeout(timeout)
def request(self, method, url, body=None, headers={}):
path = url.split("localhost", 1)[1] if "localhost" in url else url
req_str = f"{method} {path} HTTP/1.1\r\nHost: localhost\r\n"
# SPIFFE Workload API 要求设置安全 header
req_str += "Security: true\r\n"
for k, v in headers.items():
req_str += f"{k}: {v}\r\n"
req_str += "\r\n"
self.sock.sendall(req_str.encode())
# 读取响应
response = b""
while True:
chunk = self.sock.recv(4096)
if not chunk:
break
response += chunk
return response
# 简化版:直接用 requests-unixsocket 或手动 socket 调用
# 这里用最直观的方式展示原理
try:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
sock.connect(SPIRE_AGENT_SOCKET)
sock.settimeout(10)
# 构造 HTTP 请求
http_req = (
f"GET /v1/auth/jwt-svids?audience={audience} HTTP/1.1\r\n"
f"Host: localhost\r\n"
f"Security: true\r\n" # SPIFFE Workload API 必需的 header
f"Connection: close\r\n"
f"\r\n"
)
sock.sendall(http_req.encode())
# 读取响应
response = b""
while True:
chunk = sock.recv(4096)
if not chunk:
break
response += chunk
sock.close()
# 解析 HTTP 响应 body
body = response.split(b"\r\n\r\n", 1)[1]
data = json.loads(body)
jwt_svid = data["svids"][0]["svid"]
spiffe_id = data["svids"][0]["spiffe_id"]
log.info(f"✅ 获取 JWT-SVID 成功: {spiffe_id}")
return jwt_svid
except Exception as e:
log.error(f"❌ 获取 JWT-SVID 失败: {e}")
raise
def fetch_db_password():
"""用 JWT-SVID 从 Secret Server 获取数据库密码"""
jwt_token = fetch_jwt_svid_from_agent(audience="secret-server")
resp = requests.get(
f"{SECRET_SERVER_URL}/secret/db-password",
headers={"Authorization": f"Bearer {jwt_token}"},
timeout=5,
)
if resp.status_code == 200:
config = resp.json()
log.info(f"✅ 获取数据库密码成功 (TTL={config['ttl_seconds']}s)")
return config
else:
log.error(f"❌ 获取密码失败: {resp.status_code} {resp.text}")
raise RuntimeError(f"Secret Server 返回 {resp.status_code}")
def connect_db():
"""使用动态获取的密码连接数据库"""
global db_conn, db_config
db_config = fetch_db_password()
db_conn = psycopg2.connect(
host=db_config["db_host"],
port=db_config["db_port"],
dbname=db_config["db_name"],
user=db_config["db_user"],
password=db_config["db_password"],
)
log.info("✅ 数据库连接成功")
def password_refresh_loop():
"""后台线程:定期刷新数据库密码"""
while True:
time.sleep(240) # 每 4 分钟刷新
try:
log.info("🔄 刷新数据库密码...")
connect_db()
except Exception as e:
log.error(f"密码刷新失败: {e}")
@app.route("/orders", methods=["GET"])
def list_orders():
try:
cur = db_conn.cursor()
cur.execute("SELECT 1") # 简单查询验证连接
return jsonify({"status": "ok", "message": "database connected", "result": cur.fetchone()})
except Exception as e:
return jsonify({"error": str(e)}), 500
@app.route("/health", methods=["GET"])
def health():
return jsonify({"status": "ok", "has_db": db_conn is not None})
if __name__ == "__main__":
log.info("=" * 60)
log.info("Order Service 启动")
log.info("注意:启动参数中没有任何密码!")
log.info("=" * 60)
# 启动时动态获取密码并连接数据库
max_retries = 30
for i in range(max_retries):
try:
connect_db()
break
except Exception as e:
log.warning(f"等待依赖服务就绪... ({i+1}/{max_retries}): {e}")
time.sleep(2)
else:
log.error("无法连接数据库,退出")
exit(1)
# 启动后台密码刷新线程
threading.Thread(target=password_refresh_loop, daemon=True).start()
log.info("Order Service 就绪,监听 :8080")
app.run(host="0.0.0.0", port=8080)
6. Provisioning 脚本 — 一键注册所有身份
#!/bin/bash
# setup.sh — 完整的 Provisioning 脚本
set -euo pipefail
echo "=========================================="
echo " SPIRE Provisioning — 身份注册"
echo "=========================================="
SPIRE_SERVER="docker exec spire-server spire-server"
# --- Step 1: 等待 SPIRE Server 就绪 ---
echo ""
echo "[Step 1/4] 等待 SPIRE Server 就绪..."
for i in $(seq 1 30); do
if $SPIRE_SERVER healthcheck 2>/dev/null; then
echo " ✅ SPIRE Server 就绪"
break
fi
echo " 等待中... ($i/30)"
sleep 2
done
# --- Step 2: 生成 Join Token 并启动 Agent ---
echo ""
echo "[Step 2/4] 为 Agent 生成 Join Token..."
JOIN_TOKEN=$($SPIRE_SERVER token generate \
-spiffeID spiffe://example.org/agent/node-1 \
-ttl 600 | awk '{print $2}')
echo " ✅ Token 已生成(一次性,10分钟有效)"
echo " ⚠️ 此 Token 使用后立即失效,无法重放"
# Agent 使用此 token 启动(在 docker-compose 中已配置)
echo " 📝 Agent 启动命令:"
echo " spire-agent run -joinToken $JOIN_TOKEN"
# --- Step 3: 注册工作负载身份 ---
echo ""
echo "[Step 3/4] 注册工作负载身份..."
# 注册 Order Service
echo ""
echo " 注册 Order Service:"
echo " Parent: spiffe://example.org/agent/node-1"
echo " SPIFFE ID: spiffe://example.org/service/order-service"
echo " Selector: unix:uid:1000"
$SPIRE_SERVER entry create \
-parentID spiffe://example.org/agent/node-1 \
-spiffeID spiffe://example.org/service/order-service \
-selector unix:uid:1000 \
-dns order-service \
-ttl 300 \
2>/dev/null || echo " (条目可能已存在)"
echo " ✅ Order Service 已注册"
# 注册 Secret Server
echo ""
echo " 注册 Secret Server:"
echo " Parent: spiffe://example.org/agent/node-1"
echo " SPIFFE ID: spiffe://example.org/service/secret-server"
echo " Selector: unix:uid:1001"
$SPIRE_SERVER entry create \
-parentID spiffe://example.org/agent/node-1 \
-spiffeID spiffe://example.org/service/secret-server \
-selector unix:uid:1001 \
-dns secret-server \
-ttl 300 \
2>/dev/null || echo " (条目可能已存在)"
echo " ✅ Secret Server 已注册"
# --- Step 4: 验证注册结果 ---
echo ""
echo "[Step 4/4] 验证注册结果..."
echo ""
$SPIRE_SERVER entry show
echo ""
echo "=========================================="
echo " ✅ Provisioning 完成!"
echo "=========================================="
echo ""
echo "已注册的信任关系:"
echo " Linux Kernel (UID=1000) → SPIRE Agent → order-service 身份"
echo " Linux Kernel (UID=1001) → SPIRE Agent → secret-server 身份"
echo ""
echo "现在 order-service 可以:"
echo " 1. 自动从 Agent 获取 JWT-SVID(无需密码)"
echo " 2. 用 JWT-SVID 向 secret-server 证明身份"
echo " 3. 获取数据库密码(仅在内存中)"
echo ""
echo "测试命令:"
echo " curl http://localhost:8080/orders"
7. 安全性分析:为什么密码不会泄露
7.1 密码在哪里?不在哪里?
✅ 密码存在的地方(受控):
1. PostgreSQL 内部 — 数据库自身管理
2. Secret Server 进程内存 — 唯一知道密码的服务
3. Order Service 进程内存(临时) — 获取后仅存内存,4分钟刷新
❌ 密码不存在的地方(消除攻击面):
1. Order Service 的代码 — 代码中无任何密码
2. Order Service 的配置文件 — 无 config.yaml
3. Order Service 的环境变量 — docker inspect 看不到
4. Order Service 的启动命令 — ps aux 看不到
5. 网络传输中的明文 — JWT 签名保护
6. 任何磁盘文件 — 全程内存操作
7. 日志文件 — 不记录密码
8. Git 仓库 — 代码中无密码
7.2 攻击者要突破需要什么?
攻击场景 1: 偷取 JWT-SVID
❌ JWT 5分钟过期
❌ audience 绑定为 "secret-server",不能用于其他服务
❌ 传输通过 Unix Domain Socket,不经过网络
攻击场景 2: 伪造 SPIFFE 身份
❌ 需要 SPIRE Server 的 CA 私钥才能签发有效 JWT
❌ CA 私钥仅在 Server 内存中
攻击场景 3: 冒充 Order Service
❌ 需要在同一节点上以 UID=1000 运行进程
❌ 如果加了 unix:sha256 selector,还需要相同的二进制哈希
❌ 即使冒充成功,拿到的密码 5 分钟后 Secret Server 可轮换
攻击场景 4: 中间人攻击
❌ Agent 通信走 Unix Domain Socket(不经过网络栈)
❌ Secret Server 验证 JWT 签名(无法篡改)
7.3 与传统方案对比
| 攻击面 | 环境变量 | Vault+Token | SPIRE |
|---|---|---|---|
docker inspect 可见 |
⚠️ 是 | ⚠️ Token 可见 | ✅ 无 |
| Git 泄露风险 | ⚠️ 高 | ⚠️ Token 泄露 | ✅ 无 |
| 日志意外打印 | ⚠️ 可能 | ⚠️ 可能 | ✅ 无 |
| 凭证过期自动轮换 | ❌ 不会 | ⚠️ 需配置 | ✅ 自动 |
| Secret Zero 问题 | ❌ 存在 | ❌ Token 是 Secret Zero | ✅ 消除 |
| 身份可伪造 | ❌ 任何人可设置环境变量 | ⚠️ 偷到 Token 即可 | ✅ 内核级证据 |
8. 运行实验
# 1. 启动所有服务
cd spire-hands-on-lab/
docker-compose up -d
# 2. 执行 Provisioning(注册身份)
bash setup.sh
# 3. 测试正常调用
curl http://localhost:8080/orders
# 返回: {"status": "ok", "message": "database connected"}
# 4. 观察 Order Service 日志 — 看到完整的身份获取流程
docker logs order-service
# 输出:
# Order Service 启动
# 注意:启动参数中没有任何密码!
# ✅ 获取 JWT-SVID 成功: spiffe://example.org/service/order-service
# ✅ 获取数据库密码成功 (TTL=300s)
# ✅ 数据库连接成功
# 5. 确认 Order Service 容器中没有任何密码
docker exec order-service env | grep -i pass
# (无输出 — 环境变量中没有密码)
docker exec order-service cat /proc/1/cmdline | tr '\0' ' '
# python order_service.py(命令行中没有密码)
9. 总结
传统方式: SPIRE 方式:
代码写死密码 ──────────▶ 泄露 代码中零密码 ──────────▶ 安全
配置文件存密码 ─────────▶ 泄露 配置中零密码 ──────────▶ 安全
环境变量传密码 ─────────▶ 泄露 环境变量零密码 ────────▶ 安全
Vault Token ──────────▶ 又一个密码 身份来自内核 ──────────▶ 无需密码
人工轮换 ─────────────▶ 遗忘 自动轮换 ──────────────▶ 持续安全
SPIRE 的核心价值:把“你知道什么密码”变成“你是谁”。身份由操作系统和平台证据证明,再由 SPIRE 签发短期凭证。应用少拿一个长期密码,系统就少一个长期隐患。
当然,真实生产环境不会像这个 Lab 这么简单。你还要接入 KMS、PostgreSQL datastore、Kubernetes PSAT、CSI Driver、审计和告警。可是方向已经很清楚:先让工作负载拿到可信身份,再让权限围绕身份收敛。目的无他,少藏密码,多验证身份。
系列文章: - 第 1 篇:从 Workload Identity 到 Zero Trust - 第 2 篇:SPIRE 架构深度解析 - 第 3 篇:安全性分析与加固清单 - 第 4 篇:实战 Lab — 用零信任身份替代数据库密码分发(本文)
上一篇:SPIRE 系列之三:安全性分析与加固清单 — 信任链、攻击面与生产加固策略。