云原生 04:用 Envoy + Open Policy Agent 做前置代理

系列专栏声明:比较流水,主要是写一些踩坑的点,和实践中与文档差距较大的地方的思考。这个专栏的典型特征可能是 次佳实践,争取能在大量的最佳实践中生存。

Open Policy Agent 吸引我的点在于 Policy as Code,所以看上去整个云原生概念吸引我的点就在于 X as Code 或者更准确的说是 X as Declarative DSL

TODO:会有 JWT 的部分,但是第一稿来不及写了,可以收藏以后来看。

云原生 04:用 Envoy + Open Policy Agent 做前置代理_第1张图片

PBAC: Policy Base Authorization Control 中有三个核心的概念,PIPPDPPEP

OPA 是一个 PDP,即 规则引擎。审计方在这里用 自然语言的 DSL 描述权限控制规则,OPA 执行这些规则并返回 allowdeny。早期的权限规则是单向的,只能从需求被翻译成面向过程的代码,而很难从已经完成的代码反推出实际被执行的规则,也很难对规则进行更新或迁移。使用 DSL 是质变的关键,它使得需求和代码可以双向翻译了。

一、请求流向

云原生 04:用 Envoy + Open Policy Agent 做前置代理_第2张图片

这是上面概念图落地后的实例图,有两个比较明显的简化点是:

  1. 本文实际上只是第一道网关,只做简单的准入控制,不做行、列粒度的数据权限控制。所以和 Envoy(PEP) 配套的 OPA(PDP) 没有去请求额外的 PIP 获得更多属性,而是仅使用 Envoy 带来的属性包括 JWT。因此这里通常只会携带当前用户的 id 和关键 role。如果要做细粒度的权限控制,应该在 Service 处再加一套 PEP + OPA(PDP)
  2. OPA 要求所有的 Policy 都在内存里。它提供了 restful api 用来更新内存中的 policy,这是大部分入门介绍的方式,但看上去没有实际意义。另外还提供了从数据源实时同步的方式,这个应该才是 正确的方式,不在本文中详细描述了。本文在启动时从文件中加载。

docker-compose.yml

  app:
    container_name: app
    image: hub.docker.com/example/app:latest
    networks:
      - traefik
    # 这里不需要配置 traefik 相关的 labels,因为实际对外暴露的是 envoy

  app-envoy:
    container_name: app-envoy
    image: envoyproxy/envoy-alpine:v1.17.4
    networks:
      - traefik
    labels:
      - "traefik.enable=true"
      - "traefik.http.routers.app.rule=Host(`api.example.com`)"
      # 省略其它必须的 traefik labels
    volumes:
      - "/path/to/envoy.yaml:/etc/envoy/envoy.yaml"

  app-opa:
    container_name: app-opa
    image: openpolicyagent/opa:0.35.0-envoy-5-rootless
    networks:
      - traefik
    volumes:
      - "/path/to/policy.rego:/config/policy.rego"
    command:
      - run
      - --log-level=debug
      - --set=decision_logs.console=true
      - --skip-version-check # 这里关闭向 open telemetry 上报埋点
      - --server
      - --set=plugins.envoy_ext_authz_grpc.addr=:9191
      - --set=plugins.envoy_ext_authz_grpc.path=com/example/app/opa/allow
      - /config/policy.rego

几个踩坑点:

  1. opa 使用的是内置 envoy plugin版本

envoy.yaml

static_resources:
  listeners:
  - address:
      socket_address:
        address: 0.0.0.0
        port_value: 80
    filter_chains:
    - filters:
      - name: envoy.http_connection_manager
        typed_config:
          "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
          # 省略
          route_config:
            # 省略
          http_filters:
          - name: envoy.filters.http.ext_authz
            typed_config:
              "@type": type.googleapis.com/envoy.extensions.filters.http.ext_authz.v3.ExtAuthz
              transport_api_version: V3
              grpc_service:
                envoy_grpc:
                  cluster_name: app-opa
                timeout: 0.5s
              # 省略
          - name: envoy.filters.http.cors
          - name: envoy.filters.http.router
            typed_config: {}
  clusters:
  - name: app
    connect_timeout: 0.25s
    type: strict_dns
    dns_lookup_family: V4_ONLY
    lb_policy: round_robin
    load_assignment:
      cluster_name: app
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: app
                port_value: 8080
  - name: app-opa
    connect_timeout: 0.25s
    type: strict_dns
    typed_extension_protocol_options:
      envoy.extensions.upstreams.http.v3.HttpProtocolOptions:
        "@type": type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions
        explicit_http_config:
          http2_protocol_options: {}
    load_assignment:
      cluster_name: app-opa
      endpoints:
      - lb_endpoints:
        - endpoint:
            address:
              socket_address:
                address: app-opa
                port_value: 9191

几个踩坑点:

  1. envoy 1.17 开始默认使用 v3,所以升级上来的不少配置语法要改,文档比较难找,没有手把手教程,但实际改好以后平滑升级
  2. 模拟在一个 Pod 内的场景,cluster 的定义用的是在 docker 内的 name,没有去 traefik 再兜一圈进来,所以也不需要为 app 配置 traefik labels 根据 host 做路由

policy.rego

package com.example.app.opa

import input.attributes.request.http as http_request

default allow = {
  "allowed": false,
  "headers": { "x-envoy-opa": "deny", "content-type": "application/json" },
  "body": "{ \"code\": -1, \"message\": \"deny\" }",
  "http_status": 403
}

allow = response {
  http_request.method == "OPTIONS"
  response := {
    "allowed": true,
    "headers": { "x-envoy-opa": "allow" }
  }
}

allow = response {
  allowed_methods := ["GET", "POST", "PUT", "DELETE"]
  http_request.method == allowed_methods[_]
  http_request.headers["Authorzation"] == "Basic BasicAccessToken"
  glob.match("/app/open-api/*", [], http_request.path)
  response := {
    "allowed": true,
    "headers": { "x-envoy-opa": "allow" }
  }
}

几个踩坑点:

  1. 挺顺利的,Rego 的语法值得详解一下,后面补充
  2. 使用的是 opa-envoy-plugin,所以有些语法不是 opa 自带的,而是 envoy-plugin 集成进来的,比如 glob.match
  3. 等有空研究 opa 调实时接口更新规则,再补充一下

参考资料:

  1. Envoy Getting Started
  2. Getting Started With Rego,关于 Rego 的一些反直觉的语法,但你可能需要适应这些语法,在 DSL 里写代码确实是全新的体验
  3. Policy Language
  4. https://github.com/NewbMiao/o...

你可能感兴趣的:(云原生envoypolicy)