目录
1.将构建好的数据面和控制面的镜像先导入到Kubernetes集群
1.引言
什么是HTNN?
HTNN 是一个基于 Istio 构建的服务网格增强项目,通过插件机制来扩展和定制流量控制、安全策略等能力。与 Istio 原生的 Mixer 插件体系不同,HTNN 提供了更加灵活和轻量的插件注册与运行方式,适合快速验证和迭代。
为什么开发 BasicAuth 和 ACL 插件?
在 HTNN 官方仓库中,虽然提供了如
key-auth
和hmac-auth
等认证插件,但对于传统的用户名密码认证(BasicAuth)以及基于源 IP 的访问控制(ACL)仍未支持。在一些面向公网服务或细粒度控制场景中,这类插件是非常常用且必要的。因此,我基于 HTNN 插件机制,自行开发并集成了:
basicAuth
插件:用于支持 HTTP Basic Authentication,适用于快速接入第三方平台或内部服务接口保护。
acl
插件:实现对来源 IP 的白名单与黑名单策略配置,增强服务边界的访问控制能力。
2.技术背景
技术栈概览
技术 | 作用 |
---|---|
Go | 编写插件逻辑,HTNN 本身使用 Go 开发 |
Protobuf | 定义插件配置结构、实现插件与框架间的序列化通信 |
Docker | 构建包含自定义插件的 HTNN 镜像 |
Kubernetes | 部署测试 HTNN 与插件,验证实际运行效果 |
Linux | 本地开发与测试环境,使用 shell 工具辅助调试 |
Istio | 服务网格的核心组件,HTNN 作为其增强层构建在上方 |
Istio 与服务网格简述
Istio 是当前主流的服务网格框架,提供流量管理、安全控制、可观测性等能力。在微服务架构中,Istio 通过 sidecar(通常是 Envoy)注入方式拦截进出流量,实现服务间的无侵入治理。然而,Istio 自身对于某些认证或访问控制的定制化支持较为复杂或不够灵活,这正是 HTNN 出现的背景。
HTNN 框架与插件机制概览
热插拔机制:插件通过 plugins.RegisterPlugin
接口注册,支持动态加载与配置。
统一请求拦截点:插件可以在请求进入 Envoy 之前对其进行处理(如验证、拦截)。
可配置结构:通过 Protobuf 配置文件或 ConfigMap 传递运行参数,实现运行时灵活控制。
与 Istio 无缝集成:在 Istio 的 service mesh 中以 sidecar 的形式运行,兼容 Istio 原生组件。
3.插件开发详解:BasicAuth 与 ACL
3.1 BasicAuth插件
功能点
1.基于 HTTP Basic Auth 的认证:
验证 HTTP 请求中的 Authorization
头部,确保用户提供了正确的用户名和密码。
如果认证失败,返回 HTTP 401 状态码。
2.支持多用户凭据:
插件支持配置多个用户名和密码对,允许不同用户访问。
3.动态配置:
用户名和密码通过配置文件动态加载,支持灵活调整
4.安全性:
使用 Base64 解码解析 Authorization
头部中的凭据。
如果凭据无效或缺失,拒绝请求。
实现细节
1. 配置结构
配置文件定义在 [config.go] 中:
type Config struct {
Credentials map[string]string `json:"credentials,omitempty"`
}
Credentials
字段:
键是用户名,值是对应的密码。
示例配置:
{
"credentials": {
"user1": "password1",
"user2": "password2"
}
}
2. 插件注册
插件通过 plugins.RegisterPlugin 注册到框架中:
func init() {
plugins.RegisterPlugin(basicauth.Name, &plugin{})
}
3. 核心逻辑
核心逻辑实现于 [filter.go
] 中:
func (f *filter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.ResultAction {
authHeader, ok := headers.Get("Authorization")
if !ok || !strings.HasPrefix(authHeader, "Basic ") {
return &api.LocalResponse{Code: 401, Msg: "missing or invalid Authorization header"}
}
encodedCredentials := strings.TrimPrefix(authHeader, "Basic ")
decoded, err := base64.StdEncoding.DecodeString(encodedCredentials)
if err != nil {
return &api.LocalResponse{Code: 401, Msg: "invalid Authorization header"}
}
parts := strings.SplitN(string(decoded), ":", 2)
if len(parts) != 2 {
return &api.LocalResponse{Code: 401, Msg: "invalid Authorization header"}
}
username, password := parts[0], parts[1]
if validPassword, ok := f.config.Credentials[username]; !ok || validPassword != password {
return &api.LocalResponse{Code: 401, Msg: "invalid username or password"}
}
return api.Continue
}
逻辑说明:
检查
Authorization
头部是否存在,且是否以Basic
开头。使用 Base64 解码凭据,并解析出用户名和密码。
验证用户名和密码是否匹配配置中的凭据。
如果验证失败,返回 HTTP 401;否则继续处理请求。
4. 单元测试
单元测试位于 [filter_test.go
]:
func TestBasicAuthFilter(t *testing.T) {
tests := []struct {
name string
conf string
headers map[string][]string
expected int
}{
{
name: "valid credentials",
conf: `{
"credentials": {
"user1": "password1",
"user2": "password2"
}
}`,
headers: map[string][]string{
"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user1:password1"))},
},
expected: 0,
},
{
name: "invalid credentials",
conf: `{
"credentials": {
"user1": "password1",
"user2": "password2"
}
}`,
headers: map[string][]string{
"Authorization": {"Basic " + base64.StdEncoding.EncodeToString([]byte("user1:wrongpassword"))},
},
expected: 401,
},
}
}
测试覆盖了以下场景:
有效的用户名和密码。
无效的用户名或密码。
缺失
Authorization
头部
3.2 ACL插件
功能点
1.基于IP地址的访问控制:
支持通过 allow_list
和 deny_list
配置访问规则。
优先检查 deny_list
,如果匹配则拒绝访问。
2.CIDR支持:
支持 CIDR 格式的 IP 地址范围匹配。
3.动态配置:
通过配置文件动态加载访问控制规则。
4.安全性:
如果请求的 IP 地址不在 allow_list
中,默认拒绝访问。
实现细节
1.配置结构
配置文件定义在 [config.go] 中:
type Config struct {
AllowList []string `json:"allow_list,omitempty"`
DenyList []string `json:"deny_list,omitempty"`
}
AllowList
和 DenyList
字段:
AllowList
:允许访问的 IP 地址或 CIDR 范围。
DenyList
:拒绝访问的 IP 地址或 CIDR 范围。
示例配置:
{
"allow_list": ["192.168.1.0/24"],
"deny_list": ["10.0.0.1"]
}
2.插件注册
插件通过 plugins.RegisterPlugin 注册到框架中:
func init() {
plugins.RegisterPlugin(acl.Name, &plugin{})
}
3.核心逻辑
核心逻辑实现于 [filter.go
] 中:
func (f *filter) DecodeHeaders(headers api.RequestHeaderMap, endStream bool) api.ResultAction {
clientIP, ok := headers.Get("X-Forwarded-For")
if !ok {
return &api.LocalResponse{Code: 403, Msg: "client IP not found"}
}
for _, denyIP := range f.config.DenyList {
if isIPMatch(clientIP, denyIP) {
return &api.LocalResponse{Code: 403, Msg: "access denied"}
}
}
for _, allowIP := range f.config.AllowList {
if isIPMatch(clientIP, allowIP) {
return api.Continue
}
}
return &api.LocalResponse{Code: 403, Msg: "access denied"}
}
逻辑说明:
获取请求头中的
X-Forwarded-For
字段,提取客户端 IP。检查 IP 是否匹配
deny_list
,如果匹配则拒绝访问。检查 IP 是否匹配
allow_list
,如果匹配则允许访问。如果未匹配任何规则,默认拒绝访问。
4.单元测试
单元测试位于 [filter_test.go
]:
func TestACLFilter(t *testing.T) {
tests := []struct {
name string
conf string
headers map[string][]string
expected int
}{
{
name: "deny list match",
conf: `{
"allow_list": ["192.168.1.0/24"],
"deny_list": ["10.0.0.1"]
}`,
headers: map[string][]string{
"X-Forwarded-For": {"10.0.0.1"},
},
expected: 403,
},
{
name: "allow list match",
conf: `{
"allow_list": ["192.168.1.0/24"],
"deny_list": ["10.0.0.1"]
}`,
headers: map[string][]string{
"X-Forwarded-For": {"192.168.1.50"},
},
expected: 0,
},
}
}
测试覆盖了以下场景
匹配
deny_list
。匹配
allow_list
。未匹配任何规则。
4.实践过程
4.1本地构建自定义HTNN镜像
1.修改源码并添加插件
分别在types\plugins(实现配置)和plugins\plugins(实现逻辑)目录下新增basicauth和acl目录。
分别在两个目录下的主插件入口调用RegisterPlugin(...)注册。
2.构建Docker镜像
先检查实现.editorconfig文件的代码格式规范,避免因系统版本不兼容导致换行符等问题影响镜像的构建
dockerfile文件直接复用 htnn\manifests\images\cp\Dockerfile 和htnn\manifests\images\dp\Dockerfile 就行
cd到 htnn\manifests\Makefile 目录下执行 make 命令完成构建:
make build-proxy-image
make build-controller-image
4.2 在 Kubernetes 中部署插件
1.将构建好的数据面和控制面的镜像先导入到Kubernetes集群
minikube image load your-image-name1:tag
minikube image load your-image-name2:tag
2.修改Helm Chart的 values.yaml文件
#文件 htnn\manifests\charts\htnn-controller\values.yaml
hub: ""
image: htnn-controller
tag: "latest"
imagePullPolicy: "Never" # 不使用远程镜像
#文件 manifests\charts\htnn-gateway\values.yaml
gateway:
name: istio-ingressgateway
image: m.daocloud.io/docker.io/envoyproxy/envoy:latest
imagePullPolicy: Never
env:
ISTIO_DELTA_XDS: "true"
#默认的manifests\charts\htnn-gateway\values.schema.json文件中没有镜像参数
#需要在json文件中添加该结构参数
3.部署或更新 HTNN 到 Kubernetes
首次部署:
控制面
helm install htnn-controller 本地Path\htnn\manifests\charts\htnn-controller --namespace istio-system --create-namespace -f 本地Path\htnn\manifests\charts\htnn-controller\values.yaml
数据面
helm install htnn-gateway 本地Path\htnn\manifests\charts\htnn-gateway -n istio-system -f 本地Path\htnn\manifests\charts\htnn-gateway\values.yaml
已有部署则使用升级命令 upgrade 替换 install
4.查看 istio-system
命名空间下所有 Pod 的运行状态
PS C:\WINDOWS\system32> kubectl -n istio-system get pods
NAME READY STATUS RESTARTS AGE
istio-ingressgateway-674cd8d4c9-lg692 1/1 Running 3 (3d2h ago) 3d20h
istiod-6b6c464bb7-2df62 1/1 Running 3 (3d2h ago) 3d20h
如果出现异常状态,用以下命令查看原因
kubectl -n istio-system describe pod <pod-name>
4.3 测试验证流程
分别进行后端服务部署、入口流量管理、路由规则定义和安全认证策略配置,配置到kubernetes环境中
部署后端服务
//backend.yaml
---
apiVersion: v1
kind: Service
metadata:
name: backend
namespace: istio-system
labels:
app: backend
spec:
ports:
- port: 80
name: http
targetPort: 5678
selector:
app: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: backend
namespace: istio-system
spec:
replicas: 1
selector:
matchLabels:
app: backend
template:
metadata:
labels:
app: backend
spec:
containers:
- name: backend
image: hashicorp/http-echo
args:
- "-text=hello from backend"
ports:
- containerPort: 5678
定义入口网关
//gateway.yaml
apiVersion: networking.istio.io/v1beta1
kind: Gateway
metadata:
name: htnn-gateway
namespace: istio-system
spec:
selector:
istio: ingressgateway
servers:
- port:
number: 80
name: http
protocol: HTTP
hosts:
- "*"
定义虚拟服务路由规则
//vs-basicauth.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
name: vs-basicauth
namespace: istio-system
spec:
hosts:
- "*"
gateways:
- htnn-gateway
http:
- match:
- uri:
prefix: /
route:
- destination:
host: backend.istio-system.svc.cluster.local
port:
number: 80
定义 basicAuth 插件的策略
//basicauthpolicy.yaml
apiVersion: htnn.mosn.io/v1
kind: FilterPolicy
metadata:
name: basicauth-policy
namespace: istio-system
spec:
targetRef:
group: networking.istio.io
kind: VirtualService
name: vs-basicauth
filters:
basicAuth:
config:
credentials:
admin: "admin123"
user: "user123"
先通过 status 检查下策略是否被接受:
$ kubectl -n istio-system get filterpolicies.htnn.mosn.io policy -o yaml
···
status:
conditions:
- lastTransitionTime: "2025-05-15T13:56:42Z"
message: The policy has been accepted
observedGeneration: 1
reason: Accepted
status: "True"
type: Accepted
kind: List
metadata:
resourceVersion: ""
让我们在一个终端上执行 port-forward,让本地的客户端可以访问到 k8s 里面的服务:
kubectl port-forward -n istio-system svc/istio-ingressgateway 30474:80
在另一个终端上,我们可以通过 30474 端口访问到 HTNN:
//正确的用户名和密码
curl -v --user admin:admin123 http://localhost:30474/get
HTTP/1.1 200 OK
//不正确的
curl -v --user admin:admin121 http://localhost:30474/get
HTTP/1.1 401 Unauthorized
以上是basicauth插件的测试流程,acl的流程是一样的,先将配置文件写好,再应用策略:
//aclpolicy.yaml
apiVersion: htnn.mosn.io/v1
kind: FilterPolicy
metadata:
name: acl-policy
namespace: istio-system
spec:
targetRef:
group: networking.istio.io
kind: VirtualService
name: vs-acl
filters:
acl:
config:
allow_list:
- "192.168.1.0/24"
deny_list:
- "10.0.0.1"
让我们在一个终端上执行 port-forward,让本地的客户端可以访问到 k8s 里面的服务:
kubectl port-forward -n istio-system svc/istio-ingressgateway 30474:80
在另一个终端上,我们可以通过 30474 端口访问到 HTNN:
curl -v --header "Host: backend.local" --header "X-Forwarded-For: 192.168.1.100" http://localhost:30474/get
HTTP/1.1 200 OK
curl -v --header "Host: backend.local" --header "X-Forwarded-For: 10.0.0.1" http://localhost:30474/get
HTTP/1.1 403 Forbidden