Featured image of post Higress 01:key-auth 插件本地 Docker 部署与运行机制指南

Higress 01:key-auth 插件本地 Docker 部署与运行机制指南

不依赖 K8s,用 Docker 裸跑 Higress 数据面验证 key-auth 插件:官方三种独立部署方式对比、wasm 编译、proxy-wasm ABI 踩坑与 5 场景实测。

目标:不依赖 Kubernetes,只用 Docker 在本地完整复现 “McpBridge + Ingress + WasmPlugin(key-auth)” 三件套的运行效果(401 / 403 / 200 / 404),并讲清背后的原理。

本文所有操作已在 macOS + Docker Desktop 上实测通过(2026-09-17)。

目录


一、背景:要复现的原配置

原始配置是三个 K8s 资源(完整内容见 citic-wasm-plugin/key-auth/keyauth.yaml):

资源作用关键内容
McpBridge mcp-keyauth-httpbin把集群外的 httpbin.org:80 注册进网关,服务名为 httpbin.dnstype: dns,网关周期性做 DNS 解析得到 IP 池
Ingress ingress-keyauth-httpbin路由规则:Host: httpbin.example.com 的流量全部转发到 httpbin.dnshigress.io/destination、upstream-vhost、backend-protocol 三个注解
WasmPlugin wasm-keyauth-httpbin在该 Ingress 上启用 key-auth 认证2 个 consumer,只放行 consumer1;从 x-api-key / apikey 头取 Key

引用关系:Ingress.backend.resource → McpBridge 名;annotations.higress.io/destination → name.type(httpbin.dns);WasmPlugin.matchRules[].ingress → Ingress 名。

二、总体方案与架构

K8s 里这套东西最终也是由 Higress 控制面翻译成 Envoy 配置下发的。所以我们绕过控制面,直接手写等价的 Envoy 静态配置,用 Docker 跑数据面:

1
2
3
4
5
6
7
┌─────────┐   curl -H "Host: httpbin.example.com"    ┌──────────────────────────────┐      ┌──────────────────┐
│  终端    │ ──────────── localhost:10000 ──────────▶ │  envoy 容器 (higress/gateway) │ ───▶ │  httpbin 容器     │
│ (curl)  │ ◀────────────── 401/403/200 ────────────  │  ┌────────────────────────┐  │      │ go-httpbin :8080  │
└─────────┘                                          │  │ key-auth wasm 插件      │  │      │ (代替 httpbin.org)│
                                                     │  │ (本地 Go 源码编译)       │  │      └──────────────────┘
                                                     │  └────────────────────────┘  │
                                                     └──────────────────────────────┘

相比原方案的两处替换:

  • 上游替换:httpbin.org(公网)→ 本地 mccutchen/go-httpbin 容器。行为等价(都是请求回显服务),且不依赖外网。
  • 控制面替换:McpBridge/Ingress/WasmPlugin 三个 CRD → 一份 envoy.yaml。三者的语义被完整保留(域名匹配、路由名、插件配置、allow 白名单)。

所有文件在 key-auth-docker-demo/ 目录:

1
2
3
4
5
key-auth-docker-demo/
├── docker-compose.yaml   # 两个容器:envoy + httpbin
├── envoy.yaml            # 等价于三个 CRD 的 Envoy 静态配置
├── plugin.wasm           # 用 golang:1.24 容器从本仓库 Go 源码编译
└── test.sh               # 5 个场景的验证脚本

2.1 与官方独立部署方式的关系(先读这里)

Higress 官方文档提供了三种"脱离/本地化 K8s"的部署方式,它们和本文方案不是一回事,定位不同:

方式组件规模配置方式适用场景官方文档
All-in-One 单容器1 个容器(gateway+console 打包)控制台 UI / 本地 /data 卷快速体验完整网关功能deploy-by-docker↗
Docker Compose 独立部署apiserver + controller + pilot + gateway + console(可选内置 nacos、Prometheus/Loki/Grafana 监控套件),约 12 个容器控制台 UI / 模拟 K8s API / 本地配置文件接近真实架构的本地测试deploy-by-docker-compose↗
本地 K8s(kind)+ Helm完整云原生模式kubectl apply CRD(本文开头那份 YAML 可直接用)与生产完全一致的验证quickstart↗
本文方案:裸数据面2 个容器(envoy + httpbin)手写 envoy.yaml只关心插件本身的行为和原理,最轻最快本文

