Istio 和 Kubernetes ft. OPA 策略

249 阅读8分钟

企业目前面临的主要挑战之一是应用和网络安全--无论他们是拥有内部基础设施还是在云中运行其工作负载。数据必须在任何时候都受到保护,而不仅仅是当它停留在数据库中或被放置在某处存储时。确保数据在移动时得到保护不仅仅意味着加密,还意味着认证和授权。

服务网是一个透明的基础设施层,位于网络和微服务之间,因此它是确保数据加密、认证和授权的完美场所。

Istio通过使用X.509证书中的SPIFFE ID,在工作负载之间提供自动mTLS和可信身份。Kubernetes环境中的每个工作负载都以服务账户的名义运行。因此,其身份是基于工作负载的服务账户。

Istio中的身份符合SPIFFE标准,具有以下格式。

spiffe://<trust-domain>/ns/<namespace>/sa/<service-account>

因为身份是授权的基础,因为服务账户是身份的基础,所以正确设置环境,使用适当的策略很重要。在Kubernetes中,RBAC规则可以而且应该用来指定这种策略和权限设置。

首先,用户和服务账户应该被限制在一组命名空间中。当然,这只是最基本的设置,强烈建议你使用更细化的RBAC设置。另一方面,这并不能防止由于错误配置或由于恶意行为者试图以特定服务账户的名义运行工作负载而可能出现的问题。

可能出错的地方 🔗︎

让我们想象一个系统,在这个系统中,我们收集了受GDPR或类似法律限制的数据,例如,它不能离开该国或只能用于非常具体的情况或在非常具体的情况下使用。从架构开始,我们有一个数据提供者为允许的服务提供这些数据,还有一个分析消费者根据匿名的数据进行计算。

RBAC规则限制了谁可以部署到哪些Kubernetes命名空间。Istio的授权策略被设置为只有分析服务可以访问数据服务,或者更准确地说,以分析服务的服务账户运行的pod能够到达数据服务的pod。

即使这种设置看起来不错,但策略是基于服务身份的,而服务身份本身并不限制哪个工作负载可以用哪个服务账户运行,也不限制在哪里运行。我们很容易想象到,一个开发者犯了一个诚实的错误,以分析服务的名义运行一个不同的工作负载,可以接触到受限制的数据,以不应该使用的方式使用它。同样,一个攻击者可能会使用相同的载体对一个组织造成巨大的伤害。

现在,一个环境有来自不同地理位置的集群相互连接的情况也很常见。例如,大型企业的服务网络一般会在多个地区的更多集群上扩展。这就提出了能够控制和执行环境中的工作负载放置的问题,因为对数据如何以及何时离开某些区域的规定越来越多,如果有的话。

Kubernetes RBAC是一个很好的部署限制基础;Istio授权策略可以帮助限制基于身份的服务与服务之间的通信,但我们需要更好的策略管理,以确保环境的安全,使其尽可能的密闭。

OPA来拯救 🔗︎

Open Policy Agent是一个通用的策略引擎,可以用来在一系列的应用中执行策略。它可以作为一个政策中间件应用--政策可以在OPA中定义,而应用可以查询OPA以做出决定。从本质上讲,它提供政策即服务,并将政策与应用配置解耦。

策略可以用Rego(发音为 "ray-go")编写,它是专门为表达复杂的分层数据结构的策略而设计的。关于Rego的详细信息,请参见策略语言文档

通过使用准入控制器,OPA可以很容易地与Kubernetes集成。这些控制器在创建、更新和删除操作中对对象执行策略。

下面是几个例子,说明使用OPA作为验证接纳控制器时可以做什么。

  • 要求在所有的资源上有特定的标签或注释。
  • 要求容器图像只来自于受信任的来源。
  • 要求所有工作负载指定资源请求和限制。
  • 防止冲突的对象被创建。

通过部署OPA作为一个突变的接纳控制器,你可以,例如。

  • 将sidecar容器注入到pod中。
  • 在资源上设置特定的标签或注解。
  • 重写容器镜像以指向企业镜像注册表。
  • 在部署中包括节点和pod(反)亲和性选择器。

这在实践中是如何体现的? 🔗︎

OPA policiesOPA policies

假设前面描述的设置涉及到一个数据服务和一个分析服务,我们希望允许两者之间的通信。同时,我们希望限制哪些工作负载可以用分析服务账户运行。让我们看看OPA是如何帮助我们实现这些控制的。

我们将使用Banzai Cloud的基于Istio的服务网状平台Backyards(现在是Cisco Service Mesh Manager)来做这个演示。

