[CKS] Minimize Microservice Vulnerabilities (4) - OpenPolicy Agent(OPA)

OPA는 중앙 집중식 정책 결정 엔진이며, Kubernetes에서 OPA Gatekeeper로 Admission Controller로 동작하여 레이블 강제, 이미지 정책, 권한 제한 등 클러스터 리소스 생성 요청을 관리합니다.

[CKS] Minimize Microservice Vulnerabilities (4) - OpenPolicy Agent(OPA)
Photo by Jon Tyson / Unsplash

개요

CKS 자격증을 위해 아래 내용을 정리했습니다.

Open Policy Agent(OPA)

OPA는 중앙 집중식 정책 결정 지점입니다.

모든 서비스가 API를 통해 엑세스 권한을 확인하기 위해 정책을 조회할 수 있습니다.

OPA Server deploy

OPA 바이너리를 다운로드하고 실행 권한을 부여합니다. -s 플래그로 서버를 시작하고 8181 포트에서 대기합니다. 내장된 인증이나 권한 부여 기능이 없는 오픈 API를 제공합니다

curl -L -o opa https://github.com/open-policy-agent/opa/releases/download/v0.11.0/opa_linux_amd64
chmod 755 ./opa
./opa run -s
{"addrs":["8181"],"insecure_addr":"","level":"info","msg":"First line of log stream.","time":"2021-03-18T20:25:38+08:00"}

OPA의 API는 기본적으로 오픈 소스입니다. 운영 환경은 적절한 네트워크 보안 조치를 구현하는 것이 좋습니다.

Rego를 사용하여 권한 부여 정책 정의

OPA 정책은 Rego에서 작성되며 확장자가 .rego인 파일에 저장됩니다.

다음은 user가 john일 때 /home을 허용하는 정책 예시입니다.

package httpapi.authz


# HTTP API request
import input


default allow = false


allow {
    input.path == "home"
    input.user == "john"
}

PUT 으로 정책을 로드합니다.

curl -X PUT --data-binary @./sample.rego http://localhost:8181/v1/policies/example1
 
 curl http://localhost:8181/v1/policies

OPA를 Python Application과 통합하기

  • 엔드포인트 규칙은 /v1/data/{패키지경로}/{룰명} 으로 정의됩니다.
package httpapi.authz

allow {
    ...
}
@app.route('/home')
def hello_world():
    user = request.args.get("user")
    input_dict = {
        "input": {
            "user": user,
            "path": "home"
        }
    }
    rsp = requests.post("http://127.0.0.1:8181/v1/data/httpapi/authz", json=input_dict)
    if not rsp.json()["result"]["allow"]:
        return 'Unauthorized!', 401
    return 'Welcome Home!', 200
  • /v1/data/httpapi/authz → 패키지 전체 결과
  • /v1/data/httpapi/authz/allowallow 룰만 조회

Rego in the Playground

아래 링크에서 정책 작성과 테스트를 수행할 수 있습니다.

OPA 정책 테스트

테스트를 실행할 수 있는 내장 테스트 프레임워크가 포함되어있습니다.

package authz


test_post_allowed {
    allow with input as {"path": ["users"], "method": "POST"}
}


test_get_anonymous_denied {
    not allow with input as {"path": ["users"], "method": "GET"}
}


test_get_user_allowed {
    allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "bob"}
}


test_get_another_user_denied {
    not allow with input as {"path": ["users", "bob"], "method": "GET", "user_id": "alice"}
}
# 테스트
opa test -v

data.authz.test_post_allowed: PASS (1.417µs)
data.authz.test_get_anonymous_denied: PASS (426ns)
data.authz.test_get_user_allowed: PASS (367ns)
data.authz.test_get_another_user_denied: PASS (320ns)
-----------------------------------------------------------
PASS: 4/4

Kubernetes의 OPA

Kubernetes에서는 OPA Gatekeeper가 존재합니다.

Kubernetes Admission Control에 특화되어 다양한 ConstraintTemplate + Constraint CRD 정책 방식을 제공합니다.

kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.14.0/deploy/gatekeeper.yaml

kubectl get all -n gatekeeper-system

설치와 배포도 간단합니다.

NAME                                           READY   STATUS      RESTARTS   AGE
pod/gatekeeper-audit-6699999786d-6n8xt           1/1     Running     1          (12s ago)   31s
pod/gatekeeper-controller-manager-854f95df4f-dbhp7   1/1  Running     0          31s
pod/gatekeeper-controller-manager-854f95df4f-k96kj   1/1  Running     0          31s
pod/gatekeeper-controller-manager-854f95df4f-zfnbw   1/1  Running     0          31s


NAME                                          TYPE            CLUSTER-IP       EXTERNAL-IP    PORT(S)        AGE
service/gatekeeper-webhook-service            ClusterIP       172.20.60.127   <none>         443/TCP        31s


NAME                                          READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/gatekeeper-audit             1/1     1            1           31s
deployment.apps/gatekeeper-controller-manager  3/3     3            3           31s


NAME                                          DESIRED   CURRENT   READY   AGE
replicaset.apps/gatekeeper-audit-6699999786   1         1         1       31s
replicaset.apps/gatekeeper-controller-manager-854f95df4f   3         3         3       31s

OPA Constraint Framework