本文方案可以理解成:把官方架构中 pilot 最终下发给 gateway 的那份 Envoy 配置"手写"了出来,只跑数据面,不跑控制面。对"验证 key-auth 插件的认证行为(401/403/200)“这个目的来说,2 个镜像、一条 docker compose up -d 就够了。

官方两种方式的核心命令(引自官方文档,供需要完整控制台时使用):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# 方式一:All-in-One 单容器(控制台 8001,网关 8080/8443,配置存到当前目录)
mkdir higress; cd higress
docker run -d --rm --name higress-ai -v ${PWD}:/data \
        -p 8001:8001 -p 8080:8080 -p 8443:8443  \
        higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/all-in-one:latest

# 方式二:Docker Compose 多组件独立部署
curl -fsSL https://higress.io/standalone/get-higress.sh | bash -s -- [安装目录] [选项]
# 常用选项:-a 装完自动启动;--use-builtin-nacos 用内置配置中心;
#           -c file://<绝对路径> 用本地文件存配置;--console-port=8001 改控制台端口(默认 8080)

实测备注(2026-09-17 本机实际尝试过 compose 独立部署后的经验,供参考):

  • 重:约 12 个镜像(apiserver、controller、pilot、gateway、console、plugin-server、nacos、prometheus、loki、grafana 等),首次安装拉取超过 30 分钟;日常验证插件没必要。
  • -c file:///opt/higress/conf 默认会把配置写到宿主机 /opt/higress(macOS 上直接 Permission denied),要改成工作区内的绝对路径。
  • 安装脚本内部用 docker compose -p higress 管理容器;如果自己在 compose/ 目录里手动执行 docker compose 而不带 -p higress,会另建一套 compose-* 前缀的重复容器并抢 80 端口报错。
  • 一个利好:compose 部署内置 plugin-server 容器,自带 key-auth 2.0.0 等官方插件,WasmPlugin.url 直接写 http://plugin-server:8080/plugins/key-auth/2.0.0/plugin.wasm 即可,不需要自己编译 wasm——但那样跑的就不是本仓库的代码了。
  • 卸载干净的方法:安装目录下 bash bin/reset.sh(停容器、删数据卷),再删安装目录和镜像。

结论:想体验控制台和完整控制面 → 用上面官方方式;想验证/调试本仓库的插件代码、搞清楚数据面到底发生了什么 → 用本文方案。

三、原理介绍

3.1 WASM 插件是怎么跑起来的

  1. Higress 的数据面是 Envoy 的一个 fork。Envoy 内置 envoy.filters.http.wasm 过滤器,可以把一个 .wasm 文件加载进 V8 运行时,作为 HTTP 过滤器链的一环执行。
  2. 插件本身是用 Go 写的(citic-wasm-plugin/key-auth/main.go),基于 higress-group/proxy-wasm-go-sdk,通过 GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared 编译成 wasm。
  3. 插件通过 wrapper.SetCtx("key-auth", ...) 注册三类回调:
    • ParseOverrideConfigBy(parseGlobalConfig, parseOverrideRuleConfig) — 插件启动/配置变更时解析配置
    • ProcessRequestHeadersBy(onHttpRequestHeaders) — 每个请求到达时执行认证判断
  4. 请求流程:Envoy 收到请求 → 路由匹配(host + path)→ 执行 wasm 过滤器(key-auth 认证)→ 认证通过则转发给上游 cluster,并把 X-Mse-Consumer: <consumer名> 头注入给上游;不通过则由插件直接返回本地响应(401/403),请求根本不会到达上游。

插件里发回本地响应用的是 proxywasm.SendHttpResponseWithDetail(main.go:329-345),同时带上 WWW-Authenticate: Key realm=MSE Gateway 响应头——这就是实测输出里 www-authenticate: Key realm=MSE Gateway 的来源。

3.2 Proxy-Wasm ABI 与 0_2_100 的坑

Envoy 与 wasm 插件之间通过 Proxy-Wasm ABI(一组以 proxy_ 开头的导入/导出函数)通信。Envoy 加载 wasm 时会检查模块导出的 ABI 版本号函数:

  • 官方 Envoy 认 proxy_abi_version_0_2_1
  • 用官方 C++ SDK 编译的插件(如官方镜像 plugins/key-auth:1.0.0 里的 wasm)导出的就是 proxy_abi_version_0_2_1
  • 但 Higress 的 Go SDK fork 导出的是 proxy_abi_version_0_2_100(Higress 私有扩展 ABI,增加了 redis 调用等能力)