设置 🔗︎

  1. 创建一个Kubernetes集群。

    如果你需要帮助,你可以用我们的Banzai Cloud的Pipeline平台的免费版本创建一个集群。

  2. KUBECONFIG 到你的集群。

  3. 注册免费版本,并运行以下命令来安装Backyards。

    注册Cisco Service Mesh Manager(以前叫Banzai Cloud Backyards)的免费层级版本,并按照《入门指南》获取最新的安装说明。

  4. 安装OPA

    ~ ❯ helm repo add stable https://kubernetes-charts.storage.googleapis.com/
    "stable" has been added to your repositories
    
    ~ ❯ kubectl create namespace opa
    namespace/opa created
    
    ~ ❯ helm upgrade --install opa stable/opa --namespace opa \
        --values https://raw.githubusercontent.com/banzaicloud/opa-samples/master/helm-values.yaml
    

部署分析和数据服务 🔗︎

首先部署数据服务,它以数据服务账户运行。

我们将使用与通用Backyards演示相同的应用程序,称为Allspark。

~ ❯ kubectl apply -f https://raw.githubusercontent.com/banzaicloud/opa-samples/master/data-service-deploy.yaml
namespace/data created
serviceaccount/data created
deployment.apps/data created
service/data created

配置一个Istio授权策略,以便只允许分析服务到达数据服务/api/v1/data 端点。

~  kubectl apply -f - <<EOF
apiVersion: security.istio.io/v1beta1
kind: AuthorizationPolicy
metadata:
  name: data
  namespace: data
spec:
  selector:
    matchLabels:
      app: data
  rules:
  - from:
    - source:
        principals: ["cluster.local/ns/analytics/sa/analytics"]
    to:
    - operation:
        methods: ["GET"]
        paths: ["/api/v1/data"]
EOF

现在我们来部署分析服务,它以分析服务账户运行,并检查是否可以连接到数据服务

~ ❯ kubectl apply -f https://raw.githubusercontent.com/banzaicloud/opa-samples/master/analytics-service-deploy.yaml
namespace/analytics created
serviceaccount/analytics created
deployment.apps/analytics created
service/analytics created

检查服务与服务的通信 🔗︎

部署一个测试舱,向分析服务发送一个HTTP请求。在后台,它应该触发与数据服务的通信。

~  kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: test-pod
  namespace: analytics
  labels:
    app: test-pod
spec:
  containers:
  - name: app
    image: curlimages/curl:7.72.0
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 3000; done;" ]
EOF
pod/test-pod created

~  kubectl exec -ti test-pod -c app -- curl http://analytics:8080
analytics response

通过检查日志,我们可以确定,分析服务能够到达数据服务

~ ❯ kubectl logs -l app=analytics -c service | tail -2
time="2020-09-09T21:48:27Z" level=info msg="outgoing request" correlationID=70fad725-7791-4070-b655-1f80a85730f1 server=http url="http://data.data:8080/api/v1/data"
time="2020-09-09T21:48:27Z" level=info msg="response to outgoing request" correlationID=70fad725-7791-4070-b655-1f80a85730f1 responseCode=200 server=http url="http://data.data:8080/api/v1/data"

然而,测试舱不能直接到达数据服务,因为Istio的授权策略禁止这样做。

~ ❯ kubectl exec -ti test-pod -c app -- curl http://data.data:8080/api/v1/data
RBAC: access denied

我们已经预见到了到目前为止发生的一切。现在让我们看看,如果我们试图使用另一个测试舱,但这次我们用分析服务账户启动它,会发生什么。

~  kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: test-pod-with-analytics
  namespace: analytics
  labels:
    app: test-pod
spec:
  serviceAccount: analytics
  containers:
  - name: app
    image: curlimages/curl:7.72.0
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 3000; done;" ]
EOF
pod/test-pod-with-analytics created

检查来自test-pod-with-analyticspod的通信。

~ ❯ kubectl exec -ti test-pod-with-analytics -c app -- curl http://data.data:8080/api/v1/data
data service response

正如预期的那样,这表明Istio授权策略允许分析服务账户连接到数据服务,不管使用该服务账户的实际工作负载如何。

使用OPA来限制哪些工作负载可以使用哪个服务账户 🔗︎

OPA policy 1.

为了防止这种情况,我们应该限制哪些docker镜像被允许用分析服务账户运行。这就是OPA发挥作用的地方。我们已经在设置阶段安装了OPA验证接纳webhook处理器,但我们仍然需要配置它。

