Skip to main content

Command Palette

Search for a command to run...

用 Ghostunnel 和 SPIRE 为 NGINX 提供 SPIFFE 认证

Updated
4 min read

之前对 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 的一个精简版,会略细致讲述每个步骤涉及的内容。整个过程分为如下一些环节:

  1. 环境准备:准备运行环境,包括 SPIRE Agent/Server 的构建、NGINX 的安装、以及 Ghostunnel 的构建等

  2. 编写 SPIRE Server 配置,并启动

  3. 生成 Ghostunnel 以及 CURL 的 Agent Token,并编写配置文件启动对应的 SPIRE Agent

  4. 启动 Ghostunnel

  5. 获取 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 体系落地的必要条件。

后续还会根据这些问题进行进一步的尝试。

More from this blog

龙虾恐慌:AIOps 又要改名了?

ChatGPT 开始,把 AI 拉近到普罗大众的面前,让无数人感受到 AI 的亲民魅力。而龙虾,则把大模型驱动的自动化能力,突然间变得水灵灵、活泼泼地走进千家万户。它不只是“风口上的猪”,而是风口本身。热度高到让 Mac mini 一度断货,不知道这在不在库克的预料之内。 每代人都有每代人的鸡蛋,春节期间,我就领了我的鸡蛋。翻出古老的 MacBook Air M1,充值各种大模型。当然了,这个工具

Mar 9, 20261 min read

再见 2025

我猜不少人以为这个号废了吧?并没有,只是今年变化有点大,一直有种抄起键盘,无从说起的感觉,所以一直偷懒到今天,2025 的最后一天。 今年是我的第四个本命年,去年末一期播客里,大内说本命年不是灾年,是变化年,有危也有机。可是讲真啊,只看到危,没看到机。 各种因缘际会,从鹅厂跳槽到前东家,已经接近四年,第一个合同期已经进入尾声。除了前两年还在云原生领域嗷嗷叫,后两年基本都是些鸡零狗碎的东西了,用老东家的术语说是——偏离主航道,可谓是前景暗淡了。 一旦确定要滚蛋,反倒心思轻松起来,每天骑着我的小红车...

Jan 5, 20261 min read

辅助编程?dora 说:我知道你很急可是请你别急

从 OpenGPT 把大模型的火烧旺了之后,这三年来,相信很多组织或摩拳擦掌、或躬身入局,希望借助聪明能干的大模型,或想偿还技术宅,或想降本增效,或想弯道超车。一时间,沉寂许久的 AIxx 又活过来了,LLM Ops、Vibe Coding、中医大模型、GPT 算命等等,全都老树发新芽,焕发了勃勃生机。那么视角拉回从业者最关注的饭碗相关的领域之一——AI 辅助开发,产生了什么触动,应该如何拥抱呢? DORA 的年度报告中给出了很有意思的结论——强者恒强。 执行摘要部分总结了几个有趣的点: 问题...

Oct 6, 20251 min read

[译]dora:ai 辅助软件开发状态报告

执行摘要 在 2025 年,科技领导者面临的核心问题已不再是“是否要采用 AI”,而是“如何实现其价值”。 DORA 的研究基于超过 100 小时的定性访谈和来自全球近 5,000 名技术专业人士的问卷调查。研究揭示了一个关键事实:AI 在软件开发中的主要角色是“放大器”。它会放大高效能组织的优势,也会凸显组织的缺陷。 关键结论:AI 是放大器 AI 投资的最大回报并非来自工具本身,而是来自组织底层系统的战略性建设: 高质量的内部平台 清晰的工作流 团队的协同能力 缺少这些基础,AI ...

Oct 2, 202514 min read

僭越了,有人在用 Rust 写 Kubernetes

一个新语言问世,最爱做的事情之一,就是重写存量软件了。 云原生喝酒 SIG 重点扶持项目——rk8s(https://github.com/rk8s-dev/rk8s) 也可以归在这个范畴里,只不过这个项目重写的东西比较大,是 Kubernetes。 从 2025 年 1 月第一个 Commit 开始,到现在有了 200 多次 Commit,十几万行代码。当然距离 Kubernetes 的几百万行代码还差得远——老马就是喜欢整这种大无畏项目。 另外该项目也是国内第一个脱离 Cargo 转向使用 ...

Sep 27, 20253 min read

【伪】架构师

342 posts