Kubernetes 的多租戶環境下,資源隔離和安全策略至關重要。OPA/Gatekeeper 提供了強大的策略引擎,能有效管理和實施這些策略。本文實踐了根據節點汙點和親和性的租戶隔離策略,並使用 Mutating Webhook 自動修改 Pod 組態,確保 Pod 佈署到指定的節點。同時,Validating Webhook 則用於驗證組態的正確性,防止不合規的 Pod 資源佈署。此外,文章還探討了 Rego 策略語言的應用,如何編寫 Rego 策略來定義和驗證各種約束條件。最後,文章也詳細介紹了 Gatekeeper 的稽核模式,用於檢查現有資源的合規性,以及外部資料提供者的組態和使用,讓策略引擎能整合外部資料來源,實作更複雜的策略控制。

Kubernetes 中的 OPA/Gatekeeper 策略管理與實踐

在 Kubernetes 環境中,OPA(Open Policy Agent)與 Gatekeeper 的結合為叢集管理提供了強大的策略控制能力。本文將探討如何在 Kubernetes 中使用 OPA/Gatekeeper 進行策略管理,特別是在多租戶隔離的場景下。

節點汙點(Taints)與節點親和性(Node Affinity)

首先,我們需要了解節點汙點的概念。節點汙點是一種用於標記節點的機制,可以確保只有具備相應容忍度(Toleration)的 Pod 才能被排程到這些節點上。在我們的範例中,我們首先檢查了某些節點的汙點組態:

$ kubectl get nodes ip-10-0-10-77.us-east-2.compute.internal -ojsonpath='{.spec.taints}'
[{"effect":"NoSchedule","key":"tenant","value":"tenant1"}]

這些節點被標記為具有 tenant=tenant1 的汙點,這意味著只有具備對應容忍度的 Pod 才能被排程到這些節點上。

使用 Mutating Webhook 修改 Pod 組態

為了確保特定名稱空間中的 Pod 被排程到正確的節點上,我們使用了 Mutating Webhook 來自動修改 Pod 的組態。具體來說,我們新增了兩個 Mutating 組態:

  1. 為特定名稱空間中的 Pod 新增節點親和性組態
  2. 為這些 Pod 新增對應的容忍度
# Adds a node affinity to all Pods in a specific Namespace
...spec:
  applyTo:
  - groups: [""]
    kinds: ["Pod"]
    versions: ["v1"]
  match:
    namespaces: ["tenant1"]
  location: >
    "spec.affinity.nodeAffinity."
    "requiredDuringSchedulingIgnoredDuringExecution.nodeSelectorTerms"
  parameters:
    assign:
      value:
      - matchExpressions:
        - key: "tenant"
          operator: In
          values:
          - "tenant1"

內容解密:

此段 YAML 組態定義了一個 Mutating Webhook,用於為 tenant1 名稱空間中的所有 Pod 新增節點親和性組態。具體來說,它設定了 requiredDuringSchedulingIgnoredDuringExecution 規則,確保 Pod 只能被排程到標有 tenant=tenant1 的節點上。

使用 Validating Webhook 驗證 Pod 組態

除了修改 Pod 組態外,我們還使用了 Validating Webhook 來驗證這些修改是否正確應用。我們定義了兩個驗證策略:

  1. 驗證節點親和性是否正確設定
  2. 驗證容忍度是否正確新增
# ConstraintTemplate Rego to validate node affinity
package k8srequirednodeaffinity
import data.lib.k8s.helpers as helpers

violation[{"msg": msg, "details": {"missing":missing}}] {
    helpers.review_operation == input.parameters.ops[_]
    provided := {x | x := input.review.object.spec.affinity.nodeAffinity}
    required := {x | x := input.parameters.nodeAffinity}
    missing := required - provided
    count(missing) > 0
    msg := sprintf("%v: Resource missing correct node affinity. Provided node affinity: %v, Required node affinity: %v. Resource ID (ns/name/kind): %v",
                   [input.parameters.errMsg,provided,required,helpers.review_id])
}

內容解密:

此 Rego 策略用於驗證 Pod 是否具備正確的節點親和性組態。它比較了實際提供的組態與所需的組態,並在發現不符時產生違規訊息。