验证方法(无需反编译器,strings 即可):

1
2
3
strings plugin.wasm | grep proxy_abi
# Go 编译(本仓库源码):  proxy_abi_version_0_2_100
# 官方 C++ 编译镜像:     proxy_abi_version_0_2_1

结论:Go 源码编译的 key-auth 插件只能跑在 Higress fork 的 Envoy 上。如果挂载到官方 envoyproxy/envoy,启动即失败:

1
2
3
[error][wasm] Wasm VM failed Missing or unknown Proxy-Wasm ABI version
[critical][wasm] Plugin key-auth failed to load
[critical][main] error initializing config '/etc/envoy/envoy.yaml': Unable to create Wasm plugin key-auth

所以本方案使用 higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.2.4(内含 Envoy 1.36.4 的 Higress fork),通过覆盖 entrypoint 直接裸跑 envoy:

1
entrypoint: ["/usr/local/bin/envoy", "-c", "/etc/envoy/envoy.yaml", "--concurrency", "1"]

3.3 key-auth 插件的认证逻辑

核心逻辑在 main.go 的 onHttpRequestHeaders(main.go:239)。插件行为由两个变量决定:

  • global_auth:全局配置里的开关(true / false / 未设置,三种状态)
  • 当前路由是否配置了 allow 列表(即 matchRules 是否命中当前路由)

决策真值表:

global_auth当前路由有 allow行为
true否全局限流:所有 consumer 的 Key 都可用
true是只有 allow 列表里的 consumer 可用
false否直接放行,不认证
false是只有 allow 列表里的 consumer 可用
未设置全部路由都没配退化为全局认证
未设置至少一个路由配了等价于 false:没配的路由放行,配了的路由按 allow 校验

本次复现的配置是 global_auth: false + 路由配 allow: [consumer1],即上表第 4 行。

取 Key 与判定顺序(main.go:259-326):

  1. 按 keys 列表从 Header(in_header: true)或 URL query(in_query: true)取 token
  2. 取到多个 token → 401(deniedMultiKeyAuthData)
  3. 一个都没取到 → 401(deniedNoKeyAuthData,body: No Key Authentication information found)
  4. token 在 consumers 里查不到 → 403(deniedUnauthorizedConsumer,body: Unauthorized consumer)
  5. 查到 consumer 后先注入 X-Mse-Consumer: <name> 头,再检查是否在 allow 列表:
    • 不在 → 403(deniedUnauthorizedConsumer)
    • 在 → 放行,请求转发上游

⚠️ 注意:假 Key 和"不在白名单的有效 Key"都是 403,只有"完全不带 Key"或"带多个 Key"才是 401。原 YAML 讲解文档里说假 Key 返回 401 是不准确的,以源码和实测为准。

3.4 规则匹配:rules 与 match_route

K8s 里 WasmPlugin.matchRules[].ingress: [ingress-keyauth-httpbin] 经控制面翻译后,落到 wasm 插件配置里是:

1
"_rules_": [{"_match_route_": ["<路由名>"], "allow": ["consumer1"]}]

wasm-go SDK 的 wrapper 在每次请求时取 Envoy 的 route_name 属性,和 _match_route_ 列表比对:命中则用该条规则的配置(覆盖全局配置中的 allow),不命中则用全局配置。

因此在裸 Envoy 配置里,我们只要给路由起个名字(name: keyauth-route),并在插件配置里写 _match_route_: ["keyauth-route"],就等价复现了 matchRules 的语义。

3.5 与原 YAML 的字段对应关系

原 YAML 字段envoy.yaml 中的等价物
Ingress rules[0].host: httpbin.example.comvirtual_hosts[].domains: ["httpbin.example.com"]
path: / + pathType: Prefixroutes[].match.prefix: /
McpBridge(httpbin.org:80, type: dns)clusters[].name: httpbin + STRICT_DNS 指向本地 httpbin 容器
higress.io/upstream-vhost: httpbin.org本地回显服务不校验 Host,省略;真实场景对应 host_rewrite_literal
matchRules[].ingress路由命名 keyauth-route + 插件配置 _match_route_
defaultConfig(consumers/keys/in_header/global_auth)插件 JSON 配置的同名字段
matchRules[].config.allow: [consumer1]_rules_[0].allow: ["consumer1"]

