Kubernetes RBAC 分析变得简单
Krane 是一个简单的 Kubernetes RBAC 静态分析工具。它可以识别 K8s RBAC 设计中的潜在安全风险,并提供缓解建议。Krane 仪表盘展示当前的 RBAC 安全态势,并允许您浏览其定义。
您可以通过在目标 Kubernetes 集群中安装 Helm chart 或使用 Docker 在本地运行来开始使用 Krane。
假设您的机器上已经安装了 Helm CLI。```sh $ helm repo add appvia https://appvia.github.io/krane $ helm repo update $ helm install krane appvia/krane --namespace krane --create-namespace
Follow Helm chart installation output on how to port-forward Krane dashboard.
### 使用 Docker 运行
假设你的本地机器上已经运行了 [docker](https://docs.docker.com/get-docker/)。如果还没有安装 [docker-compose](https://docs.docker.com/compose/install/#install-compose),请先安装。
Krane 依赖于 RedisGraph。`docker-compose` 栈定义了在本地构建和运行 _Krane_ 服务所需的一切。它还会处理对 [RedisGraph](https://oss.redislabs.com/redisgraph/) 的依赖。```
docker-compose up -d
Krane docker 镜像如果本地不存在,将自动预构建。
注意,在本地运行 docker-compose 时,Krane 不会自动启动 RBAC 报告 和 仪表板。相反,容器默认会休眠 24 小时——此值可以在 docker-compose.override.yml 中调整。进入正在运行的 Krane 容器以运行命令。本地的 docker-compose 还会将 kube 配置(~/.kube/config)挂载到容器内部,使您能够针对已拥有访问权限的任何 Kubernetes 集群运行报告。
进入正在运行的 Krane 容器。```sh docker-compose exec krane bash
一旦进入容器,你就可以开始使用 `krane` 命令。试试 `krane -help`。```sh
krane -h
检查正在运行的服务及其关联端口:``` docker-compose ps
停止 _Krane_ 及其依赖服务:```
docker-compose down
$ krane --help
NAME:
krane
DESCRIPTION:
Kubernetes RBAC static analysis & visualisation tool
COMMANDS:
dashboard Start K8s RBAC dashboard server
help Display global or [command] help documentation
report Run K8s RBAC report
GLOBAL OPTIONS:
-h, --help
Display help documentation
-v, --version
Display version information
-t, --trace
Display backtrace when an error occurs
AUTHOR:
Marcin Ciszak <[email protected]> - Appvia Ltd <appvia.io>
### 生成RBAC报告
#### 使用本地 `kubectl` 上下文
要对正在运行的集群生成报告,您必须提供 _kubectl_ 上下文```
krane report -k <context>
你也可以传递 -c <cluster-name> 标志,如果你计划针对多个集群运行该工具,并分别为每个集群名称索引 RBAC 图。
要针对本地的 RBAC yaml/json 文件运行报告,提供一个目录路径``` krane report -d </path/to/rbac-directory>
注意:_Krane_ 期望以下文件(以 YAML 或 JSON 格式)存在于指定目录路径中:
- psp
- roles
- clusterroles
- rolebindings
- clusterrolebindings
如果未使用 Pod Security Policies,您可以通过手动创建一个包含以下内容的 `psp` 文件来绕过上述期望:```json
{
"items": []
}
注意,PodSecurityPolicy 在 Kubernetes v1.21 中已被弃用,并在 v1.25 中从 Kubernetes 中移除。
要从在 Kubernetes 集群中运行的容器运行报告``` krane report --incluster
注意:_Krane_ 所使用的服务账户需要访问 RBAC 资源。详情请参见[前提条件](https://github.com/appvia/krane/blob/HEAD/k8s/one-time/prerequisites.yaml)。
#### 在 CI/CD 流水线中
在 CI/CD 流水线中将 RBAC 定义验证作为一个步骤```
krane report --ci -d </path/to/rbac-directory>
注意:Krane 期望对本地存储的RBAC资源文件遵循特定的命名约定。请参见上面的章节。为了运行 krane 命令,建议CI执行器引用 quay.io/appvia/krane:latest Docker镜像。
CI模式通过 --ci 标志启用。Krane 将返回非零状态码以及违反风险规则的详细信息,当检测到一个或多个危险时。
要查看RBAC方面树、网络图以及最新的报告发现,你需要先启动仪表板服务器。``` krane dashboard
Cluster flag `-c <cluster-name>` may be passed if you want to run the dashboard against specific cluster name. Dashboard will look for data related to specified cluster name which is cached on the file system.
如果需要针对特定集群名称运行仪表盘,可以传递集群标志 `-c <cluster-name>`。仪表盘将查找与指定集群名称相关的数据,这些数据已缓存在文件系统中。
Command above will start local web server on default port `8000`, and display the dashboard link.
上面的命令将在默认端口 `8000` 上启动本地 Web 服务器,并显示仪表盘链接。
## Architecture
## 架构
### RBAC Data indexed in a local Graph database
### 本地图数据库中的 RBAC 数据索引
_Krane_ indexes RBAC entites in RedisGraph. This allows us to query network of dependencies efficiently and simply using subset of [CypherQL](https://oss.redislabs.com/redisgraph/cypher_support/) supported by [RedisGraph](https://oss.redislabs.com/redisgraph/).
_Krane_ 在 RedisGraph 中索引 RBAC 实体。这使我们能够使用 [RedisGraph](https://oss.redislabs.com/redisgraph/) 支持的 [CypherQL](https://oss.redislabs.com/redisgraph/cypher_support/) 子集,高效且简单地查询依赖关系网络。
#### Schema
#### 架构图

#### Nodes
#### 节点
The following nodes are created in the Graph for the relevant RBAC objects:
以下节点是为相关 RBAC 对象在图数据库中创建的:
* `Psp` - A PSP node containing attributes around the pod security policy. Only applicable when working with K8s < 1.25.
* `Rule` - Rule node represents access control rule around Kubernetes resources.
* `Role` - Role node represents a given Role or ClusterRole. `kind` attribute defines type of role.
* `Subject` - Subject represents all possible actors in the cluster (`kind`: User, Group and ServiceAccount)
* `Namespace` - Kubernetes Namespace node.
* `Psp` - 包含 Pod 安全策略相关属性的 PSP 节点。仅适用于使用 K8s < 1.25 的情况。
* `Rule` - 规则节点,表示对 Kubernetes 资源的访问控制规则。
* `Role` - 角色节点,表示给定的 Role 或 ClusterRole。`kind` 属性定义角色类型。
* `Subject` - 主体节点,表示集群中所有可能的参与者(`kind`:User、Group 和 ServiceAccount)
* `Namespace` - Kubernetes 命名空间节点。
#### Edges
#### 边
* `:SECURITY` - Defines a link between Rule and Psp nodes. Only applicable when working with K8s < 1.25.
* `:GRANT` - Defines a link between Role and Rule associated with that role.
* `:ASSIGN` - Defines a link between an Actor (Subject) and given Role/ClusterRole (Role node).
* `:RELATION` - Defines a link between two different Actor (Subject) nodes.
* `:SCOPE` - Defines a link between Role and Namespace nodes.
* `:ACCESS` - Defines a link between Subject and Namespace nodes.
* `:AGGREGATE` - Defines a link between ClusterRoles (one ClusterRole aggregates another) `A-(aggregates)->B`
* `:COMPOSITE` - Defines a link between ClusterRoles (one ClusterRole can be aggregated in another) `A<-(is a composite of)-B`
* `:SECURITY` - 定义 Rule 节点与 Psp 节点之间的连接。仅适用于使用 K8s < 1.25 的情况。
* `:GRANT` - 定义 Role 节点与该角色关联的 Rule 节点之间的连接。
* `:ASSIGN` - 定义 Actor(Subject)与给定 Role/ClusterRole(Role 节点)之间的连接。
* `:RELATION` - 定义两个不同 Actor(Subject)节点之间的连接。
* `:SCOPE` - 定义 Role 节点与 Namespace 节点之间的连接。
* `:ACCESS` - 定义 Subject 节点与 Namespace 节点之间的连接。
* `:AGGREGATE` - 定义 ClusterRole 之间的连接(一个 ClusterRole 聚合另一个 ClusterRole)`A-(aggregates)->B`
* `:COMPOSITE` - 定义 ClusterRole 之间的连接(一个 ClusterRole 可被聚合到另一个中)`A<-(is a composite of)-B`
All edges are bidirectional, which means graph can be queried in either direction.
Only exceptions are `:AGGREGATE` and `:COMPOSITE` relations which are uni-directional, though concerned with the same edge nodes.
所有边都是双向的,这意味着可以从任一方向查询图。
唯一的例外是 `:AGGREGATE` 和 `:COMPOSITE` 关系,它们是单向的,但涉及相同的边节点。
#### Querying the Graph
#### 查询图
In order to query the graph directly you can exec into a running `redisgraph` container, start `redis-cli` and run your arbitrary queries. Follow official [instructions](https://oss.redislabs.com/redisgraph/) for examples of [commands](https://oss.redislabs.com/redisgraph/commands/).
要直接查询图,可以 exec 进入正在运行的 `redisgraph` 容器,启动 `redis-cli` 并运行任意查询。请参考官方 [说明](https://oss.redislabs.com/redisgraph/) 中的 [命令](https://oss.redislabs.com/redisgraph/commands/) 示例。
You can also query the Graph from _Krane_ console. First exec into running _Krane_ container, then
您也可以从 _Krane_ 控制台查询图。首先 exec 进入正在运行的 _Krane_ 容器,然后```ruby
# Start Krane console - this will open interactive ruby shell with Krane code preloaded
console
# Instantiate Graph client
graph = Krane::Clients::RedisGraph.client cluster: 'default'
# Run arbitrary CypherQL query against indexed RBAC Graph
res = graph.query(%Q(
MATCH (r:Rule {resource: "configmaps", verb: "update"})<-[:GRANT]-(ro:Role)<-[:ASSIGN]-(s:Subject)
RETURN s.kind as subject_kind, s.name as subject_name, ro.kind as role_kind, ro.name as role_name))
# Print the results
res.print_resultset
可定制的攻击场景:灵活的架构,可模拟真实的攻击模式,包括DDoS、恶意软件传播、钓鱼等。
自动化漏洞扫描:集成流行扫描工具,自动识别和利用弱点。
基于机器学习的检测绕过:先进机器学习模型,规避现代安全控制和检测机制。
全面报告:详细日志和报告,用于事后分析和合规性检查。
RedTeam Toolkit 仅用于合法的安全测试和教育目的。用户有责任确保遵守所有适用法律和法规。在测试任何不属于自己的系统之前,务必获得明确的书面许可。
⚠️ 免责声明: 作者和贡献者不对本工具包的任何误用或损坏负责。使用风险自负。```
+----------------+--------------------------------+-----------+------------------------------------------------+ | subject_kind | subject_name | role_kind | role_name | +----------------+--------------------------------+-----------+------------------------------------------------+ | ServiceAccount | bootstrap-signer | Role | system:controller:bootstrap-signer | | User | system:kube-controller-manager | Role | system::leader-locking-kube-controller-manager | | ServiceAccount | kube-controller-manager | Role | system::leader-locking-kube-controller-manager | | User | system:kube-scheduler | Role | system::leader-locking-kube-scheduler | | ServiceAccount | kube-scheduler | Role | system::leader-locking-kube-scheduler | +----------------+--------------------------------+-----------+------------------------------------------------+
注意:上面的查询示例将选择所有具有分配角色/ClusterRoles授予`update configmaps`访问权限的主体。
## 配置
### RBAC 风险规则
RBAC 风险规则定义在 [规则](https://github.com/appvia/krane/blob/HEAD/config/rules.yaml) 文件中。每条规则的结构基本不言自明。
内置规则集可以通过向 [自定义规则](https://github.com/appvia/krane/blob/HEAD/config/custom-rules.yaml) 文件添加额外的自定义规则来扩展/覆盖。
#### 风险规则宏
宏是用于一组通用/共享属性的“容器”,并被一个或多个风险规则引用。如果你选择在某个风险规则中使用宏,你需要按名称引用它,例如 `macro: <macro-name>`。请注意,在引用的 `macro` 中定义的属性将优先于规则级别上定义的相同属性。
宏可以包含以下任何属性:
- `query` - [RedisGraph 查询](#querying-the-graph)。优先级高于 `template`。需要定义 `writer`。
- `writer` - 用于格式化 `query` 结果集的 Ruby 表达式。优先级高于 `template`。
- `template` - 内置查询/writer 模板名称。如果未指定 `query` 和 `writer`,则将使用选择的查询生成器及其匹配的 writer。
#### 风险规则属性
规则可以包含以下任何属性:
- `id` [必需] 规则 ID 是唯一的规则标识符。
- `group_title` [必需] 适用于此风险检查下所有项的名称。
- `severity` [必需] 严重级别,为 :danger、:warning 或 :info 之一。
- `info` [必需] 关于检查的文本信息以及如何缓解风险的建议。
- `query` [条件] [RedisGraph 查询](#querying-the-graph)。
- 优先级高于 `template`。需要定义 `writer`。
- `writer` [条件] 用于格式化查询结果集的 Ruby 表达式。
- 优先级高于 `template`。需要定义 `query`。
- `template` [条件] 内置查询/writer 模板名称。如果未指定 `query` 和 `writer`,则将使用选择的查询生成器及其匹配的 writer。
- 某些内置模板需要在单个规则级别指定 `match_rules` 属性,以便构建正确的查询。当前需要它的模板包括:
- **_risky-role_** - 根据 `match_rules` 指定的访问规则构建多匹配图查询。生成的图查询返回以下列:
- role_name
- role_kind
- namespace_name(如果返回多个项目,则返回一个 _数组_)
- `match_rules` [条件] 当 `template` 依赖匹配规则来构建查询时需要。
- 示例:
```yaml
match_rules:
- resources: ['cronjobs']
verbs: ['update']
```
属性和值遵循 [Kubernetes RBAC 角色规范](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#role-examples)。
- `custom_params` [可选] 自定义键值对列表,将在规则 `query` 和 `writer` 表示中被评估和替换。
- 示例:
```yaml
custom_params:
- attrA: valueA
- attrB: valueB
```
模板中上述键的占位符 `{{attrA}}` 和 `{{attrB}}` 将分别被替换为 `valueA` 和 `valueB`。
- `threshold` [可选] 数值。如果定义,它将作为模板占位符 `{{threshold}}` 在 `writer` 表达式中可用。
- `macro` [可选] 引用在命名宏中定义的公共参数。
- `disabled` [可选] 设置为 `true` 时将禁用给定规则并将其排除在评估之外。
默认情况下所有规则都是启用的。
#### 风险规则示例
##### 显式查询和 writer 表达式```yaml
- id: verbose-rule-example
group_title: Example rule
severity: :danger
info: Risk description and instructions on how to mitigate it goes here
query: |
MATCH
(s:Subject)-[:ACCESS]->(ns:Namespace)
WHERE
NOT s.name IN {{whitelist_subject_names}}
RETURN
s.kind as subject_kind,
s.name as subject_name,
COLLECT(ns.name) as namespace_names
ORDER BY
subject_kind,
subject_name,
namespace_names DESC
threshold: 2
writer: |
if result.namespace_names.count > {{threshold}}
"#{result.subject_kind} #{result.subject_name} can access namespaces: #{result.namespace_names.join(', ')}"
end
disabled: true
上面的示例显式定义了一个用于评估 RBAC 风险的图形 query,以及一个用于格式化查询结果集的 writer 表达式。该查询仅仅选择所有 Subjects(排除白名单中的)以及它们有权访问的 Namespaces。注意,结果集只包含有权访问超过 2 个 Namespaces 的 Subjects(注意到那里的 threshold 值了吗?)。最后的 writer 表达式将被捕获并输出为格式化结果条目。
writer 可以通过 result 对象访问结果集中的条目,该对象具有与查询返回的元素匹配的方法,例如 result.subject_kind、result.subject_name 等。
注意:
writer 表达式中的 {{threshold}} 占位符将被替换为规则的 threshold 关键字值。{{whitelist_subject_names}} 表示一个自定义字段,它将与为给定规则 id 定义的 Whitelist 值进行插值。如果白名单中未定义占位符字段名称,则默认将其替换为空数组 ['']。有关白名单的更多信息,请参见下文。内置模板显著简化了风险规则的定义,然而,它们旨在提取特定类型的信息,可能不适用于您的自定义规则。如果您发现自己跨多个规则重用相同的 query 或 writer 表达式,应考虑将这些表达式提取到一个 macro 中,并在自定义规则中引用它们以避免重复(DRY)。```yaml
上面的示例展示了其中一个内置规则。它引用了 `risky-role` 模板,在处理时,该模板会在规则评估触发之前通过注入 `query` 和 `writer` 表达式来扩展规则。`match_rules` 将用于构建适当的匹配查询。
### RBAC 风险白名单
可选的白名单包含一组自定义属性名称及其对应的(白名单)值。
#### 白名单属性
属性名称及其值是任意的。它们在 [Whitelist](https://github.com/appvia/krane/blob/HEAD/config/whitelist.yaml) 文件中定义,并分为三个独立的部分:
- `global` - 顶层作用域。此处定义的属性将适用于所有风险规则,无论集群名称如何。
- `common` - 自定义属性将限定到特定的风险规则 `id`,无论集群名称如何。
- `cluster`(带有嵌套的集群名称列表) - 自定义属性将适用于给定集群名称下的特定风险规则 `id`。
每个 [风险规则](#rbac-risk-rules) 在评估时,将尝试插值 `query` 中使用的所有参数占位符,例如 `{{your_whitelist_attribute_name}}`。如果某个占位符参数名称(即双花括号之间的名称)与该风险规则 `id` 的任何白名单属性名称匹配,它将被其计算值替换。
如果未找到给定占位符的值,则将其替换为 `['']`。
#### 白列表示例
下面的白列表示例为 `id` 属性值匹配 _"some-risk-rule-id"_ 的 [风险规则](#rbac-risk-rules) 产生了以下 `placeholder-key => value` 映射。```
{{whitelist_role_names}} => ['acp:prometheus:operator']
{{whitelist_subject_names}} => ['privileged-psp-user', 'another-user']
上面的占位符键,当在自定义图查询中使用时,将在风险评估规则评估时被替换为其各自的值。
rules: global: # global scope - applies to all risk rule and cluster names whitelist_role_names: # custom attribute name - acp:prometheus:operator # custom attribute values
common: # common scope - applies to specific risk rule id regardless of cluster name some-risk-rule-id: # this corresponds to risk rule id defined in config/rules.yaml whitelist_subject_names: # custom attribute name - privileged-psp-user # custom attribute values
cluster: # cluster scope - applies to speciifc risk rule id and cluster name default: # example cluster name some-risk-rule-id: # risk rule id whitelist_subject_names: # custom attribute nane - another-user # custom attribute values
## Kubernetes 部署
_Krane_ 可以轻松部署到本地或远程的 Kubernetes 集群。
### K8s 先决条件
Kubernetes 命名空间、服务账户以及适当的 RBAC 必须存在于集群中。请参考[先决条件](https://github.com/appvia/krane/blob/HEAD/k8s/one-time/prerequisites.yaml)。
默认的 _Krane_ 入口点执行 [bin/in-cluster-run](https://github.com/appvia/krane/blob/HEAD/bin/in-cluster-run),它会等待 RedisGraph 实例可用,然后启动 RBAC _报告_ 循环和 _仪表盘_ Web 服务器。
您可以通过以下环境变量控制集群内执行的某些方面:
* `KRANE_REPORT_INTERVAL` - 定义 RBAC 静态分析报告运行的间隔(秒)。默认值:`300`(秒,即 5 分钟)。
* `KRANE_REPORT_OUTPUT` - 定义 RBAC 风险报告输出格式。可能的值:`:json`、`:yaml`、`:none`。默认值:`:json`。
### 本地或远程 K8s 集群
#### Helm Chart
在开始之前,您需要以下工具:
* [Helm CLI](https://helm.sh/docs/intro/install/)
安装 Helm Chart:```sh
$ helm repo add appvia https://appvia.github.io/krane
$ helm repo update
$ helm install krane appvia/krane --namespace krane --create-namespace
详见 values.yaml 文件中其他可设置选项和参数的详细信息。
kubectl create
--context
--namespace krane
-f k8s/redisgraph-service.yaml
-f k8s/redisgraph-deployment.yaml
-f k8s/krane-service.yaml
-f k8s/krane-deployment.yaml
注意,_Krane_ 仪表盘服务默认不暴露!```sh
kubectl port-forward svc/krane 8000 \
--context=<docker-desktop> \
--namespace=krane
# Open Krane dashboard at http://localhost:8000
你可以在 k8s 目录中找到示例部署清单。
根据你的部署需求修改清单,确保在其 部署文件 中引用正确版本的 Krane Docker 镜像。有关可用标签,请参阅 Krane Docker Registry,或者直接使用 latest。
如果你的 Kubernetes 集群内置了 Compose-on-Kubernetes 控制器支持(docker-desktop 默认支持),那么你可以通过一条 docker stack 命令来部署 Krane 及其依赖项:```sh
docker stack deploy
--orchestrator kubernetes
--namespace krane
--compose-file docker-compose.yml
--compose-file docker-compose.k8s.yml krane
注意:在运行上述命令之前,请确保当前的 kube 上下文设置正确!
现在,应用栈应已部署到 Kubernetes 集群,所有服务已就绪并暴露。请注意,_Krane_ 将自动启动其报告循环和仪表板服务器。```sh
docker stack services --orchestrator kubernetes --namespace krane krane
上述命令将产生以下输出:``` ID NAME MODE REPLICAS IMAGE PORTS 0de30651-dd5 krane_redisgraph replicated 1/1 redislabs/redisgraph:1.99.7 *:6379->6379/tcp aa377a5f-62b krane_krane replicated 1/1 quay.io/appvia/krane:latest *:8000->8000/tcp
通过访问 http://localhost:8000 检查你的 Kubernetes 集群 RBAC 安全态势。注意,对于远程集群部署,你可能需要先端口转发 _Krane_ 服务。```sh
kubectl --context=my-remote-cluster --namespace=krane port-forward svc/krane 8000
删除堆栈```sh
docker stack rm krane
--orchestrator kubernetes
--namespace krane
## Notifications
Krane 将通过其 Slack 集成通知您检测到的中高严重性异常。
若要启用通知,请在 [config/config.yaml](https://github.com/appvia/krane/blob/HEAD/config/config.yaml) 文件中指定 Slack 的 `webhook_url` 和 `channel`,或者同时设置 `SLACK_WEBHOOK_URL` 和 `SLACK_CHANNEL` 环境变量。环境变量将优先于配置文件中的值。
## Local Development
本节介绍启用本地开发的步骤。
### Setup
使用以下命令安装 _Krane_ 代码依赖项```sh
./bin/setup
Krane 依赖于 RedisGraph。docker-compose 是在本地运行 Krane 依赖项的最快方式。```sh
docker-compose up -d redisgraph
检查 RedisGraph 服务是否已启动:```sh
docker-compose ps
停止服务:```sh docker-compose down
### 开发
此时,你应该能够通过在本机 shell 中调用命令来修改 _Krane_ 代码库并测试结果。```sh
$ ./bin/krane --help # to get help
$ ./bin/krane report -k docker-desktop # to generate your first report for
# local docker-desktop k8s cluster
...
启用仪表板UI本地开发模式```sh $ cd dashboard $ npm install $ npm start
这将自动启动 Dashboard 服务器,打开默认浏览器并监视源文件的更改。
_Krane_ 预配置了改进的开发体验,配合 [Skaffold](https://skaffold.dev/) 使用。在本地或远程 Kubernetes 集群中运行整个栈来迭代项目并验证应用程序变得更加容易。
代码热重载使得本地更改能够自动传播到正在运行的容器中,从而加快开发周期。```sh
skaffold dev --kube-context docker-desktop --namespace krane --port-forward
在本地运行测试使用```sh bundle exec rspec
## 为 Krane 做出贡献
我们欢迎来自社区的任何贡献!请查看我们的[贡献指南](https://github.com/appvia/krane/blob/HEAD/CONTRIBUTING.md)以了解如何开始。如果您使用 _Krane_、认为它有用,或者对 Kubernetes 安全性感兴趣,请通过**加星**和**关注**此仓库来告诉我们。谢谢!
## 参与其中
加入我们[社区频道](https://www.appvia.io/join-the-appvia-community)的讨论。
Krane 是一个社区项目,我们欢迎您的贡献。要报告 bug、提出改进建议或请求新功能,请打开一个 Github issue。请参阅我们的[贡献指南](https://github.com/appvia/krane/blob/HEAD/CONTRIBUTING.md)以了解更多关于如何提供帮助的信息。
## 路线图
查看我们的[路线图](https://github.com/appvia/krane/projects/1),了解有关项目计划的详细信息。
## 许可证
作者:Marcin Ciszak <[email protected]>
版权所有 (c) 2019-2020 [Appvia Ltd](https://appvia.io)
本项目根据 [Apache 许可证 2.0 版](https://github.com/appvia/krane/blob/HEAD/LICENSE) 分发。