測試與驗證

為了驗證我們的組態是否正確,我們進行了正向測試和負向測試:

  1. tenant1 名稱空間中建立 Pod,並驗證其是否具備正確的容忍度和節點親和性組態。
  2. default 名稱空間中建立 Pod,並驗證其是否未被應用不正確的組態。
  3. default 名稱空間中建立帶有特定容忍度的 Pod,並驗證是否被正確拒絕。
$ kubectl -n default apply -f test/70-test-pod.yaml
Error from server (Forbidden): error when creating "test/70-test-pod.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [toleration-ns-check-pod] default namespace cannot use tolerations: [{"effect": "NoSchedule", "key": "tenant", "operator": "Equal", "value": "tenant1"}]

隨著 Kubernetes 環境的日益複雜,OPA/Gatekeeper 的應用將變得越來越重要。未來,我們可以期待看到更多根據 OPA/Gatekeeper 的創新策略管理方案,例如:

  • 更細粒度的策略控制
  • 自動化的策略驗證和修正
  • 跨叢集的統一策略管理

這些發展將進一步鞏固 OPA/Gatekeeper 在 Kubernetes 生態系統中的重要地位。

Kubernetes 中的 OPA/Gatekeeper 工作流程

  graph LR
    A[Kubernetes API Server] -->|請求|> B[OPA/Gatekeeper Admission Controller]
    B -->|驗證/修改請求|> C{策略檢查}
    C -->|透過|> D[允許請求]
    C -->|失敗|> E[拒絕請求]
    D --> F[資源建立/更新]
    E --> G[傳回錯誤訊息]

圖表翻譯: 此圖表展示了 Kubernetes 中的 OPA/Gatekeeper 工作流程。當使用者透過 Kubernetes API Server 發起請求時,OPA/Gatekeeper 的 Admission Controller 會攔截並驗證/修改該請求。根據策略檢查的結果,請求要麼被允許並繼續執行,要麼被拒絕並傳回錯誤訊息給使用者。

圖表內容解密:

  1. Kubernetes API Server接收使用者請求。
  2. OPA/Gatekeeper Admission Controller攔截請求。
  3. 策略檢查決定請求是否合規。
  4. 合規的請求被允許並繼續執行。
  5. 不合規的請求被拒絕並傳回錯誤訊息。

這個流程確保了 Kubernetes 叢集中的資源操作符合預定的策略要求,從而提高了叢集的安全性和可控性。

Gatekeeper 稽核模式與外部資料提供者

稽核模式

在 Kubernetes 中實施 Policy-as-Code (PaC) 時,需要考慮某些資源可能不會在 webhook 超時期間被評估,或者在政策實施後才被建立。Gatekeeper 的稽核功能旨在評估現有資源組態是否符合約束條件。

稽核功能組態

要啟用稽核功能,需要組態 Gatekeeper 的設定。以下是一個範例組態,其中非預設設定以粗體顯示:

# Gatekeeper 設定(片段)
- args:
  - --audit-interval=60
  - --log-level=INFO
  - --constraint-violations-limit=20
  - --audit-from-cache=false
  - --audit-chunk-size=500
  - --audit-match-kind-only=false
  - --emit-audit-events=true
  - --operation=audit
  - --operation=status
  - --operation=mutation-status
  - --logtostderr
  - --health-addr=:9090
  - --prometheus-port=8888
  - --enable-external-data=false
  - --enable-generator-resource-expansion=false
  - **--metrics-backend=prometheus**
  - --disable-cert-rotation=true

稽核流程

當稽核控制器啟動時,如果沒有組態任何約束條件,則會每隔 60 秒輸出以下日誌記錄:

// Gatekeeper 日誌記錄(無約束條件時)
{"level":"info","ts":1672433592.7277467,"logger":"controller","msg":"no constraint is found with apiversion","process":"audit","audit_id":"2022-12-30T20:53:12Z","constraint apiversion":"constraints.gatekeeper.sh/v1beta1"}
{"level":"info","ts":1672433592.7278154,"logger":"controller","msg":"auditing is complete","process":"audit","audit_id":"2022-12-30T20:53:12Z","event_type":"audit_finished"}