OPA Constraint Framework를 통해 필수 조건을 지정하는 정책을 적절할 위치에서 검사하게 정의할 수 있습니다. 모든 객체에 "billing" 레이블을 포함하도록 하려면 프레임워크가 Kubernetes 어드미션 컨트롤러를 통해 이 규칙을 적용합니다.

Rego를 통한 Label 유효성 검사

  • provided: 들어오는 Pod 객체에서 실제로 가지고 있는 레이블들
  • required: 반드시 있어야 하는 필수 레이블들 (예: "billing")
  • missing: 집합연산
    • required가 ["billing"] provided ["app", "version"] → missing은 ["billing"]
      • 필수 값 누락
    • required가 ["billing"], provided ["billing", "app"]→ missing은 []
      • 필수 값 충족
package systemrequiredlabels


import data.lib.helpers


violation["msg": msg, "details": {"missing_labels": missing}} {
    provided := {label | input.request.object.metadata.labels[label]}
    required := {label | label == ["billing"]}
    missing = required - provided
    count(missing) > 0
    msg = sprintf("you must provide labels: %v", [missing])
}
package systemrequiredlabels


import data.lib.helpers


violation["msg"] = msg {
    details := {"missing_labels": missing}
    provided := {label | input.request.object.metadata.labels[label]}
    required := {label | label := ["billing"]}
    missing = required - provided
    count(missing) > 0
    msg = sprintf("you must provide labels: %v", [missing])
}
package systemrequiredlabels


import data.lib.helpers


violation["msg": msg, "details": {"missing_labels": missing}} {
    provided := {label | input.request.object.metadata.labels[label]}
    required := {label | label = ["billing"]}
    missing := required - provided
    count(missing) > 0
    msg = sprintf("you must provide labels: %v", [missing])
}

Constraint Template > 파라미터를 통한 확장

Namespace에 따라 다른 레이블을 적용하는 방식 등 동적인 시나리오를 위해 Constraint Template을 사용합니다. 레이블 하드코딩 대신 필요한 매개변수로 전달 가능합니다.

  • Rego(템플릿)하나로 여러 Constraint를 찍어낼 수 있습니다.
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
  name: systemrequiredlabels
spec:
  crd:
    spec:
      names:
        kind: SystemRequiredLabel # Constraint에서 쓸 Kind
      validation:
        # Schema for the 'parameters' field goes here
  targets:
    - target: admission.k8s.gatekeeper.sh
      rego: |
        package systemrequiredlabels


        import data.lib.helpers


        violation[{"msg": msg, "details": {"missing_labels": missing}}] {
          provided := {label | input.request.object.metadata.labels[label]}
          # Use the parameter passed in the constraint instead of hardcoding
          required := {label | label == input.parameters.labels[_]}
          missing = required - provided
          count(missing) > 0
          msg = sprintf("you must provide labels: %v", [missing])
        }

spec.crd.spec.names.kind 에서 정의한 Constraint를 재사용합니다.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: SystemRequiredLabel  # 기존 템플릿에서 정의된 Kind 재사용
metadata:
  name: require-budget-label
spec:
  match:
    namespaces: ["finance"]   # 새로운 네임스페이스
  parameters:
    labels: ["budget"]        # 새로운 레이블

특정 네임스페이스의 파드가 파라미터를 가지고 있어야 함.

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: require-tech-label
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces: ["engineering"]
  parameters:
    labels: ["tech"]

deployment 제한

apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sReplicaLimits
metadata:
  name: replica-limits
spec:
  match:
    kinds:
      - kinds: ["Deployment"]
  parameters:
    ranges:
      - min_replicas: 2
        max_replicas: 5

ConfigMap을 통한 생성

kubectl create configmap untrusted-registry --from-file=/root/untrusted-registry.rego -n opa

정리

  • API Server가 요청을 받음
  • Admission Controller(Gatekeeper)가 정책 검사
    • Gatekeeper 설치 시 자동으로 Validating Admission Webhook 으로 등록됨
  • 정책 위반 → 요청 거부 (Pod 자체가 생성 안 됨)
  • 정책 통과 → 요청 승인 → Pod 생성

OPA 자체는 범용 엔진으로 kubernetes 외에서도 동작하지만 k8s에서는 Admission controller로 동작

주요 사용 사례

  • 레이블/어노테이션 강제: 비용 추적용 billing, 팀 식별용 owner 등 필수 메타데이터 요구
  • 이미지 정책: 특정 레지스트리(예: 회사 내부 레지스트리)에서만 이미지 pull 허용, latest 태그 금지
    • input.request.object.spec.containers[_].securityContext.privileged
    • input.request.object.spec.containers[_].image
  • 권한 제한: 컨테이너가 root로 실행되는 것 금지, privileged 모드 금지
  • 리소스 제한: 모든 Pod에 CPU/메모리 limits 필수 지정
    • input.request.object.spec.containers[_].resources.limits.memory
  • 네임스페이스 규칙: 특정 네임스페이스에만 특정 리소스 생성 허용

결국 OPA Gatekeeper는 외부 사용자(end-user)의 HTTP 요청을 처리하는 게 아니라, 클러스터 내부에서 리소스 생성/수정 요청을 관리하는 목적으로 사용합니다.

  • 강의 정리) 정의된 정책을 위반하는 객체 생성은 승인 단계에서 오류를 발생시켜 규정을 준수하지 않는 객체가 클러스터에 승인되는 것을 방지합니다.