~ ❯ kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
  labels:
    openpolicyagent.org/policy: rego
  name: opa-main
  namespace: opa
data:
  main: |
    package system

    import data.kubernetes.admission

    main = {
      "apiVersion": "admission.k8s.io/v1beta1",
      "kind": "AdmissionReview",
      "response": response,
    }

    default uid = ""

    uid = input.request.uid

    response = {
        "allowed": false,
        "uid": uid,
        "status": {
            "reason": reason,
        },
    } {
        reason = concat(", ", admission.deny)
        reason != ""
    }
    else = {"allowed": true, "uid": uid}
EOF

除了主要的配置,我们可以把策略添加为configmaps,并给它们贴上标签让OPA发现。在配置图被应用后,OPA策略管理器对其进行注释,使用密钥openpolicyagent.org/policy-status ,其中包含实际的策略状态。政策中的任何错误都会用这个注释报告。

下面的OPA策略描述了一个场景,其中只允许banzaicloud/allspark:0.1.2banzaicloud/istio-proxyv2:1.7.0-bzc 图像与分析服务账户一起运行。第一个是实际工作负载,第二个是Istio代理容器镜像。

~ ❯ kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
  labels:
    openpolicyagent.org/policy: rego
  name: opa-pod-allowlist
  namespace: opa
data:
  main: |
    package kubernetes.admission

    allowlist = [
        {
            "serviceAccount": "analytics",
            "images": {"banzaicloud/allspark:0.1.2", "banzaicloud/istio-proxyv2:1.7.0-bzc"},
        },
    ]

    deny[msg] {
        input.request.kind.kind == "Pod"
        input.request.operation == "CREATE"

        serviceAccount := input.request.object.spec.serviceAccountName

        # check whether the service account is restricted
        allowlist[a].serviceAccount == serviceAccount

        image := input.request.object.spec.containers[_].image

        # check whether the pod images allowed to run with the specified service account
        not imageWithServiceAccountAllowed(serviceAccount, image)

        msg := sprintf("pod with serviceAccount %q, image %q is not allowed", [serviceAccount, image])
    }

    imageWithServiceAccountAllowed(serviceAccount, image) {
        allowlist[a].serviceAccount == serviceAccount
        allowlist[a].images[image]
    }
EOF
configmap/opa-pod-allowlist created

~ ❯ kubectl -n opa get cm opa-pod-allowlist -o jsonpath='{.metadata.annotations.openpolicyagent\.org/policy-status}'
{"status":"ok"}

应用OPA策略后,我们应该尝试用分析服务账户重新创建测试pod,并观察它的惨痛失败。

~ ❯ kubectl delete pods test-pod-analytics --grace-period=0
pod "test-pod-analytics" deleted

~ ❯ kubectl create -f - <<EOF
apiVersion: v1
kind: Pod
metadata:
  name: test-pod-with-analytics
  namespace: analytics
  labels:
    app: test-pod
spec:
  serviceAccount: analytics
  containers:
  - name: app
    image: banzaicloud/allspark:0.1.2
    command: [ "/bin/sh", "-c", "--" ]
    args: [ "while true; do sleep 3000; done;" ]
EOF
Error from server (pod with serviceAccount "analytics", image "curlimages/curl:7.72.0" is not allowed): error when creating "STDIN": admission webhook "webhook.openpolicyagent.org" denied the request: pod with serviceAccount "analytics", image "curlimages/curl:7.72.0" is not allowed

使用OPA来限制具有特定服务账户的工作负载可以运行的地方 🔗︎

OPA policy 2.

公司政策也可能要求一些服务只在特定地区运行,或者要求使用特定的硬件(例如,用于加密或加密歌唱)。随着多集群、多区域服务网络的新兴利用,一个环境在多个区域拥有可用资源的情况越来越普遍,因此应该制定政策,防止工作负载的错位。

为了证明这种情况可能发生,我们可以通过添加另一个地区的Kubernetes集群,轻松地扩展现有的Backyards服务网,只需要新集群的kubeconfig。

OPA验证接纳控制器也必须安装在新集群上。要做到这一点,在新集群上重复设置部分的第4步,并应用opa-main configmap。

~ ❯ backyards istio cluster attach ~/Download/waynz0r-0910-01.yaml
✓ creating service account and rbac permissions
...
✓ attaching cluster started successfully name=waynz0r-0910-01

~ ❯ backyards istio cluster status
Name     Type  Status     Gateway Address              Istio Control Plane  Message
Clusters in the mesh

