[CKS] Minimize Microservice Vulnerabilities (4) - OpenPolicy Agent(OPA)
OPA는 중앙 집중식 정책 결정 엔진이며, Kubernetes에서 OPA Gatekeeper로 Admission Controller로 동작하여 레이블 강제, 이미지 정책, 권한 제한 등 클러스터 리소스 생성 요청을 관리합니다.
개요
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/allow→allow룰만 조회
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은[]- 필수 값 충족
- required가
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으로 등록됨
- Gatekeeper 설치 시 자동으로
- 정책 위반 → 요청 거부 (Pod 자체가 생성 안 됨)
- 정책 통과 → 요청 승인 → Pod 생성
OPA 자체는 범용 엔진으로 kubernetes 외에서도 동작하지만 k8s에서는 Admission controller로 동작
주요 사용 사례
- 레이블/어노테이션 강제: 비용 추적용
billing, 팀 식별용owner등 필수 메타데이터 요구 - 이미지 정책: 특정 레지스트리(예: 회사 내부 레지스트리)에서만 이미지 pull 허용,
latest태그 금지input.request.object.spec.containers[_].securityContext.privilegedinput.request.object.spec.containers[_].image
- 권한 제한: 컨테이너가 root로 실행되는 것 금지, privileged 모드 금지
- 리소스 제한: 모든 Pod에 CPU/메모리 limits 필수 지정
input.request.object.spec.containers[_].resources.limits.memory
- 네임스페이스 규칙: 특정 네임스페이스에만 특정 리소스 생성 허용
결국 OPA Gatekeeper는 외부 사용자(end-user)의 HTTP 요청을 처리하는 게 아니라, 클러스터 내부에서 리소스 생성/수정 요청을 관리하는 목적으로 사용합니다.
- 강의 정리) 정의된 정책을 위반하는 객체 생성은 승인 단계에서 오류를 발생시켜 규정을 준수하지 않는 객체가 클러스터에 승인되는 것을 방지합니다.