安全是 Dapr 的基础,本文我们将来说明在分布式应用中使用 Dapr 时的安全特性和能力,主要可以分为以下几个方面。
- 与服务调用和
pub/sub
APIs 的安全通信。 - 组件上的安全策略并通过配置进行应用。
- 运维操作安全实践。
- 状态安全,专注于静态的数据。
Dapr 通过服务调用 API 提供端到端的安全性,能够使用 Dapr 对应用程序进行身份验证并设置端点访问策略。
安全通信
服务调用范围访问策略
跨命名空间的服务调用
Dapr 应用程序可以被限定在特定的命名空间,以实现部署和安全,当然我们仍然可以在部署到不同命名空间的服务之间进行调用。默认情况下,服务调用支持通过简单地引用应用 ID (比如 nodeapp
) 来调用同一命名空间内的服务:
localhost:3500/v1.0/invoke/nodeapp/method/neworder
服务调用还支持跨命名空间的调用,在所有受支持的托管平台上,Dapr 应用程序 ID 符合包含目标命名空间的有效 FQDN
格式,可以同时指定:
- 应用 ID (如
nodeapp
) - 应用程序运行的命名空间(
production
)。
比如在 production
命名空间中的 nodeapp
应用上调用 neworder
方法,则可以使用下面的方式:
localhost:3500/v1.0/invoke/nodeapp.production/method/neworder
当使用服务调用在命名空间中调用应用程序时,我们可以使用命名空间对其进行限定,特别在 Kubernetes 集群中的跨命名空间调用是非常有用的。
为服务调用应用访问控制列表配置
访问控制策略在配置文件中被指定,并被应用于被调用应用程序的 Dapr sidecar,对被调用应用程序的访问是基于匹配的策略动作,你可以为所有调用应用程序提供一个默认的全局动作,如果没有指定访问控制策略,默认行为是允许所有调用应用程序访问被调用的应用程序。
在具体学习访问控制策略配置之前,我们需要先了解两个概念:
TrustDomain
- “信任域”是管理信任关系的逻辑组。每个应用程序都分配有一个信任域,可以在访问控制列表策略规范中指定。如果未定义策略规范或指定了空的信任域,则使用默认值public
,该信任域用于在 TLS 证书中生成应用程序的身份。App Identity
- Dapr 请求 sentry 服务为所有应用程序生成一个 SPIFFE id,这个 id 附加在 TLS 证书中。SPIFFE id 的格式为:spiffe://<trustdomain>/ns/<namespace>/<appid>
,对于匹配策略,调用应用的信任域、命名空间和应用 ID 值从调用应用的 TLS 证书中的SPIFFE id
中提取,这些值与策略规范中指定的信任域、命名空间和应用 ID 值相匹配。如果这三个都匹配,则更具体的策略将进一步匹配。
访问控制策略会遵循如下所示的一些规则:
- 如果未指定访问策略,则默认行为是允许所有应用访问被调用应用上的所有方法
- 如果未指定全局默认操作且未定义应用程序特定策略,则将空访问策略视为未指定访问策略,并且默认行为是允许所有应用程序访问被调用应用程序上的所有方法
- 如果未指定全局默认操作,但已定义了一些特定于应用程序的策略,则会采用更安全的选项,即假设全局默认操作拒绝访问被调用应用程序上的所有方法
- 如果定义了访问策略并且无法验证传入的应用程序凭据,则全局默认操作将生效
- 如果传入应用的信任域或命名空间与应用策略中指定的值不匹配,则应用策略将被忽略并且全局默认操作生效
下面是一些使用访问控制列表进行服务调用的示例场景。
场景 1:拒绝所有应用程序的访问,除非 trustDomain = public
、namespace = default
、appId = app1
,使用如下所示的配置,允许所有 appId = app1
的调用方法,并拒绝来自其他应用程序的所有其他调用请求。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
accessControl:
defaultAction: deny
trustDomain: "public"
policies:
- appId: app1
defaultAction: allow
trustDomain: "public"
namespace: "default"
场景 2:拒绝访问除 trustDomain = public
、namespace = default
、appId = app1
、operation = op1
之外的所有应用程序,使用此配置仅允许来自 appId = app1
的方法 op1
,并且拒绝来自所有其他应用程序的所有其他方法请求,包括 app1
上的其他方法。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
accessControl:
defaultAction: deny
trustDomain: "public"
policies:
- appId: app1
defaultAction: deny
trustDomain: "public"
namespace: "default"
operations:
- name: /op1
httpVerb: ["*"]
action: allow
场景 3:拒绝对所有应用程序的访问,除非 HTTP 的特定 verb 和 GRPC 的操作匹配,使用如下所示的配置,仅允许以下场景访问,并且来自所有其他应用程序的所有其他方法请求(包括 app1
或 app2
上的其他方法)都会被拒绝。
- trustDomain = public、namespace = default、appID = app1、operation = op1、http verb = POST/PUT
- trustDomain = “myDomain”、namespace = “ns1”、appID = app2、operation = op2 并且应用程序协议是 GRPC,仅允许来自
appId = app1
的方法op1
上的 POST/PUT 请求以及来自所有其他应用程序的所有其他方法请求,包括app1
上的其他方法,被拒绝
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
accessControl:
defaultAction: deny
trustDomain: "public"
policies:
- appId: app1
defaultAction: deny
trustDomain: "public"
namespace: "default"
operations:
- name: /op1
httpVerb: ["POST", "PUT"]
action: allow
- appId: app2
defaultAction: deny
trustDomain: "myDomain"
namespace: "ns1"
operations:
- name: /op2
action: allow
场景 4:允许访问除 trustDomain = public
、namespace = default
、appId = app1
、operation = /op1/*
所有 http verb 之外的所有方法。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
accessControl:
defaultAction: allow
trustDomain: "public"
policies:
- appId: app1
defaultAction: allow
trustDomain: "public"
namespace: "default"
operations:
- name: /op1/*
httpVerb: ["*"]
action: deny
场景 5:允许访问 trustDomain = public
、namespace = ns1
、appId = app1
的所有方法并拒绝访问 trustDomain = public
、namespace = ns2
、appId = app1
的所有方法,此场景展示了如何指定具有相同应用 ID 但属于不同命名空间的应用。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
accessControl:
defaultAction: allow
trustDomain: "public"
policies:
- appId: app1
defaultAction: allow
trustDomain: "public"
namespace: "ns1"
- appId: app1
defaultAction: deny
trustDomain: "public"
namespace: "ns2"
场景 6:允许访问除 trustDomain = public
、namespace = default
、appId = app1
、operation = /op1/**/a
、所有 http 动词之外的所有方法。
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
accessControl:
defaultAction: allow
trustDomain: "public"
policies:
- appId: app1
defaultAction: allow
trustDomain: "public"
namespace: "default"
operations:
- name: /op1/**/a
httpVerb: ["*"]
action: deny
下面我们通过一个具体的示例来展示下访问控制策略的使用,同样还是使用 quickstarts
示例中的 hello-world
进行说明。
git clone [-b <dapr_version_tag>] https://github.com/dapr/quickstarts.git
cd quickstarts/tutorials/hello-world/node
hello world
该示例应用中包含一个 python 应用去调用一个 node.js 应用程序,访问控制列表依靠 Dapr Sentry 服务来生成带有 SPIFFE id
的 TLS 证书进行认证,这意味着 Sentry 服务必须在本地运行或部署到你的托管环境,比如 Kubernetes 集群。
下面的 nodeappconfig
例子显示了如何拒绝来自 pythonapp
的 neworder
方法的访问,其中 pythonapp
是在 myDomain
信任域和 default
命名空间中,nodeapp
在 public
公共信任域中。
# nodeappconfig.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: nodeappconfig
spec:
tracing:
samplingRate: "1"
accessControl:
defaultAction: allow
trustDomain: "public"
policies:
- appId: pythonapp
defaultAction: allow
trustDomain: "myDomain"
namespace: "default"
operations:
- name: /neworder
httpVerb: ["POST"]
action: deny
代码语言:javascript复制# pythonappconfig.yaml
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: pythonappconfig
spec:
tracing:
samplingRate: "1"
accessControl:
defaultAction: allow
trustDomain: "myDomain"
接下来我们先在本地自拓管模式下来使用启用访问策略配置,首先需要在启用 mTLS 的情况下在本地运行 Sentry 服务,我们可以直接在 https://github.com/dapr/dapr/releases 页面下载对应的 sentry
二进制文件,比如我们这里是 Mac M1,则可以使用下面的命令直接下载:
# wget https://github.91chi.fun/https://github.com/dapr/dapr/releases/download/v1.8.4/sentry_darwin_arm64.tar.gz
$ wget https://github.com/dapr/dapr/releases/download/v1.8.4/sentry_darwin_arm64.tar.gz
$ tar -xvf sentry_darwin_arm64.tar.gz
然后为 Sentry 服务创建一个目录以创建自签名根证书:
代码语言:javascript复制$ mkdir -p $HOME/.dapr/certs
使用以下命令在本地运行 Sentry 服务:
代码语言:javascript复制$ ./sentry --issuer-credentials $HOME/.dapr/certs --trust-domain cluster.local
INFO[0000] starting sentry certificate authority -- version 1.8.4 -- commit 18575823c74318c811d6cd6f57ffac76d5debe93 instance=MBP2022.local scope=dapr.sentry type=log ver=1.8.4
INFO[0000] configuration: [port]: 50001, [ca store]: default, [allowed clock skew]: 15m0s, [workload cert ttl]: 24h0m0s instance=MBP2022.local scope=dapr.sentry.config type=log ver=1.8.4
WARN[0000] loading default config. couldn't find config name: daprsystem: stat daprsystem: no such file or directory instance=MBP2022.local scope=dapr.sentry type=log ver=1.8.4
INFO[0000] starting watch on filesystem directory: /Users/cnych/.dapr/certs instance=MBP2022.local scope=dapr.sentry type=log ver=1.8.4
INFO[0000] certificate authority loaded instance=MBP2022.local scope=dapr.sentry type=log ver=1.8.4
INFO[0000] root and issuer certs not found: generating self signed CA instance=MBP2022.local scope=dapr.sentry.ca type=log ver=1.8.4
# ......
INFO[0000] sentry certificate authority is running, protecting ya'll instance=MBP2022.local scope=dapr.sentry type=log ver=1.8.4
运行成功后 Sentry 服务将在指定目录中创建根证书,可以通过如下所示的命令来配置环境变量指定相关证书路径:
代码语言:javascript复制export DAPR_TRUST_ANCHORS=`cat $HOME/.dapr/certs/ca.crt`
export DAPR_CERT_CHAIN=`cat $HOME/.dapr/certs/issuer.crt`
export DAPR_CERT_KEY=`cat $HOME/.dapr/certs/issuer.key`
export NAMESPACE=default
然后我们就可以运行 daprd
为启用了 mTLS 的 node.js 应用启动 Dapr sidecar,并引用本地的 Sentry 服务:
daprd --app-id nodeapp --dapr-grpc-port 50002 -dapr-http-port 3501 -metrics-port 9091 --log-level debug --app-port 3000 --enable-mtls --sentry-address localhost:50001 --config nodeappconfig.yaml
上面的命令我们通过 --enable-mtls
启用了 mTLS,通过 --config
指定了上面的 nodeappconfig.yaml
这个配置文件。
然后启动 node.js 应用:
代码语言:javascript复制$ cd node && yarn
$ node app.js
Node App listening on port 3000!
同样的方式在另外的终端中设置环境变量:
代码语言:javascript复制export DAPR_TRUST_ANCHORS=`cat $HOME/.dapr/certs/ca.crt`
export DAPR_CERT_CHAIN=`cat $HOME/.dapr/certs/issuer.crt`
export DAPR_CERT_KEY=`cat $HOME/.dapr/certs/issuer.key`
export NAMESPACE=default
然后运行 daprd
为启用了 mTLS 的 python 应用启动 Dapr sidecar,并引用本地的 Sentry 服务:
daprd --app-id pythonapp --dapr-grpc-port 50003 --metrics-port 9092 --log-level debug --enable-mtls --sentry-address localhost:50001 --config pythonappconfig.yaml
在重新开一个终端直接启动 Python 应用即可:
代码语言:javascript复制$ cd python && pip3 install -r requirements.txt
$ python3 app.py
HTTP 403 => {"errorCode":"ERR_DIRECT_INVOKE","message":"fail to invoke, id: nodeapp, err: rpc error: code = PermissionDenied desc = access control policy has denied access to appid: pythonapp operation: neworder verb: POST"}
HTTP 403 => {"errorCode":"ERR_DIRECT_INVOKE","message":"fail to invoke, id: nodeapp, err: rpc error: code = PermissionDenied desc = access control policy has denied access to appid: pythonapp operation: neworder verb: POST"}
# ......
由于 nodeappconfig
文件中我们配置了对 /neworder
接口的 POST 拒绝操作,所以应该会在 python 应用程序命令提示符中看到对 node.js 应用程序的调用失败,如果我们将上面的 nodeappconfig
配置中的 action: deny
修改为 action: allow
并重新运行应用程序,然后我们应该会看到此调用成功。
对于 Kubernetes 模式则更简单,只需要创建上述配置文件 nodeappconfig.yaml
和 pythonappconfig.yaml
并将其应用于 Kubernetes 集群,然后在应用的注解中添加 dapr.io/config: "pythonappconfig"
来指定配置即可开启服务访问控制。
annotations:
dapr.io/enabled: "true"
dapr.io/app-id: "pythonapp"
dapr.io/config: "pythonappconfig"
Pub/sub 主题范围访问策略
对于 Pub/sub
组件,你可以限制允许哪些主题类型和应用程序发布和订阅特定主题。
命名空间或组件范围可用于限制组件对特定应用程序的访问,这些添加到组件的应用程序范围仅限制具有特定 ID 的应用程序能够使用该组件。如下所示显示了如何将两个启用 Dapr 的应用程序(应用程序 ID 为 app1
和 app2
)授予名为 statestore
的 Redis 组件,该组件本身位于 production
命名空间中:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: statestore
namespace: production
spec:
type: state.redis
version: v1
metadata:
- name: redisHost
value: redis-master:6379
scopes:
- app1
- app2
除了这个通用组件的 scopes 范围之外,发布/订阅组件还可以限制以下内容:
- 可以使用哪些主题(发布或订阅)
- 允许哪些应用发布到特定主题
- 允许哪些应用订阅特定主题
这被称为发布/订阅主题范围。我们可以为每个发布/订阅组件定义发布/订阅范围,比如你可能有一个名为 pubsub
的 pub/sub 组件,它具有一组范围,另一个 pubsub2
具有另外不同的范围。
示例 1:主题访问范围。如果你的主题包含敏感信息并且仅允许你的应用程序的子集发布或订阅这些信息,那么限制哪些应用程序可以发布/订阅主题可能会很有用。如下以下是三个应用程序和三个主题的示例:
代码语言:javascript复制apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: publishingScopes
value: "app1=topic1;app2=topic2,topic3;app3="
- name: subscriptionScopes
value: "app2=;app3=topic1"
这里我们设置了 publishingScopes
和 subscriptionScopes
两个属性,分别用于配置发布范围和订阅范围。要拒绝应用发布到任何主题,请将主题列表留空,比如我们这里配置的 app1=topic1;app2=topic2,topic3;app3=
,其中的 app3=
就表示该应用不允许发布到任何主题上去。
根据我们的配置下表显示了允许哪些应用程序发布到主题中:
Topic1 | Topic2 | Topic3 | |
---|---|---|---|
app1 | X | ||
app2 | X | X | |
app3 |
下表显示了哪些应用程序可以订阅主题:
Topic1 | Topic2 | Topic3 | |
---|---|---|---|
app1 | X | X | X |
app2 | |||
app3 | X |
注意:如果未列出应用程序(例如,
subscriptionScopes
中的app1
),则允许它订阅所有主题。因为不使用allowedTopics
并且app1
没有任何订阅范围,所以它也可以使用上面未列出的其他主题。
示例 2:限制允许的主题。如果 Dapr 应用程序向其发送消息,则会创建一个主题,在某些情况下,应管理此主题的创建。例如:
- Dapr 应用程序中生成主题名称的错误可能导致创建无限数量的主题
- 精简主题名称和总数,防止主题无限增长
在这些情况下,可以使用 allowedTopics
属性进行配置,以下就是三个允许主题的示例:
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "topic1,topic2,topic3"
示例 3:组合 allowedTopics 和范围。有时你想结合这两个范围,因此只有一组固定的允许主题并为某些应用程序指定范围。以下是三个应用程序和两个主题的示例:
代码语言:javascript复制apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: pubsub
spec:
type: pubsub.redis
version: v1
metadata:
- name: redisHost
value: "localhost:6379"
- name: redisPassword
value: ""
- name: allowedTopics
value: "A,B"
- name: publishingScopes
value: "app1=A"
- name: subscriptionScopes
value: "app1=;app2=A"
注意这里我们没有列出第三个应用程序,如果没有在范围内指定应用程序,则允许它使用所有主题。
根据上面的配置下表显示了允许哪个应用程序发布到主题中:
A | B | C | |
---|---|---|---|
app1 | X | ||
app2 | X | X | |
app3 | X | X |
下表显示了允许哪个应用程序订阅主题:
A | B | C | |
---|---|---|---|
app1 | |||
app2 | X | ||
app3 | X | X |