用 Ghostunnel 和 SPIRE 为 NGINX 提供 SPIFFE 认证
之前对 SPIFFE 和 SPIRE 进行了一个相对全面/啰嗦的介绍,这一篇就反过来,用一个简单的例子来展示 SPIRE 的基本用法,本文中会以 NGINX 作为服务生产方,使用 Ghostunnel 当做 NGINX 的反向代理,把原有的 HTTP 通信升级为支持定期正顺轮转的双向 TLS 认证协议,并且用 CURL 使用客户端证书来通过 Ghostunnel 安全地访问背后的 NGINX。这里为 CURL 和 NGINX 提供证书以及轮转的,就是 SPIRE 的 Server 和 Agent。
Ghostunnel 是一个简单的 TLS 代理,能为非 TLS 的后端提供双向认证能力。Ghostunnel 能够以服务端(反向代理)或者客户端(代理)的模式进行工作,类似 stunnel。不同的是,他还支持访问控制、证书轮转、ACME 以及最近总在唠叨的 SPIFFE。
本文中会演示的过程实际上是 Ghostunnel 的 SPIFFE DEMO 的一个精简版,会略细致讲述每个步骤涉及的内容。整个过程分为如下一些环节:
- 环境准备:准备运行环境,包括 SPIRE Agent/Server 的构建、NGINX 的安装、以及 Ghostunnel 的构建等
- 编写 SPIRE Server 配置,并启动
- 生成 Ghostunnel 以及 CURL 的 Agent Token,并编写配置文件启动对应的 SPIRE Agent
- 启动 Ghostunnel
- 获取 CURL 客户端证书并测试连接
环境准备
这里使用的是基于 ARM 的一个 Ubuntu 系统,使用 APT 安装并启动 NGINX。另外后续步骤还需要 GIT 工具以及连接 GITHUB,并使用 GOLANG 构建 SPIRE 以及 Ghostunnel。
GIT 获取 SPIRE 版本,并进行构建:
$ git clone --single-branch --branch v1.4.0 https://github.com/spiffe/spire.git
Cloning into 'spire'...
...
$ cd spire
$ make bin/spire-agent
Installing go1.18.4...
Building bin/spire-agent...
$ make bin/spire-server
Building bin/spire-server...
接下来获取 Ghostunnel 并进行构建:
$ git clone https://github.com/ghostunnel/ghostunnel.git
Cloning into 'ghostunnel'...
...
$ make ghostunnel
go build -ldflags '-X main.version=v1.6.1-25-g8ae18ea' -o ghostunnel .
...
构建成功后,把三个新生成的可执行文件拷贝到可见目录备用。
然后建立测试目录,大致目录结构如下:
- spire-101
- certs
- conf
- data
- logs
- socks
编写 SPIRE Server 配置并启动服务
server {
bind_address = "0.0.0.0"
bind_port = "8081"
socket_path = "socks/spire-server.sock"
trust_domain = "spiffe.dom"
data_dir = "data/spire-server"
log_level = "DEBUG"
ca_ttl = "30m"
default_svid_ttl = "2m"
ca_subject = {
country = ["CN"],
organization = ["FUNNY"],
common_name = "",
}
}
plugins {
DataStore "sql" {
plugin_data {
database_type = "sqlite3"
connection_string = "data/spire-server/datastore.sqlite3"
}
}
NodeAttestor "join_token" {
plugin_data {
}
}
KeyManager "disk" {
plugin_data {
keys_path = "data/spire-server/keys.json"
}
}
}
此处配置文件的几个要点:
- TCP 监听
0.0.0.0:8081
- 监听 Socket 路径为
socks/spire-server.sock
- 使用
spiffe.dom
作为信任域 - SVID 的默认寿命为 2 分钟
- 使用 SQLite3 作为数据存储引擎,数据库文件保存在
data/spire-server/datastore.sqlite3
- 在本地存储 Key,路径为
data/spire-server/keys.json
。
然后用这个配置文件启动 SPIRE Server:spire-server run -config conf/spire-server.conf > logs/spire-server.log 2>&1 &
启动 Agent
这个小实验需要用到两个 Agent,分别负责服务端和客户端的身份。在运行 Agent 之前,首先要获取 SPIRE Server 的 Trust Bundle:
$ spire-server bundle show \
-socketPath socks/spire-server.sock > conf/bundle.crt
上述命令将 Trunst Bundle 保存到文件 conf/bundle.crt
。
服务端 Agent 配置文件如下:
agent {
data_dir = "data/server-side-agent"
log_level = "DEBUG"
server_address = "127.0.0.1"
server_port = "8081"
socket_path ="socks/server-side-agent.sock"
trust_bundle_path = "conf/bundle.crt"
trust_domain = "spiffe.dom"
}
plugins {
NodeAttestor "join_token" {
plugin_data {
}
}
KeyManager "disk" {
plugin_data {
directory = "data/server-side-agent"
}
}
WorkloadAttestor "unix" {
plugin_data {
discover_workload_path = true
}
}
}
这个配置的要点是:
- 使用
127.0.0.1:8081
作为 SPIRE Server - 监听
socks/spire-server.sock
- 信任
conf/bundle.crt
- Unix Workload Attestor 中开放了选项
discover_workload_path
,从而可以通过二进制文件位置或者哈希识别调用 Agent 的应用的身份
为这个 Agent 创建一个 Token,用于标识 Agent 的身份:
$ spire-server token generate \
-socketPath socks/spire-server.sock \
-spiffeID spiffe://spiffe.dom/server-node
Token: [Token Hash]
上面命令生成了一个 Token,其 SPIFFE ID 为 spiffe://spiffe.dom/server-node
。然后启动服务侧 Agent:
$ spire-agent run \
-config conf/client-side-agent.conf \
-joinToken [Token Hash] > logs/client-side-agent.log 2>&1 &
接下来启动客户侧的 Agent,配置文件如下:
agent {
data_dir = "data/client-side-agent"
log_level = "DEBUG"
server_address = "127.0.0.1"
server_port = "8081"
socket_path ="socks/client-side-agent.sock"
trust_bundle_path = "conf/bundle.crt"
trust_domain = "spiffe.dom"
}
plugins {
NodeAttestor "join_token" {
plugin_data {
}
}
KeyManager "disk" {
plugin_data {
directory = "data/client-side-agent"
}
}
WorkloadAttestor "unix" {
plugin_data {
}
}
}
跟上面的类似,我们也需要创建 Token 之后才能启动 Agent:
$ spire-server token generate \
-socketPath socks/spire-server.sock \
-spiffeID spiffe://spiffe.dom/client-node
Token: [Token Hash]
使用上述 Token 和 配置文件启动 Agent:
$ spire-agent run \
-config conf/client-side-agent.conf \
-joinToken "$TOKEN" > logs/client-side-agent.log 2>&1 &
启动 Ghostunnel
首先要给 Ghostunnel 一个身份,也就是 Entry:
$ spire-server entry create \
-selector unix:path:/usr/local/bin/ghostunnel \
-socketPath socks/spire-server.sock \
-spiffeID spiffe://spiffe.dom/ghost \
-parentID spiffe://spiffe.dom/server-node
Entry ID : fe4b1fd5-9e0a-440b-b08e-5c2c886b6a6e
SPIFFE ID : spiffe://spiffe.dom/ghost
Parent ID : spiffe://spiffe.dom/server-node
Revision : 0
TTL : default
Selector : unix:path:/usr/local/bin/ghostunnel
上面的命令参数解释如下:
- selector:类似 Kubernetes 中的 Label Selector,用 Workload 属性来界定身份,这里使用的是二进制路径:
unix:path:/usr/local/bin/ghostunnel
,此文件启动之后,可以使用 Workload API 向 Agent 请求 SVID - socketPath:指定 SPIRE Server 的监听 Socket
- spiffeID:Workload 的 SPIFFE ID
- parentID:Node 的 SPIFFE ID
创建这个 Entry 之后,SPIRE Server 会据此创建 SVID 下发给 Agent,Agent 只要根据 Selector 判断 Workload 身份,如果符合就可以发放 SVID 了。
接下来启动 Ghostunnel:
$ ghostunnel server \
--use-workload-api-addr unix:///$(pwd)/socks/server-side-agent.sock \
--listen=0.0.0.0:9099 \
--target=localhost:80 \
--allow-uri=spiffe://spiffe.dom/curl
这里使用了一个参数 --use-workload-api-addr
,要求使用 SPIFFE Workload API,对应 Agent Socket 为前面生成的 socks/server-side-agent.sock
。--listen
和 --target
分别代表了监听端口和被代理端口(也就是 NGINX)。而 --allow-uri
参数则是一种访问控制手段,此处是允许 spiffe://spiffe.dom/curl
的 SPIFFE ID 访问本服务。除了这种死板的方式之外,Ghostunnel 还能对接 OPA 实现更加复杂的符合生产要求的策略管控能力。
如果此时用浏览器或者 CURL 访问该节点的 9099 端口,就会出现客户端证书不匹配的错误。
获取 CURL 客户端证书并测试连接
类似的,我们给 CURL 创建一个 SVID:
$ spire-server entry create \
-selector unix:uid:1000 \
-socketPath socks/spire-server.sock \
-spiffeID spiffe://spiffe.dom/curl \
-ttl 600 \
-parentID spiffe://spiffe.dom/client-node
Entry ID : 50911ef7-f191-4917-adde-1bf4e6192002
SPIFFE ID : spiffe://spiffe.dom/curl
Parent ID : spiffe://spiffe.dom/client-node
Revision : 0
TTL : 600
Selector : unix:uid:1000
因为我们用的是 CURL,并不具备直接访问 Workload API 的能力,所以这里用了比较特别的参数:
- Selector 设置为当前用户的 ID,也就是说该用户执行的进程是可以匹配到这个 Entry 从而获取 SVID 的
- 设置了 10 分钟的 TTL,满足我们后续手动操作的需要
然后用 spire-agent api fetch
的方式获取证书:
$ spire-agent api fetch \
--socketPath socks/client-side-agent.sock \
-write certs
命令执行后,会在 certs
发现导出的证书文件,CURL 加上这个证书就能成功访问到 NGINX 了。
$ curl -kv https://127.0.0.1:9099 \
--cert certs/svid.0.pem --key certs/svid.0.key
* Trying 127.0.0.1:9099...
* Connected to 127.0.0.1 (127.0.0.1) port 9099 (#0)
* ALPN, offering h2
* ALPN, offering http/1.1
* TLSv1.0 (OUT), TLS header, Certificate Status (22):
...
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
...
然后
如果观察 logs
目录中的日志,会看到在两个 Agent 的目录中会频频出现 Node 和 Workload 的 SVID 轮转的信息。那么如果 Server 挂了呢?这里就会发现,SPIRE Server 是系统中的一个单点,各个 Node 会因为 SVID 无法更新而异常退出,例如:
level=error msg="Agent crashed" error="current SVID has already expired and rotation failed: failed to dial dns:///127.0.0.1:8081: connection error: desc = \"transport: error while dialing: dial tcp 127.0.0.1:8081: connect: connection refused\""
因此需要对 SPIRE Server 进行高可用部署。另外这个手工过程中我们也会看到,手工创建 Entry、传播 Bundle 以及获取证书、参数授权等,是不可能适应快速变更的云服务环境的,因此自动注册机制、策略执行机制以及相应的防篡改机制都是 SPIFFE 体系落地的必要条件。
后续还会根据这些问题进行进一步的尝试。