Name             Type  Status     Gateway Address              Istio Control Plane   Message
waynz0r-0908-01  Host  Available  [18.195.59.67 52.57.74.102]  -
waynz0r-0910-01  Peer  Available  [35.204.169.33]              cp-v17x.istio-system

The `waynz0r-0908-01` cluster is running on AWS in the `eu-central-1` region, the `waynz0r-0910-01` cluster is a GKE cluster in the `europe-west4` region.

The following OPA policy was extended to support restrictions based on the `nodeSelector` property of the pods in question. Let's apply it to both clusters!

```bash
~ ❯ kubectl apply -f - <<EOF
kind: ConfigMap
apiVersion: v1
metadata:
  labels:
    openpolicyagent.org/policy: rego
  name: opa-pod-allowlist
  namespace: opa
data:
  main: |
    package kubernetes.admission

    allowlist = [
        {
            "serviceAccount": "analytics",
            "images": {"banzaicloud/allspark:0.1.2", "banzaicloud/istio-proxyv2:1.7.0-bzc"},
            "nodeSelector": [{"failure-domain.beta.kubernetes.io/region": "eu-central-1"}],
        },
    ]

    deny[msg] {
        input.request.kind.kind == "Pod"
        input.request.operation == "CREATE"

        serviceAccount := input.request.object.spec.serviceAccountName

        # check whether the service account is restricted
        allowlist[a].serviceAccount == serviceAccount

        image := input.request.object.spec.containers[_].image

        # check whether the pod images allowed to run with the specified service account
        not imageWithServiceAccountAllowed(serviceAccount, image)

        msg := sprintf("pod with serviceAccount %q, image %q is not allowed", [serviceAccount, image])
    }

    imageWithServiceAccountAllowed(serviceAccount, image) {
        allowlist[a].serviceAccount == serviceAccount
        allowlist[a].images[image]
    }

    deny[msg] {
        input.request.kind.kind == "Pod"
        input.request.operation == "CREATE"

        serviceAccount := input.request.object.spec.serviceAccountName

        # check whether the service account is restricted
        allowlist[a].serviceAccount == serviceAccount

        # check whether pod location is restricted
        count(allowlist[a].nodeSelector[ns]) > 0

        image := input.request.object.spec.containers[_].image
        nodeSelector := object.get(input.request.object.spec, "nodeSelector", [])

        # check whether pod location is allowed
        not podAtLocationAllowed(serviceAccount, nodeSelector)

        msg := sprintf("pod with serviceAccount %q, image %q is not allowed at the specified location", [serviceAccount, image])
    }

    podAtLocationAllowed(serviceAccount, nodeSelector) {
        allowlist[a].serviceAccount == serviceAccount

        # requires that at least one nodeSelector combination matches this image and serviceAccount combination
        selcount := count(allowlist[a].nodeSelector[ns])
        count({k | allowlist[a].nodeSelector[s][k] == nodeSelector[k]}) == selcount
    }
EOF
configmap/opa-pod-allowlist created

现在,让我们看看当我们试图将分析服务部署到对等集群上时会发生什么。

~ ❯ kubectl apply -f https://raw.githubusercontent.com/banzaicloud/opa-samples/master/analytics-service-deploy.yaml
namespace/analytics created
serviceaccount/analytics created
deployment.apps/analytics created
service/analytics created

乍看起来很好,但吊舱实际上还没有被创建,因为OPA策略阻止了这种情况的发生。

~ ❯ kubectl get event
LAST SEEN   TYPE      REASON              OBJECT                            MESSAGE
2m24s       Warning   FailedCreate        replicaset/analytics-6cb4bfc97f   Error creating: admission webhook "webhook.openpolicyagent.org" denied the request: pod with serviceAccount "analytics", image "banzaicloud/allspark:0.1.2" is not allowed at the specified location, pod with serviceAccount "analytics", image "banzaicloud/istio-proxyv2:1.7.0-bzc" is not allowed at the specified location

必须指出的是,保护这些OPA策略很重要,所以需要应用适当的Kubernetes RBAC规则,以防止对opa 命名空间和验证webhook配置资源的不需要的访问

启示 🔗︎

Kubernetes提供了在集群层面上执行策略所需的基本组件。内置的Kubernetes RBAC规则为确保环境安全提供了一个良好的基础。当你有一个服务网状结构时,授权策略是实现服务与服务通信安全策略的自然下一步。然而,拥有一个通用的策略引擎会引入一系列其他的选项和可能性。即使OPA策略语言有一个陡峭的学习曲线,正如这篇博文中所展示的那样,它也因此提供了大量的力量和灵活性。