作者仅是云原生安全相关和opa相关生态的初学者,在此分享一些学习笔记和经验总结,以下是参考文章:

http://blog.newbmiao.com/2020/03/13/opa-quick-start.html

https://github.com/NewbMiao/opa-koans

https://moelove.info/2021/12/06/Open-Policy-Agent-OPA-%E5%85%A5%E9%97%A8%E5%AE%9E%E8%B7%B5/

Rego和OPA

Rego 是一种声明式、功能强大的策略语言,由 Open Policy Agent(OPA)项目开发并维护。OPA 是一个开源、轻量级、云原生的安全和策略引擎,用于在微服务、云和 Kubernetes 中实现统一的访问控制和策略管理。

OPA解决的问题

img

可以看到,很多云原生生态相关的产品,都需要通过配置好的策略来进行控制,比如Dockerfile,K8s等等。而在不同的场景中,他们都需要和不同的开发者开发的应用做耦合,难以管理,OPA可以做到统一配置的策略,降低维护成本,方便服用的工作。

声明式策略语言Rego

那么OPA是如何实现统一配置策略呢,就是通过将业务和策略分离,将策略统一发送给引擎,引擎通过规则来进行决策。

image-20230419153130555

从图中可以看到,当请求进来时,OPA会通过Rego来“翻译”规则,并且根据Rego的结果,进行对应的决策,发送给Service。

语法

也许您只是想浏览一下文章大意,那么可以略过语法这一部分。

如果想尝试一下,可以使用在线运行网站或者安装opa的命令行工具又或者是vsc,jetbrains的opa插件。

有几个比较抽象的概念,这里简单介绍一下,但是还是建议看官方文档,学语法二手资料实在看起来折磨…

定义变量

1
2
pi := 3.14159
rect := {"width": 2, "height": 4}

比较

1
2
rect == {"height": 4, "width": 2} //比较复合值
v if "hello" == "world" //表达式为真则v为真

_:如果变量在引用之外未使用,我们更愿意将它们替换为下划线 (_) 字符。

同名函数

多个同名函数,满足一个就为true

对于这样的输入,来写一个规则判断配置文件的后缀,只要满足一个,那么函数的返回就是true

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"files": [
{
"type": "posix",
"path": "/Users/newbmiao/Documents/1.yaml"
},
{
"type": "posix",
"path": "/Users/newbmiao/Documents/2.yaml"
},
{
"type": "traditional-mac",
"path": "Macintosh HD:Users:newbmiao:Documents:3.yml"
},
{
"type": "traditional-mac",
"path": "Macintosh HD:Users:newbmiao:Documents:3.json"
}
]
}
1
2
3
4
5
6
7
8
9
10
11
is_config_file(str) {
contains(str, ".yaml")
}

is_config_file(str) {
contains(str, ".yml")
}

is_config_file(str) [
contains(str, ".json")
]

剩下的虚拟文档,推导式等等就先没看了,简单了解了一下语法

实践

这部分主要是学习Trivy这个开源的容器安全扫描器中对于rego的使用,主要是在iac的安全检查这一块的应用。相比于直接用正则匹配,用rego来写规则,对Dockerfile或者k8s的配置文件等进行检查,会更易于编写也能够更准确的匹配。

其中检测规则这部分主要在这个仓库https://github.com/aquasecurity/defsec/tree/master/rules

这里看一个简单的例子,按照官方的Dockerfile编写最佳实践,如果不解压tar`文件,最好用COPY取代ADD,那么如果我们想对Dockerfile做检测怎么办呢,Trivy的这个检测部分就是先将Dockerfile解析成JSON,然后根据传入的JSON,用写好的rego规则进行匹配。

https://github.com/aquasecurity/defsec/blob/master/rules/docker/policies/add_instead_of_copy.rego

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
# METADATA
# title: ADD instead of COPY
# description: You should use COPY instead of ADD unless you want to extract a tar file. Note that an ADD command will extract a tar file, which adds the risk of Zip-based vulnerabilities. Accordingly, it is advised to use a COPY command, which does not extract tar files.
# scope: package
# related_resources:
# - https://docs.docker.com/engine/reference/builder/#add
# schemas:
# - input: schema["dockerfile"]
# custom:
# id: DS005
# avd_id: AVD-DS-0005
# severity: LOW
# short_code: use-copy-over-add
# recommended_action: Use COPY instead of ADD
# input:
# selector:
# - type: dockerfile
package builtin.dockerfile.DS005

import data.lib.docker

get_add[output] {
add := docker.add[_]
args := concat(" ", add.Value)

not contains(args, ".tar")
output := {
"args": args,
"cmd": add,
}
}

deny[res] {
output := get_add[_]
msg := sprintf("Consider using 'COPY %s' command instead of 'ADD %s'", [output.args, output.args])
res := result.new(msg, output.cmd)
}

这个规则比较好懂,先导入了data.lib.docker这个包中的add,那么看一下他们抽出来的这个lib库做了什么。

https://github.com/aquasecurity/defsec/blob/master/rules/docker/lib/docker.rego

1
2
3
4
add[instruction] {
instruction := input.Stages[_].Commands[_]
instruction.Cmd == "add"
}

主要是做了封装,对传入的JSON根据Dockerfile中的具体命令做匹配,比如说ADDCOPY等等,做对应的封装。

get_add函数做的就是获取DockerfileADD命令所对应的内容,检测是否包含.tar

deny函数做的是调用get_add,如果有对应的output,那么就会触发deny,往result中添加结果。