envoy.yaml 中插件配置的完整 JSON(即 defaultConfig + matchRules 合并后的结果):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
{
  "consumers": [
    {"name": "consumer1", "credential": "2bda943c-ba2b-11ec-ba07-00163e1250b5"},
    {"name": "consumer2", "credential": "c8c8e9ca-558e-4a2d-bb62-e700dcc40e35"}
  ],
  "keys": ["x-api-key", "apikey"],
  "in_header": true,
  "global_auth": false,
  "_rules_": [
    {"_match_route_": ["keyauth-route"], "allow": ["consumer1"]}
  ]
}

四、搭建步骤(从零开始)

4.1 准备镜像

1
2
3
4
5
6
7
8
# Higress gateway(含支持 Go 插件 ABI 的 Envoy fork)
docker pull higress-registry.cn-hangzhou.cr.aliyuncs.com/higress/gateway:v2.2.4

# 本地回显服务
docker pull mccutchen/go-httpbin:latest

# 编译用的 golang 镜像
docker pull golang:1.24

docker.io 不通时可走镜像加速器,例如 DaoCloud: docker pull docker.m.daocloud.io/library/golang:1.24 && docker tag docker.m.daocloud.io/library/golang:1.24 golang:1.24。 注意 mccutchen/go-httpbin 不在 DaoCloud 白名单内,需直连 docker.io 或开代理;阿里云 registry(higress-registry.cn-hangzhou.cr.aliyuncs.com)国内直连无压力。

4.2 编译 plugin.wasm

用 Docker 编译本仓库的 Go 源码,不污染本地 Go 环境:

1
2
3
4
5
6
7
8
9
cd citic-wasm-plugin/key-auth
docker run --rm \
  -v "$PWD:/src" -w /src \
  -v gomodcache:/go/pkg/mod -v gocache:/root/.cache/go-build \
  -e GOOS=wasip1 -e GOARCH=wasm -e GOPROXY=https://goproxy.cn,direct \
  golang:1.24 go build -buildmode=c-shared -o plugin.wasm .

# 拷贝到部署目录(compose 里挂载的是 key-auth-docker-demo/plugin.wasm)
cp plugin.wasm ../../key-auth-docker-demo/

产物约 5.4MB。gomodcache/gocache 两个 named volume 用于缓存依赖,二次编译会快很多。

备选:也可以直接从官方 OCI 镜像提取预编译 wasm(注意是 C++ 版本,ABI 为 0_2_1,可跑在官方 Envoy 上,但拒绝响应的文案与 Go 版不同):

1
2
3
4
5
TOKEN=$(curl -s "https://dockerauth.cn-hangzhou.aliyuncs.com/auth?service=registry.aliyuncs.com:cn-hangzhou:china:cri-r0xfyoxudmtseqq7&scope=repository:plugins/key-auth:pull" \
  | python3 -c "import sys,json;print(json.load(sys.stdin)['token'])")
# 先 GET /v2/plugins/key-auth/manifests/1.0.0 找到 wasm 层的 digest,再:
curl -sL -H "Authorization: Bearer $TOKEN" -o plugin.wasm \
  "https://higress-registry.cn-hangzhou.cr.aliyuncs.com/v2/plugins/key-auth/blobs/<digest>"

4.3 编写配置

envoy.yaml 和 docker-compose.yaml 已就绪(见 key-auth-docker-demo/),关键点:

  • 监听 0.0.0.0:10000,宿主机映射 10000:10000
  • admin 开在 9901 端口(可选,用于看配置和统计)
  • wasm 过滤器配置中 runtime: envoy.wasm.runtime.v8,code.local.filename: /etc/envoy/plugin.wasm
  • plugin.wasm 以只读方式挂载进容器

4.4 启动与验证

1
2
3
4
5
6
7
8
cd key-auth-docker-demo
docker compose up -d

# 确认插件加载成功
docker compose logs envoy | grep "plugin start successfully"

# 跑全部验证场景
bash test.sh

五、验证结果(实测输出)

① 不带 Key → 401

1
2
3
4
5
HTTP/1.1 401 Unauthorized
www-authenticate: Key realm=MSE Gateway
server: envoy