一旦增加了 Gatekeeper 政策(ConstraintTemplate 和 Constraint),稽核流程就會使用該政策來檢測違規情況,如以下 JSON 日誌記錄所示:

// 稽核政策日誌記錄
{"level":"info","ts":1672434373.4897652,"logger":"controller","msg":"constraint status update","process":"audit","audit_id":"2022-12-30T21:06:12Z","object":{"apiVersion":"constraints.gatekeeper.sh/v1beta1","kind":"K8sPSPSeccomp","name":"psp-seccomp"}}
{"level":"info","ts":1672434373.4930406,"logger":"controller","msg":"handling constraint update","process":"constraint_controller","instance":{"apiVersion":"constraints.gatekeeper.sh/v1beta1","kind":"K8sPSPSeccomp","name":"psp-seccomp"}}
{"level":"info","ts":1672434373.4934077,"logger":"controller","msg":"updated constraint status violations","process":"audit","audit_id":"2022-12-30T21:06:12Z","constraintName":"psp-seccomp","count":3}

稽核結果

稽核結果可以透過以下命令檢視:

# 取得違規狀態
$ kubectl get k8spspseccomp -ojsonpath='{.items[].status}'
..."totalViolations":3,"violations":[{"enforcementAction":"deny","group":"","kind":"Pod","message":"Seccomp profile 'not configured' is not allowed for container 'coredns'. Found at: no explicit profile found. Allowed profiles: {\"RuntimeDefault\",\"docker/default\"}...

稽核限制

需要注意的是,並非所有約束條件都可以使用稽核流程。某些欄位(如 review.userInforeview.operationreview.uid)無法被稽核流程填入,因此不應依賴這些欄位來檢測稽核違規。

外部資料提供者

Gatekeeper 的外部資料提供者功能允許編寫 Rego 程式碼,以呼叫 Gatekeeper 外部的服務來檢索資料以進行政策決策。這在政策所需的資料不是請求的 AdmissionReview 物件或現有的 Kubernetes 物件的一部分時非常有用。

外部資料提供者組態

外部資料提供者可以透過以下 YAML 組態:

# 外部資料提供者組態
apiVersion: externaldata.gatekeeper.sh/v1beta1
kind: Provider
metadata:
  labels:
    app: answers
    billing: lob-cc
    env: dev
    owner: jimmy
  name: answers-provider
spec:
  caBundle: LS0t...
  timeout: 1
  url: https://answers.answers.svc/provide

使用外部資料提供者

透過使用外部資料提供者,Gatekeeper 可以擴充套件其政策評估能力,以包含外部資料來源。這使得 Gatekeeper 可以根據更廣泛的資料進行政策決策。

圖表翻譯:Gatekeeper 稽核流程圖示
  graph LR;
    A[稽核控制器啟動] --> B[檢查約束條件];
    B -->|無約束條件|> C[輸出日誌記錄];
    B -->|有約束條件|> D[評估資源組態];
    D --> E[檢測違規情況];
    E --> F[輸出稽核結果];

圖表翻譯: 此圖示呈現了 Gatekeeper 的稽核流程。首先,稽核控制器啟動並檢查是否存在約束條件。如果沒有約束條件,則輸出日誌記錄。如果存在約束條件,則評估資源組態並檢測違規情況,最終輸出稽核結果。

隨著 Kubernetes 和 Gatekeeper 的不斷發展,未來可能會出現更多強大的政策管理功能。這些功能將進一步增強 Gatekeeper 的能力和靈活性,使其成為 Kubernetes 環境中不可或缺的工具。

外部資料提供者組態與實作詳解

外部資料提供者組態概述

外部資料提供者(External Data Provider)是用於與外部資料來源服務進行互動的組態。主要透過 spec.caBundlespec.url 進行設定:

  • spec.caBundle:用於與外部資料來源服務進行 TLS 通訊的憑證授權單位(CA)憑證。
  • spec.url:指向外部資料來源服務的 URL。

在給定的組態範例中,answers-provider 指向位於 answers 名稱空間中的 Answers 服務。Gatekeeper 使用 caBundle 欄位中的憑證與該服務進行安全通訊,該服務使用自簽名的 TLS 金鑰和憑證。

TLS 組態與指令碼實作

為了組態 TLS 連線,下面的 shell 指令碼展示瞭如何建立所需的憑證和金鑰,並將其應用於 Kubernetes 環境中。

TLS 組態指令碼

#!/usr/bin/env bash
# 錯誤處理
set -e
trap 'catch $? $LINENO' ERR
catch() {
  if [ "$1" != "0" ]; then
    echo "Error $1 occurred on $2"
  fi
}

OWNER="jimmy"
ENV="dev"
BILLING="lob-cc"
KUBECTL="kubectl"
CA_BUNDLE=""
CONFIGS_DIRECTORY="generated/configs"
SECRETS_DIRECTORY="generated/secrets"
TEMPLATES_DIRECTORY="templates"

# 建立必要的目錄並清理舊檔案
if [ ! -d "$TEMPLATES_DIRECTORY" ]; then
  echo "$TEMPLATES_DIRECTORY not found, install aborted"
  exit 99
fi
mkdir -p $CONFIGS_DIRECTORY
mkdir -p $SECRETS_DIRECTORY
rm -f $CONFIGS_DIRECTORY/*
rm -f $SECRETS_DIRECTORY/*

# 生成 CA 憑證和金鑰
openssl genrsa -out $SECRETS_DIRECTORY/answers-ca.key 2048
openssl req -x509 -new -nodes -sha256 -key $SECRETS_DIRECTORY/answers-ca.key \
  -days 365 -out $SECRETS_DIRECTORY/answers-ca.crt -subj /CN=admission_ca 2>&1

# 生成伺服器憑證和金鑰
openssl genrsa -out $SECRETS_DIRECTORY/answers-server.key 2048
openssl req -new -key $SECRETS_DIRECTORY/answers-server.key -sha256 -out \
  $SECRETS_DIRECTORY/answers-server.csr -subj /CN=answers.answers.svc -config \
  $TEMPLATES_DIRECTORY/server.conf 2>&1
openssl x509 -req -in $SECRETS_DIRECTORY/answers-server.csr -sha256 -CA \
  $SECRETS_DIRECTORY/answers-ca.crt -CAkey $SECRETS_DIRECTORY/answers-ca.key \
  -CAcreateserial -out $SECRETS_DIRECTORY/answers-server.crt -days 100000 \
  -extensions v3_ext -extfile $TEMPLATES_DIRECTORY/server.conf

# 建立並應用 Kubernetes 資源組態
cat $TEMPLATES_DIRECTORY/ns-template.yaml | sed -e \
  "s/__OWNER_VALUE__/${OWNER}/g" | sed -e "s/__ENV_VALUE__/${ENV}/g" | \
  sed -e "s/__BILLING_VALUE__/${BILLING}/g" > "$CONFIGS_DIRECTORY/ns.yaml"
${KUBECTL} apply -f $CONFIGS_DIRECTORY/ns.yaml

# 建立 TLS secret
${KUBECTL} -n answers delete secret answers-server --ignore-not-found 2>&1
${KUBECTL} -n answers create secret tls answers-server \
  --cert=$SECRETS_DIRECTORY/answers-server.crt \
  --key=$SECRETS_DIRECTORY/answers-server.key

# 建立 Answers 應用組態並應用
cat $TEMPLATES_DIRECTORY/answers-app-template.yaml | sed -e \
  "s/__OWNER_VALUE__/${OWNER}/g" | sed -e "s/__ENV_VALUE__/${ENV}/g" | \
  sed -e "s/__BILLING_VALUE__/${BILLING}/g" > \
  "$CONFIGS_DIRECTORY/answers-app.yaml"
${KUBECTL} apply -f $CONFIGS_DIRECTORY/answers-app.yaml

# 建立外部資料提供者組態並應用
CA_BUNDLE="$(base64 -i $SECRETS_DIRECTORY/answers-ca.crt)"
cat $TEMPLATES_DIRECTORY/provider-template.yaml | sed -e \
  "s/__OWNER_VALUE__/${OWNER}/g" | sed -e "s/__ENV_VALUE__/${ENV}/g" | \
  sed -e "s/__BILLING_VALUE__/${BILLING}/g" | sed -e \
  "s/__CA_BUNDLE_VALUE__/${CA_BUNDLE}/g" > \
  "$CONFIGS_DIRECTORY/answers-provider.yaml"
${KUBECTL} apply -f $CONFIGS_DIRECTORY/answers-provider.yaml

#### 內容解密:

此指令碼首先進行錯誤處理設定,接著定義相關變數。然後,它會檢查範本目錄是否存在,並建立必要的組態和金鑰目錄。指令碼接著生成 CA 和伺服器的憑證與金鑰,並建立 Kubernetes 的名稱空間、TLS secret 和 Answers 應用。最後,它建立並應用外部資料提供者的組態。

外部資料提供者元件架構

下圖展示了 Gatekeeper 外部資料提供者解決方案的元件架構:

  graph LR;
    A[Kubernetes API Server] -->|Request|> B[Gatekeeper];
    B -->|Forward Request|> C[Answers External Data Provider];
    C -->|Response|> B;
    B -->|Decision|> A;
    A -->|Response|> D[Kubernetes Client];

圖表翻譯: 此圖展示了 Gatekeeper 如何接收來自 Kubernetes API Server 的請求,並將其轉發給 Answers 外部資料提供者。Answers 提供者回應後,Gatekeeper 根據回應做出決策,並將結果傳回給 API Server,最終傳回給 Kubernetes 使用者端。

外部資料提供者請求與回應範例

請求負載範例如下:

{
  "apiVersion": "externaldata.gatekeeper.sh/v1beta1",
  "kind": "ProviderRequest",
  "request": {
    "keys": [
      "gcr.io/google-containers/pause:3.2",
      "gcr.io/google-containers/pause:3.3"
    ]
  }
}

使用 curl 命令傳送請求:

$ curl -vX POST http://localhost:8080/provide \
  -d @request.json \
  --header "Content-Type: application/json"

回應負載範例如下:

{
  "apiVersion": "externaldata.gatekeeper.sh/v1beta1",
  "kind": "ProviderResponse",
  "response": {
    "idempotent": true,
    "items": [
      {
        "key": "gcr.io/google-containers/pause:3.2",
        "value": "Outlook not so good.:N",
        "error": "Outlook not so good.:N:DENIED"
      },
      {
        "key": "gcr.io/google-containers/pause:3.3",
        "value": "Outlook not so good.:N",
        "error": "Outlook not so good.:N:DENIED"
      }
    ]
  }
}

#### 內容解密:

此範例展示瞭如何使用 JSON 格式的請求負載向外部資料提供者傳送請求,並接收 JSON 格式的回應。請求中包含了需要查詢的映像檔名稱,回應中則包含了對應的檢查結果。

ConstraintTemplate 組態範例

以下是一個參考 Gatekeeper 專案的 ConstraintTemplate 範例,該範例參照了 answers-provider 外部資料提供者:

apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: k8sanswerverification
  annotations:
    description: >-
      Calls external data provider answers app.
spec:
  crd:
    spec:
      names:
        kind: K8sAnswerVerification
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package k8sanswerverification
        
        violation[{"msg": msg}] {
          # 建立包含映像檔的鍵列表
          images := [img | img = input.review.object.spec.containers[_].image]
          # 傳送外部資料請求
          response := external_data({"provider": "answers-provider", "keys": images})
          response_with_error(response)
          msg := sprintf("invalid response: %v", [response])
        }
        
        response_with_error(response) {
          count(response.errors) > 0
          errs := response.errors[_]
          contains(errs[1],"DENIED")
        }

#### 內容解密:

此 ConstraintTemplate 名為 k8sanswerverification,用於呼叫 answers-provider 外部資料提供者。它定義了一個違規規則,當外部資料提供者回應中包含錯誤或被拒絕時,會觸發違規並顯示相應的錯誤訊息。Rego 程式碼邏輯首先從輸入中提取容器映像檔名稱,然後傳送外部資料請求,最後檢查回應中是否包含錯誤或拒絕訊息。