OPA(Open Policy Agent)는 범용 정책 엔진이고, Gatekeeper는 OPA를 Kubernetes에 통합하여 Admission Control 정책을 관리한다. 이를 통해 “컨테이너는 반드시 non-root로 실행되어야 한다” 같은 정책을 선언적으로 적용할 수 있다.
OPA 개요
OPA란?
OPA는 정책을 코드로 관리하는 오픈소스 정책 엔진이다.
1
2
3
4
5
6
7
8
9
10
11
12
| ┌────────────────────────────────────────────────────────────┐
│ OPA │
├────────────────────────────────────────────────────────────┤
│ │
│ Input (JSON) Policy (Rego) Output (Decision) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 요청 데이터│ + │ 정책 코드 │ ───→ │ allow: │ │
│ │ (Pod Spec)│ │ (Rego) │ │ true/false│ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │
└────────────────────────────────────────────────────────────┘
|
특징:
- 범용 정책 엔진 (Kubernetes 외에도 사용)
- Rego 언어로 정책 작성
- JSON 입력, JSON 출력
- 경량, 고성능
Rego 언어 기초
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| # 기본 구조
package kubernetes.admission
# 거부 규칙 정의
deny[msg] {
input.request.kind.kind == "Pod"
container := input.request.object.spec.containers[_]
not container.securityContext.runAsNonRoot
msg := sprintf("Container %v must run as non-root", [container.name])
}
# 조건문
allow {
input.request.kind.kind == "Pod"
input.request.object.spec.containers[_].image == "nginx"
}
# 변수 할당
pod := input.request.object
containers := pod.spec.containers
|
Rego 주요 개념:
- 규칙은 조건이 모두 참일 때 true
_는 반복 변수 (any)[msg]는 메시지를 포함한 set 반환
Gatekeeper
Gatekeeper란?
Gatekeeper는 OPA를 Kubernetes Admission Controller로 통합한 것이다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
| ┌─────────────────────────────────────────────────────────────┐
│ Gatekeeper Architecture │
├─────────────────────────────────────────────────────────────┤
│ │
│ API Request │
│ │ │
│ ▼ │
│ ┌──────────────┐ │
│ │ API Server │ │
│ └──────┬───────┘ │
│ │ Admission Webhook │
│ ▼ │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ Gatekeeper │────→│ ConstraintTemplate │ │
│ │ Controller │ │ (정책 템플릿) │ │
│ └──────────────┘ └──────────────────────┘ │
│ │ │ │
│ │ ┌───────▼────────┐ │
│ │ │ Constraint │ │
│ │ │ (정책 인스턴스)│ │
│ │ └────────────────┘ │
│ ▼ │
│ ┌──────────────┐ │
│ │ Allow/Deny │ │
│ └──────────────┘ │
└─────────────────────────────────────────────────────────────┘
|
주요 개념:
- ConstraintTemplate: 정책의 템플릿 (Rego 코드 포함)
- Constraint: ConstraintTemplate의 인스턴스 (파라미터 지정)
설치
1
2
3
4
5
6
| # Gatekeeper 설치
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper/v3.14.0/deploy/gatekeeper.yaml
# 확인
kubectl get pods -n gatekeeper-system
kubectl get crd | grep gatekeeper
|
ConstraintTemplate 작성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredlabels
spec:
crd:
spec:
names:
kind: K8sRequiredLabels
validation:
openAPIV3Schema:
type: object
properties:
labels:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredlabels
violation[{"msg": msg, "details": {"missing_labels": missing}}] {
provided := {label | input.review.object.metadata.labels[label]}
required := {label | label := input.parameters.labels[_]}
missing := required - provided
count(missing) > 0
msg := sprintf("Missing required labels: %v", [missing])
}
|
Constraint 생성
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- production
excludedNamespaces:
- kube-system
parameters:
labels:
- "app"
- "owner"
|
동작 확인
1
2
3
4
5
6
7
8
9
| # 정책 위반 시
kubectl run test --image=nginx -n production
# Error: admission webhook "validation.gatekeeper.sh" denied the request:
# [require-app-label] Missing required labels: {"app", "owner"}
# 정책 준수 시
kubectl run test --image=nginx -n production \
--labels="app=test,owner=team-a"
# pod/test created
|
일반적인 정책 예시
1. 프라이빗 레지스트리만 허용
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
spec:
crd:
spec:
names:
kind: K8sAllowedRepos
validation:
openAPIV3Schema:
type: object
properties:
repos:
type: array
items:
type: string
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sallowedrepos
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
satisfied := [good | repo := input.parameters.repos[_]; good := startswith(container.image, repo)]
not any(satisfied)
msg := sprintf("Container image %v is not from an allowed registry", [container.image])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: allowed-repos
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
repos:
- "registry.company.com/"
- "gcr.io/my-project/"
|
2. Privileged 컨테이너 금지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblockprivileged
spec:
crd:
spec:
names:
kind: K8sBlockPrivileged
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblockprivileged
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
container.securityContext.privileged
msg := sprintf("Privileged container not allowed: %v", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
container.securityContext.privileged
msg := sprintf("Privileged init container not allowed: %v", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockPrivileged
metadata:
name: block-privileged
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
|
3. 리소스 제한 필수
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequireresources
spec:
crd:
spec:
names:
kind: K8sRequireResources
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequireresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container %v must have CPU limits", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container %v must have memory limits", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequireResources
metadata:
name: require-resources
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
namespaces:
- production
|
4. HostPath 볼륨 금지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblockhostpath
spec:
crd:
spec:
names:
kind: K8sBlockHostPath
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblockhostpath
violation[{"msg": msg}] {
volume := input.review.object.spec.volumes[_]
volume.hostPath
msg := sprintf("HostPath volume not allowed: %v", [volume.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockHostPath
metadata:
name: block-hostpath
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
|
5. 최신 태그 금지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sblocklatest
spec:
crd:
spec:
names:
kind: K8sBlockLatest
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8sblocklatest
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
endswith(container.image, ":latest")
msg := sprintf("Container %v uses 'latest' tag", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not contains(container.image, ":")
msg := sprintf("Container %v has no tag (implies 'latest')", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sBlockLatest
metadata:
name: block-latest-tag
spec:
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
|
Gatekeeper 라이브러리
기본 제공 라이브러리
Gatekeeper는 gatekeeper-library에서 일반적인 정책 템플릿을 제공한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| # gatekeeper-library 설치
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/pod-security-policy/allow-privilege-escalation/template.yaml
# 사용 가능한 템플릿들:
# - K8sBlockProcessNamespaceSharing
# - K8sContainerLimits
# - K8sDisallowedTags
# - K8sPSPAllowPrivilegeEscalation
# - K8sPSPCapabilities
# - K8sPSPHostFilesystem
# - K8sPSPHostNamespace
# - K8sPSPPrivilegedContainer
# - K8sRequiredProbes
# 등...
|
Audit 모드
Dry-Run (감사만)
1
2
3
4
5
6
7
8
9
10
11
12
13
| apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
name: require-app-label-audit
spec:
enforcementAction: dryrun # 차단 안 함, 로그만
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
parameters:
labels:
- "app"
|
enforcementAction 옵션:
deny: 정책 위반 시 거부 (기본값)dryrun: 감사만, 거부 안 함warn: 경고만 출력, 거부 안 함
감사 결과 확인
1
2
3
4
5
6
| # Constraint 상태 확인
kubectl describe k8srequiredlabels require-app-label
# 위반 목록
kubectl get k8srequiredlabels require-app-label -o yaml
# status.violations에 위반 리소스 목록
|
Mutation
Gatekeeper Mutation
Gatekeeper 3.7+부터 Mutation(변형) 기능을 지원한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| # 기본 라벨 자동 추가
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: add-default-owner
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
kinds:
- apiGroups: ["*"]
kinds: ["Pod"]
location: "metadata.labels.owner"
parameters:
assign:
value: "default-team"
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| # SecurityContext 강제 설정
apiVersion: mutations.gatekeeper.sh/v1
kind: Assign
metadata:
name: force-nonroot
spec:
applyTo:
- groups: [""]
kinds: ["Pod"]
versions: ["v1"]
match:
scope: Namespaced
location: "spec.securityContext.runAsNonRoot"
parameters:
assign:
value: true
|
트러블슈팅
일반적인 문제
1
2
3
4
5
6
7
8
9
10
11
12
13
| # Gatekeeper Pod 상태
kubectl get pods -n gatekeeper-system
# Gatekeeper 로그
kubectl logs -n gatekeeper-system deployment/gatekeeper-controller-manager
# Constraint 상태
kubectl get constraints
kubectl describe constraint <name>
# ConstraintTemplate 상태
kubectl get constrainttemplates
kubectl describe constrainttemplate <name>
|
Webhook 타임아웃
1
2
3
4
5
| # Webhook 설정 확인
kubectl get validatingwebhookconfigurations gatekeeper-validating-webhook-configuration -o yaml
# 타임아웃 조정
# webhooks[].timeoutSeconds 값 조정
|
정책 테스트
1
2
3
4
5
| # 로컬에서 정책 테스트
opa eval -i input.json -d policy.rego "data.kubernetes.admission.deny"
# Gatekeeper 없이 정책 검증
opa test ./policies -v
|
기술 면접 대비
자주 묻는 질문
Q: OPA와 Gatekeeper의 관계는?
A: OPA는 범용 정책 엔진으로 Rego 언어로 정책을 작성하고 평가한다. Gatekeeper는 OPA를 Kubernetes Admission Controller로 통합한 것이다. Gatekeeper는 ConstraintTemplate(정책 템플릿)과 Constraint(정책 인스턴스) CRD를 제공하여 Kubernetes 친화적인 방식으로 정책을 관리한다.
Q: Gatekeeper와 Pod Security Admission(PSA)의 차이는?
A: PSA는 Kubernetes 내장 기능으로 사전 정의된 3가지 수준(Privileged, Baseline, Restricted)의 정책만 적용할 수 있다. Gatekeeper는 Rego로 커스텀 정책을 작성할 수 있어 훨씬 유연하다. 이미지 레지스트리 검증, 라벨 필수화, 리소스 제한 등 다양한 정책을 구현할 수 있다.
Q: ConstraintTemplate과 Constraint를 분리한 이유는?
A: 관심사 분리를 위해서다. ConstraintTemplate은 보안 팀이 정책 로직(Rego)을 정의하고, Constraint는 플랫폼 팀이나 개발 팀이 파라미터(적용 범위, 허용 값 등)를 지정한다. 하나의 템플릿으로 여러 Constraint를 만들어 환경별로 다른 파라미터를 적용할 수 있다.
Q: Gatekeeper의 성능 영향은?
A: Admission Webhook으로 동작하므로 모든 API 요청에 지연이 추가된다. 정책이 복잡하거나 많으면 지연이 증가한다. audit 모드로 먼저 검증하고, 정책을 점진적으로 활성화하는 것이 좋다. 또한 excludedNamespaces로 kube-system 등 중요 namespace를 제외해야 한다.
다음 단계