Request denied by Key Auth check. No Key Authentication information found.

② consumer1 的 Key(在白名单)→ 200

1
2
3
curl -s -i -H "Host: httpbin.example.com" \
  -H "x-api-key: 2bda943c-ba2b-11ec-ba07-00163e1250b5" \
  http://localhost:10000/get
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
HTTP/1.1 200 OK
server: envoy
x-envoy-upstream-service-time: 16

{
  "headers": {
    "Host": ["httpbin.example.com"],
    "X-Mse-Consumer": ["consumer1"],   ← 插件注入,证明上游收到了认证身份
    ...
  }
}

③ consumer2 的 Key(有效但不在 allow)→ 403

1
2
3
4
HTTP/1.1 403 Forbidden
www-authenticate: Key realm=MSE Gateway

Request denied by Key Auth check. Unauthorized consumer.

④ 假 Key → 403(不是 401!)

1
2
3
HTTP/1.1 403 Forbidden

Request denied by Key Auth check. Unauthorized consumer.

credential 在 consumers 里查不到,与"不在白名单"走同一个分支(main.go:288-292)。

⑤ Host 不匹配 → 404

1
curl -s -i http://localhost:10000/get     # 不带 Host 头
1
HTTP/1.1 404 Not Found

请求根本没命中 httpbin.example.com 这个 virtual host,路由不存在,与插件无关。

六、踩坑记录

  1. 官方 Envoy 加载不了 Go 编译的插件:报 Missing or unknown Proxy-Wasm ABI version。原因是 Higress Go SDK 导出 proxy_abi_version_0_2_100,官方 Envoy 只认 0_2_1。解法:换 higress/gateway 镜像。(详见 3.2)

  2. docker pull OCI wasm 插件镜像报 unsupported media type application/vnd.oci.empty.v1+json:wasm 插件镜像是 FROM scratch + OCI 空层的纯 artifact,Docker 旧存储驱动不支持。解法:要么在 Docker Desktop 设置里启用 containerd 存储,要么绕过 daemon 直接 curl registry API 下载 blob(见 4.2 备选方案)。

  3. docker.io 拉取超时:国内直连 docker.io 经常不通。阿里云 registry 不受影响;其余镜像走加速器或代理。

  4. 原始文档的错误:网上流传的该 YAML 讲解中"假 Key 返回 401"的说法与 Go 实现不符,实际是 403。读插件源码(main.go 的四个 denied* 函数)是最可靠的事实来源。

七、常用命令速查

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
cd key-auth-docker-demo

docker compose up -d                 # 启动
docker compose down                  # 停止并删除容器
docker compose restart envoy         # 改了 envoy.yaml / 插件配置后重启
docker compose logs -f envoy         # 看日志(含插件日志,如 consumer "consumer1" authenticated)
bash test.sh                         # 一键跑 5 个验证场景

# 修改插件代码后重新编译(在仓库根目录执行)
cd citic-wasm-plugin/key-auth && docker run --rm -v "$PWD:/src" -w /src \
  -v gomodcache:/go/pkg/mod -v gocache:/root/.cache/go-build \
  -e GOOS=wasip1 -e GOARCH=wasm -e GOPROXY=https://goproxy.cn,direct \
  golang:1.24 go build -buildmode=c-shared -o plugin.wasm . \
  && cp plugin.wasm ../../key-auth-docker-demo/ && cd ../../key-auth-docker-demo \
  && docker compose restart envoy

# envoy admin 接口(调试)
curl localhost:9901/config_dump | python3 -m json.tool | head -50
curl localhost:9901/stats | grep wasm

附:单元测试路线(不需要 Docker)

如果只想快速验证插件逻辑的正确性,不追求真实 HTTP 效果,可以直接跑仓库里自带的单元测试(main_test.go,基于 wasm-go 的 host 模拟框架,wazero 在进程内跑 wasm,不需要 Docker):

1
2
cd citic-wasm-plugin/key-auth
go test ./... -v

它覆盖了配置解析(合法/非法配置)、Header/Query 取 Key、401/403 分支、X-Mse-Consumer 注入等场景。但它验证的是"插件内部逻辑”,而不是"网关端到端行为"——后者还是要靠本文的 Docker 方案。

使用 Hugo 构建
主题 Stack 由 Jimmy 设计