把两条策略移入 RBAC 过滤器并验证评估顺序
目标
编写 DENY 和 ALLOW 两个 AuthorizationPolicy,先按评估顺序预测结果,再把两个策略转换成两个 Envoy RBAC 过滤器并启动,与预测对照。通过调换顺序、去掉 ALLOW、使用 dry-run,逐条确认规则。
为什么重要
授权类事故大多源于读错了评估顺序——多加了一条 ALLOW 结果全部被拦住,或者只写了路径的 DENY 切断了 TCP 服务。了解策略如何转换成过滤器链之后,看到一个 403,就能指出是哪条策略的哪一行拦住了它。
步骤
- 在
/root/ist2-authz/deny.yaml中编写 AuthorizationPolicydeny-admin(命名空间default,selector.matchLabels.app: payments,action: DENY,一条规则——to.operation.paths: ["/admin*"]),并在/root/ist2-authz/allow.yaml中编写allow-get(同一命名空间和 selector,action: ALLOW,to.operation.methods: ["GET"])。在/root/ist2-authz中,把istioctl validate -f deny.yaml -f allow.yaml的输出和退出码保存到/root/ist2-authz/01-validate.txt(最后一行为rc=)。 - 当两个策略同时作用于
payments时,仅凭评估顺序预测四个请求——GET /、GET /admin/x、POST /、POST /admin——的判定,并按<메서드> <경로>=allow|deny(占位符依次为方法与路径)的格式分四行写入/root/ist2-authz/02-predict.txt(例如:HEAD /=deny)。此时还不要启动任何东西。 - 在
/root/ist2-authz/authz.yaml中编写 Envoy 配置——管理端口9987;监听器virtualInbound为127.0.0.1:10087,HCM 的stat_prefix为inbound_0.0.0.0_8110,把所有路径发往集群inbound|8110||(127.0.0.1:8110)。http_filters恰好放三个,顺序如下——(1)istio_authz_deny:RBAC,rules.action: DENY,策略名称ns[default]-policy[deny-admin]-rule[0],权限为url_path前缀/admin;(2)envoy.filters.http.rbac:RBAC,rules.action: ALLOW,策略名称ns[default]-policy[allow-get]-rule[0],权限为头:method恰好等于GET;两个策略都带principals: [{any: true}];(3)路由器。然后用yq读取http_filters,在/root/ist2-authz/03-chain.txt中为每个过滤器写一行<이름> <rules.action>(第一个占位符为名称;路由器写-)。 - 用
python3 /opt/lab/envoy/upstream.py 8110 ok启动上游,并用/root/ist2-authz/authz.yaml启动 Envoy。把第 2 步的四个请求发往localhost:10087,在/root/ist2-authz/04-result.txt中写入四行<메서드> <경로>=<HTTP 코드>(占位符依次为方法、路径与 HTTP 状态码),并在第五行body_403=中写入GET /admin/x收到的正文的第一行。 - 把
/root/ist2-authz/authz.yaml复制为/root/ist2-authz/authz-swapped.yaml,然后只调换两个 RBAC 过滤器的顺序(ALLOW 过滤器在前,DENY 过滤器在后,路由器在最后,其余保持不变)。用这个文件重新启动 Envoy,发送同样的四个请求,并在/root/ist2-authz/05-swapped.txt中写入四行(<메서드> <경로>=<코드>,占位符依次为方法、路径与状态码)以及same_as_04=(四个状态码与第 4 步全部相同则写yes,否则写no)。 - 创建从
/root/ist2-authz/authz.yaml中只去掉 ALLOW 过滤器(envoy.filters.http.rbac)的/root/ist2-authz/authz-denyonly.yaml,并重新启动 Envoy。发送POST /和GET /admin/x,在/root/ist2-authz/06-denyonly.txt中写入两行(<메서드> <경로>=<코드>,占位符依次为方法、路径与状态码),并在第三行evaluation_rule=中,用数字写出放行POST /的是评估顺序五行中的第几行。 - 以
/root/ist2-authz/authz.yaml为基础创建/root/ist2-authz/authz-dryrun.yaml——把 DENY 过滤器(istio_authz_deny)的策略从rules移到shadow_rules(不保留rules),并在同一个过滤器中加入shadow_rules_stat_prefix: istio_dry_run_deny_。ALLOW 过滤器和路由器保持不变。用这个文件重新启动 Envoy,各发送一次GET /admin/x和POST /,并在/root/ist2-authz/07-dryrun.txt中写入四行——两个请求的<메서드> <경로>=<코드>(占位符依次为方法、路径与状态码)、stat=(管理端口/stats中以shadow_denied结尾的统计里,带前缀的那一条的完整名称)、shadow_denied=(它的值)。 - 在
/root/ist2-authz/08-report.md中写五行——chain=(第 3 步http_filters的名称,按顺序用逗号分隔)、deny_body=(RBAC 拒绝时的正文)、swapped_same=(第 5 步的same_as_04值)、dry_run_field=(放置 dry-run 策略的 RBAC 字段名称)、deny_tcp_fix=(第 1 步警告中建议添加到 DENY 规则 operation 中的字段名称)。并在下面写至少四行以-开头的说明。
参考
- 这个 Pod 中既没有真正的 istiod,也没有真正的 sidecar。所以无法用
istioctl proxy-config查看实际生成的内容,需要了解转换规则,并亲手编写等价的 Envoy 配置来确认行为。同样的规则会原样出现在生产集群的proxy-config输出中。 - 模拟上游的服务器只认识 GET。其他方法如果通过了代理,应用会返回 501。被代理拦住的请求,通过 403 和正文
RBAC: access denied来区分。 - 第 4–7 步每次修改配置文件,都要重新启动 Envoy。不要修改前面步骤的文件,请用新名称创建——评分器查看的是文件,而不是正在运行的 Envoy。
- 启动 Envoy 时,请用
setsid --fork nohup envoy -c <파일> --log-level warn > <로그> 2>&1 </dev/null(占位符依次为配置文件与日志文件)使它与 shell 完全脱离。重新启动之前,用pkill -x envoy清理(pkill -f 'envoy -c'会把包含该字符串的 shell 自己也杀掉)。 - 镜像中有模拟上游的服务器:
python3 /opt/lab/envoy/upstream.py <포트> ok|fail|slow(占位符为端口)。响应正文为<모드>:<포트> <경로>(占位符依次为模式、端口与路径)。 - 修改配置之后,启动之前先用
envoy --mode validate -c <파일>(占位符为配置文件)筛查一遍。集群名称中含有|,所以在 YAML 中必须用引号括起来。
编写 DENY 和 ALLOW 两个策略,并用 istioctl 筛查
在 /root/ist2-authz/deny.yaml 中编写 AuthorizationPolicy deny-admin(命名空间 default,selector.matchLabels.app: payments,action: DENY,一条规则——to.operation.paths: ["/admin*"]),并在 /root/ist2-authz/allow.yaml 中编写 allow-get(同一命名空间和 selector,action: ALLOW,to.operation.methods: ["GET"])。在 /root/ist2-authz 中,把 istioctl validate -f deny.yaml -f allow.yaml 的输出和退出码保存到 /root/ist2-authz/01-validate.txt(最后一行为 rc=)。
istioctl 不需要集群也能检查策略。两个文件中有一个会带有警告——虽然退出码是 0,但这是不能直接略过的内容。请把警告语句读完。这个警告源于 DENY 处理“请求中没有该属性时”的方式与 ALLOW 相反,第 8 步会再次用到。输出请用 > 파일 2>&1(占位符为文件)把两个流一起保存。
启动之前,先按评估顺序预测结果
当两个策略同时作用于 payments 时,仅凭评估顺序预测四个请求——GET /、GET /admin/x、POST /、POST /admin——的判定,并按 <메서드> <경로>=allow|deny(占位符依次为方法与路径)的格式分四行写入 /root/ist2-authz/02-predict.txt(例如:HEAD /=deny)。此时还不要启动任何东西。
Istio 的评估顺序有五行。CUSTOM 拒绝就拒绝 → 匹配 DENY 就拒绝 → 该工作负载上没有任何 ALLOW 策略就允许 → 匹配 ALLOW 就允许 → 其余拒绝。请对每个请求从上往下查,找出它最先停下来的那一行。/admin* 是前缀匹配。关键在于,在有 ALLOW 策略的工作负载上,“没有命中任何 ALLOW 的请求”会怎样。
把两个策略转换成两个 RBAC 过滤器
在 /root/ist2-authz/authz.yaml 中编写 Envoy 配置——管理端口 9987;监听器 virtualInbound 为 127.0.0.1:10087,HCM 的 stat_prefix 为 inbound_0.0.0.0_8110,把所有路径发往集群 inbound|8110||(127.0.0.1:8110)。http_filters 恰好放三个,顺序如下——(1)istio_authz_deny:RBAC,rules.action: DENY,策略名称 ns[default]-policy[deny-admin]-rule[0],权限为 url_path 前缀 /admin;(2)envoy.filters.http.rbac:RBAC,rules.action: ALLOW,策略名称 ns[default]-policy[allow-get]-rule[0],权限为头 :method 恰好等于 GET;两个策略都带 principals: [{any: true}];(3)路由器。然后用 yq 读取 http_filters,在 /root/ist2-authz/03-chain.txt 中为每个过滤器写一行 <이름> <rules.action>(第一个占位符为名称;路由器写 -)。
istiod 会把作用于一个工作负载的 DENY 策略汇集成一个过滤器,把 ALLOW 策略汇集成另一个,并把 DENY 一侧放在前面。因为一个 RBAC 过滤器只能有一个 action。策略名称 ns[<네임스페이스>]-policy[<이름>]-rule[<번호>](占位符依次为命名空间、名称与编号)是 Istio 实际使用的形式,所以在生产环境中,它是通过日志或 config_dump 找到原始资源的线索。Istio 的 /admin* 在 Envoy 中会变成 url_path.path.prefix,methods 则变成 :method 头匹配。"@type" 是 type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC。在 yq 中,没有值时的替代值写作 (.a // "-")。
启动后与预测对照
用 python3 /opt/lab/envoy/upstream.py 8110 ok 启动上游,并用 /root/ist2-authz/authz.yaml 启动 Envoy。把第 2 步的四个请求发往 localhost:10087,在 /root/ist2-authz/04-result.txt 中写入四行 <메서드> <경로>=<HTTP 코드>(占位符依次为方法、路径与 HTTP 状态码),并在第五行 body_403= 中写入 GET /admin/x 收到的正文的第一行。
方法用 curl -X POST 来改。状态码用 -o 파일 -w '%{http_code}'(占位符为文件)获取,正文会留在那个文件中。如果 RBAC 过滤器拒绝,请求不会到达上游,而是由 Envoy 直接返回 403 和一小段正文。在第 2 步中写 allow 的应该得到 200,写 deny 的应该得到 403,这样预测才算正确。如果错了,请重新追踪它是在哪一行停下的。
调换过滤器顺序,判定也相同
把 /root/ist2-authz/authz.yaml 复制为 /root/ist2-authz/authz-swapped.yaml,然后只调换两个 RBAC 过滤器的顺序(ALLOW 过滤器在前,DENY 过滤器在后,路由器在最后,其余保持不变)。用这个文件重新启动 Envoy,发送同样的四个请求,并在 /root/ist2-authz/05-swapped.txt 中写入四行(<메서드> <경로>=<코드>,占位符依次为方法、路径与状态码)以及 same_as_04=(四个状态码与第 4 步全部相同则写 yes,否则写 no)。
在 HTTP 过滤器链中,每个 RBAC 过滤器能做的事只有两件——交给下一个过滤器,或者当场拒绝。没有哪个过滤器会用“允许”这一判定跳过整条链。那么,两个过滤器放行请求的条件是如何组合在一起的呢?可以用 yq 对调列表中的两项,也可以直接手工修改文件。写下结果之前,请先用 envoy --mode validate 筛查。
没有任何 ALLOW 策略时,未被 DENY 命中的请求全部通过
创建从 /root/ist2-authz/authz.yaml 中只去掉 ALLOW 过滤器(envoy.filters.http.rbac)的 /root/ist2-authz/authz-denyonly.yaml,并重新启动 Envoy。发送 POST / 和 GET /admin/x,在 /root/ist2-authz/06-denyonly.txt 中写入两行(<메서드> <경로>=<코드>,占位符依次为方法、路径与状态码),并在第三行 evaluation_rule= 中,用数字写出放行 POST / 的是评估顺序五行中的第几行。
如果工作负载上没有 ALLOW 策略,istiod 根本不会创建 ALLOW 过滤器——因为放置一个空的 ALLOW 过滤器,所有请求都会被拒绝(RBAC 的 rules 中一条策略都没有,就会全部拒绝)。这个实验中的上游只认识 GET,对其他方法会返回 501。如果不是 403 和 RBAC: access denied,就说明代理放行了,状态码是应用决定的。请把收到的状态码原样写下。
dry-run 会把同样的策略放进 shadow_rules
以 /root/ist2-authz/authz.yaml 为基础创建 /root/ist2-authz/authz-dryrun.yaml——把 DENY 过滤器(istio_authz_deny)的策略从 rules 移到 shadow_rules(不保留 rules),并在同一个过滤器中加入 shadow_rules_stat_prefix: istio_dry_run_deny_。ALLOW 过滤器和路由器保持不变。用这个文件重新启动 Envoy,各发送一次 GET /admin/x 和 POST /,并在 /root/ist2-authz/07-dryrun.txt 中写入四行——两个请求的 <메서드> <경로>=<코드>(占位符依次为方法、路径与状态码)、stat=(管理端口 /stats 中以 shadow_denied 结尾的统计里,带前缀的那一条的完整名称)、shadow_denied=(它的值)。
在 Istio 中,给策略加上 istio.io/dry-run: "true" 注解之后,istiod 会把这条策略放进 shadow_rules,而不是 rules。RBAC 过滤器只评估 shadow 一侧,并不用于判定——结果只会留在统计和动态元数据中。完全没有 rules 的 RBAC 过滤器不会拒绝任何请求。前缀在统计名称的什么位置、用什么分隔符连接,请亲眼看过之后再抄写:curl -s localhost:<관리포트>/stats | grep shadow(占位符为管理端口)。
整理 AuthorizationPolicy 在 Envoy 中变成了什么
在 /root/ist2-authz/08-report.md 中写五行——chain=(第 3 步 http_filters 的名称,按顺序用逗号分隔)、deny_body=(RBAC 拒绝时的正文)、swapped_same=(第 5 步的 same_as_04 值)、dry_run_field=(放置 dry-run 策略的 RBAC 字段名称)、deny_tcp_fix=(第 1 步警告中建议添加到 DENY 规则 operation 中的字段名称)。并在下面写至少四行以 - 开头的说明。
数值请从前面步骤的文件中抄录。第 1 步的警告源于 DENY 把“请求中没有的属性”视为匹配——只写了 HTTP 路径的 DENY,会匹配所有没有路径这一属性的 TCP 连接。请把警告语句中建议缩小范围所用的对象,写成 operation 的字段名称(复数形式)。