增强 HTNN 服务网格功能:基于 Istio 的BasicAuth 与 ACL 插件开发实战

发布于:2025-05-16 ⋅ 阅读:(15) ⋅ 点赞:(0)

目录

1.引言

什么是HTNN?

为什么开发 BasicAuth 和 ACL 插件?

2.技术背景

技术栈概览

Istio 与服务网格简述

HTNN 框架与插件机制概览

3.插件开发详解:BasicAuth 与 ACL

3.1 BasicAuth插件

功能点

实现细节

3.2 ACL插件

功能点

实现细节

4.实践过程

4.1本地构建自定义HTNN镜像

1.修改源码并添加插件

2.构建Docker镜像

4.2 在 Kubernetes 中部署插件

1.将构建好的数据面和控制面的镜像先导入到Kubernetes集群

2.修改Helm Chart的 values.yaml文件

4.3 测试验证流程


1.引言

什么是HTNN?

HTNN 是一个基于 Istio 构建的服务网格增强项目,通过插件机制来扩展和定制流量控制、安全策略等能力。与 Istio 原生的 Mixer 插件体系不同,HTNN 提供了更加灵活和轻量的插件注册与运行方式,适合快速验证和迭代。

项目地址:https://github.com/mosn/htnn

为什么开发 BasicAuth 和 ACL 插件?

在 HTNN 官方仓库中,虽然提供了如 key-authhmac-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


网站公告

今日签到

点亮在社区的每一天
去签到