更好的外部授权
AuthorizationPolicy 现在支持 CUSTOM 操作,将授权委托给外部系统。
背景
Istio 的授权策略为网格中的服务提供访问控制。它速度快、功能强大,并且是广泛使用的功能。自 Istio 1.4 首次发布以来,我们一直在不断改进策略以使其更加灵活,包括 DENY
操作、排除语义、X-Forwarded-For
标头支持、嵌套 JWT 声明支持 等等。这些功能提高了授权策略的灵活性,但仍然有很多用例无法通过此模型支持,例如
您有自己的内部授权系统,无法轻松迁移到或无法轻松替换为授权策略。
您希望与第三方解决方案(例如 Open Policy Agent 或
oauth2
代理)集成,这可能需要在 Istio 中使用 低级 Envoy 配置 API,或者根本不可能。授权策略缺乏您的用例所需的语义。
解决方案
在 Istio 1.9 中,我们通过引入 CUSTOM
操作 在授权策略中实现了可扩展性,这使您可以将访问控制决策委托给外部授权服务。
CUSTOM
操作使您可以将 Istio 与实现其自定义授权逻辑的外部授权系统集成。下图显示了这种集成的总体架构
在配置时,网格管理员会配置一个具有 CUSTOM
操作的授权策略,以在代理(网关或边车)上启用外部授权。管理员应验证外部授权服务是否已启动并正在运行。
在运行时,
代理会拦截请求,并且代理会将检查请求发送到外部授权服务,如用户在授权策略中配置的那样。
外部授权服务将决定是否允许该请求。
如果允许,请求将继续并由
ALLOW
/DENY
操作定义的任何本地授权强制执行。如果拒绝,请求将立即被拒绝。
让我们看一个带有 CUSTOM
操作的授权策略示例
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: ext-authz
namespace: istio-system
spec:
# The selector applies to the ingress gateway in the istio-system namespace.
selector:
matchLabels:
app: istio-ingressgateway
# The action "CUSTOM" delegates the access control to an external authorizer, this is different from
# the ALLOW/DENY action that enforces the access control right inside the proxy.
action: CUSTOM
# The provider specifies the name of the external authorizer defined in the meshconfig, which tells where and how to
# talk to the external auth service. We will cover this more later.
provider:
name: "my-ext-authz-service"
# The rule specifies that the access control is triggered only if the request path has the prefix "/admin/".
# This allows you to easily enable or disable the external authorization based on the requests, avoiding the external
# check request if it is not needed.
rules:
- to:
- operation:
paths: ["/admin/*"]
它引用了一个名为 my-ext-authz-service
的提供程序,该提供程序在网格配置中定义
extensionProviders:
# The name "my-ext-authz-service" is referred to by the authorization policy in its provider field.
- name: "my-ext-authz-service"
# The "envoyExtAuthzGrpc" field specifies the type of the external authorization service is implemented by the Envoy
# ext-authz filter gRPC API. The other supported type is the Envoy ext-authz filter HTTP API.
# See more in https://envoy.k8s.ac.cn/docs/envoy/v1.16.2/intro/arch_overview/security/ext_authz_filter.
envoyExtAuthzGrpc:
# The service and port specifies the address of the external auth service, "ext-authz.istio-system.svc.cluster.local"
# means the service is deployed in the mesh. It can also be defined out of the mesh or even inside the pod as a separate
# container.
service: "ext-authz.istio-system.svc.cluster.local"
port: 9000
具有 CUSTOM
操作 的授权策略在运行时启用外部授权,它可以被配置为有条件地触发外部授权,具体取决于使用与您在其他操作中已经使用相同的规则的请求。
外部授权服务目前在 meshconfig
API 中定义,并通过其名称引用。它可以与或不与代理一起部署在网格中。如果有代理,您还可以进一步使用 PeerAuthentication
来在代理和您的外部授权服务之间启用 mTLS。
CUSTOM
操作目前处于 **实验阶段**;根据用户反馈,API 可能会以不向后兼容的方式更改。当与 CUSTOM
操作一起使用时,授权策略规则目前不支持身份验证字段(例如源主体或 JWT 声明)。对于给定的工作负载,只允许一个提供程序,但您仍然可以在不同的工作负载上使用不同的提供程序。
有关更多信息,请参阅 更好的外部授权设计文档。
与 OPA 的示例
在本节中,我们将演示在入口网关上使用 Open Policy Agent 作为外部授权器与 CUSTOM
操作一起使用。我们将有条件地在除 /ip
之外的所有路径上启用外部授权。
您也可以参考 外部授权任务,以获取更基本的介绍,其中使用示例 ext-authz
服务器。
创建示例 OPA 策略
运行以下命令创建 OPA 策略,如果路径的前缀与 JWT 令牌中的声明“path”(base64 编码)匹配,则允许请求
$ cat > policy.rego <<EOF
package envoy.authz
import input.attributes.request.http as http_request
default allow = false
token = {"valid": valid, "payload": payload} {
[_, encoded] := split(http_request.headers.authorization, " ")
[valid, _, payload] := io.jwt.decode_verify(encoded, {"secret": "secret"})
}
allow {
is_token_valid
action_allowed
}
is_token_valid {
token.valid
now := time.now_ns() / 1000000000
token.payload.nbf <= now
now < token.payload.exp
}
action_allowed {
startswith(http_request.path, base64url.decode(token.payload.path))
}
EOF
$ kubectl create secret generic opa-policy --from-file policy.rego
部署 httpbin 和 OPA
启用边车注入
$ kubectl label ns default istio-injection=enabled
运行以下命令来部署示例应用程序 httpbin 和 OPA。OPA 可以部署为 httpbin pod 中的单独容器,也可以完全部署在单独的 pod 中
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: httpbin-with-opa
labels:
app: httpbin-with-opa
service: httpbin-with-opa
spec:
ports:
- name: http
port: 8000
targetPort: 80
selector:
app: httpbin-with-opa
---
# Define the service entry for the local OPA service on port 9191.
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
name: local-opa-grpc
spec:
hosts:
- "local-opa-grpc.local"
endpoints:
- address: "127.0.0.1"
ports:
- name: grpc
number: 9191
protocol: GRPC
resolution: STATIC
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: httpbin-with-opa
labels:
app: httpbin-with-opa
spec:
replicas: 1
selector:
matchLabels:
app: httpbin-with-opa
template:
metadata:
labels:
app: httpbin-with-opa
spec:
containers:
- image: docker.io/kennethreitz/httpbin
imagePullPolicy: IfNotPresent
name: httpbin
ports:
- containerPort: 80
- name: opa
image: openpolicyagent/opa:latest-envoy
securityContext:
runAsUser: 1111
volumeMounts:
- readOnly: true
mountPath: /policy
name: opa-policy
args:
- "run"
- "--server"
- "--addr=localhost:8181"
- "--diagnostic-addr=0.0.0.0:8282"
- "--set=plugins.envoy_ext_authz_grpc.addr=:9191"
- "--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow"
- "--set=decision_logs.console=true"
- "--ignore=.*"
- "/policy/policy.rego"
livenessProbe:
httpGet:
path: /health?plugins
scheme: HTTP
port: 8282
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /health?plugins
scheme: HTTP
port: 8282
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: proxy-config
configMap:
name: proxy-config
- name: opa-policy
secret:
secretName: opa-policy
EOF
$ kubectl apply -f - <<EOF
apiVersion: v1
kind: Service
metadata:
name: opa
labels:
app: opa
spec:
ports:
- name: grpc
port: 9191
targetPort: 9191
selector:
app: opa
---
kind: Deployment
apiVersion: apps/v1
metadata:
name: opa
labels:
app: opa
spec:
replicas: 1
selector:
matchLabels:
app: opa
template:
metadata:
labels:
app: opa
spec:
containers:
- name: opa
image: openpolicyagent/opa:latest-envoy
securityContext:
runAsUser: 1111
volumeMounts:
- readOnly: true
mountPath: /policy
name: opa-policy
args:
- "run"
- "--server"
- "--addr=localhost:8181"
- "--diagnostic-addr=0.0.0.0:8282"
- "--set=plugins.envoy_ext_authz_grpc.addr=:9191"
- "--set=plugins.envoy_ext_authz_grpc.query=data.envoy.authz.allow"
- "--set=decision_logs.console=true"
- "--ignore=.*"
- "/policy/policy.rego"
ports:
- containerPort: 9191
livenessProbe:
httpGet:
path: /health?plugins
scheme: HTTP
port: 8282
initialDelaySeconds: 5
periodSeconds: 5
readinessProbe:
httpGet:
path: /health?plugins
scheme: HTTP
port: 8282
initialDelaySeconds: 5
periodSeconds: 5
volumes:
- name: proxy-config
configMap:
name: proxy-config
- name: opa-policy
secret:
secretName: opa-policy
EOF
也部署 httpbin
$ kubectl apply -f @samples/httpbin/httpbin.yaml@
定义外部授权器
运行以下命令来编辑 meshconfig
$ kubectl edit configmap istio -n istio-system
将以下 extensionProviders
添加到 meshconfig
中
apiVersion: v1
data:
mesh: |-
# Add the following contents:
extensionProviders:
- name: "opa.local"
envoyExtAuthzGrpc:
service: "local-opa-grpc.local"
port: "9191"
apiVersion: v1
data:
mesh: |-
# Add the following contents:
extensionProviders:
- name: "opa.default"
envoyExtAuthzGrpc:
service: "opa.default.svc.cluster.local"
port: "9191"
创建一个带有 CUSTOM 操作的 AuthorizationPolicy
运行以下命令来创建授权策略,该策略在除 /ip
之外的所有路径上启用外部授权
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin-opa
spec:
selector:
matchLabels:
app: httpbin-with-opa
action: CUSTOM
provider:
name: "opa.local"
rules:
- to:
- operation:
notPaths: ["/ip"]
EOF
$ kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
name: httpbin-opa
spec:
selector:
matchLabels:
app: httpbin
action: CUSTOM
provider:
name: "opa.default"
rules:
- to:
- operation:
notPaths: ["/ip"]
EOF
测试 OPA 策略
创建一个客户端 pod 来发送请求
$ kubectl apply -f @samples/sleep/sleep.yaml@ $ export SLEEP_POD=$(kubectl get pod -l app=sleep -o jsonpath={.items..metadata.name})
使用 OPA 签名的测试 JWT 令牌
$ export TOKEN_PATH_HEADERS="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXRoIjoiTDJobFlXUmxjbk09IiwibmJmIjoxNTAwMDAwMDAwLCJleHAiOjE5MDAwMDAwMDB9.9yl8LcZdq-5UpNLm0Hn0nnoBHXXAnK4e8RSl9vn6l98"
测试 JWT 令牌具有以下声明
{ "path": "L2hlYWRlcnM=", "nbf": 1500000000, "exp": 1900000000 }
path
声明的值为L2hlYWRlcnM=
,它是/headers
的 base64 编码。向路径
/headers
发送没有令牌的请求。由于没有 JWT 令牌,这将被拒绝并返回 403 错误。$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin-with-opa:8000/headers -s -o /dev/null -w "%{http_code}\n" 403
$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin:8000/headers -s -o /dev/null -w "%{http_code}\n" 403
向路径
/get
发送带有有效令牌的请求。由于路径/get
与令牌/headers
不匹配,这将被拒绝并返回 403 错误。$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin-with-opa:8000/get -H "Authorization: Bearer $TOKEN_PATH_HEADERS" -s -o /dev/null -w "%{http_code}\n" 403
$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin:8000/get -H "Authorization: Bearer $TOKEN_PATH_HEADERS" -s -o /dev/null -w "%{http_code}\n" 403
向路径
/headers
发送带有有效令牌的请求。由于路径与令牌匹配,这将被允许并返回 200 状态码。$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin-with-opa:8000/headers -H "Authorization: Bearer $TOKEN_PATH_HEADERS" -s -o /dev/null -w "%{http_code}\n" 200
$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin:8000/headers -H "Authorization: Bearer $TOKEN_PATH_HEADERS" -s -o /dev/null -w "%{http_code}\n" 200
向路径
/ip
发送没有令牌的请求。由于路径/ip
被排除在授权之外,这将被允许并返回 200 状态码。$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin-with-opa:8000/ip -s -o /dev/null -w "%{http_code}\n" 200
$ kubectl exec ${SLEEP_POD} -c sleep -- curl http://httpbin:8000/ip -s -o /dev/null -w "%{http_code}\n" 200
检查代理和 OPA 日志以确认结果。
总结
在 Istio 1.9 中,授权策略中的 CUSTOM
操作允许您轻松地将 Istio 与任何外部授权系统集成,并具有以下优势
授权策略 API 中的一流支持
易于使用:只需使用 URL 定义外部授权器,然后使用授权策略启用它,无需再费心使用
EnvoyFilter
API条件触发,从而提高性能
支持各种外部授权器的部署类型
具有或不具有代理的普通服务和 pod
作为单独容器位于工作负载 pod 内
位于网格外部
我们正在努力在后续版本中将此功能提升到更稳定的阶段,欢迎您在 discuss.istio.io 上提供反馈。
致谢
感谢 Craig Box
、Christian Posta
和 Limin Wang
审阅本文草稿。