Kubernetes 보안: RBAC, 포드 보안 표준 및 OPA Gatekeeper
기본이지만 놀라울 정도로 안전하지 않은 Kubernetes 클러스터: 컨테이너를 실행할 수 있음 루트로 호스트 노드 파일 시스템을 마운트하고 토큰을 사용하여 서버 API에 액세스합니다. 광범위한 권한을 가진 ServiceAccount. 2025년 환경 보안 사고의 68% 컨테이너화는 취약점이 아닌 구성 오류로 인해 발생했습니다. 소프트웨어(출처: Sysdig 컨테이너 보안 보고서 2025). 좋은 소식: 쿠버네티스 이러한 위험한 설정을 제거하는 강력한 도구를 제공합니다.
이 문서에서는 전체 클러스터 강화에 대해 다룹니다. RBAC 확인하다 누가 Kubernetes API로 무엇을 할 수 있는지, 포드 보안 표준 방지하기 위해 특권 컨테이너 및 위험한 구성, e OPA 게이트키퍼 에 대한 "모든 이미지는 다음에서 가져와야 합니다"와 같은 맞춤형 회사 정책을 구현합니다. 내부 레지스트리" 또는 "리소스 제한 없이 배포 불가"입니다.
무엇을 배울 것인가
- Kubernetes 인증 모델: RBAC, 동사, 리소스, 범위
- 최소 권한 원칙: Role, ClusterRole, RoleBinding 적용
- ServiceAccount: 자동 탑재 토큰을 제한하고 IRSA/워크로드 아이덴티티를 사용하는 방법
- 포드 보안 표준: 권한 있음, 기준, 제한됨 및 네임스페이스별 적용
- OPA Gatekeeper: 설치, Rego의 ConstraintTemplate, 제약
- 공통 정책: 승인된 레지스트리 이미지, 필수 리소스 제한, 루트 없음
- 감사 로깅: 클러스터에서 누가 무엇을 하는지 모니터링
- 강화 체크리스트 완료
RBAC: 역할 기반 액세스 제어
RBAC(Role-Based Access Control)는 Kubernetes의 핵심 인증 메커니즘입니다. 정의하다 WHO (제목: User, Group, ServiceAccount) 실행 가능 어떤 행동 (동사: 가져오기, 나열, 감시, 생성, 업데이트, 패치, 삭제) 위로 어떤 자원 (리소스: 포드, 배포, 비밀) 어느 범위 (Role/RoleBinding이 있는 네임스페이스 또는 ClusterRole/ClusterRoleBinding).
개발팀의 역할 및 역할 바인딩
# rbac-developer-role.yaml
# Un ruolo per i developer: possono vedere e modificare
# deployment/pod/service nel loro namespace, ma non secrets
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: developer
namespace: team-alpha
rules:
# Deployment: lettura + scaling
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
# Pod: lettura + exec + logs
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create"]
# Service e ConfigMap: accesso completo
- apiGroups: [""]
resources: ["services", "configmaps", "endpoints"]
verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
# HPA
- apiGroups: ["autoscaling"]
resources: ["horizontalpodautoscalers"]
verbs: ["get", "list", "watch"]
# Ingress
- apiGroups: ["networking.k8s.io"]
resources: ["ingresses"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: team-alpha-developers
namespace: team-alpha
subjects:
# Gruppo di utenti (gestito da OIDC/SSO)
- kind: Group
name: "team-alpha-devs"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: Role
name: developer
apiGroup: rbac.authorization.k8s.io
제한된 범위의 SRE/관리자를 위한 ClusterRole
# rbac-sre-clusterrole.yaml
# SRE: accesso in lettura a tutto il cluster, write solo su namespace specifici
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: cluster-reader
rules:
- apiGroups: ["*"]
resources: ["*"]
verbs: ["get", "list", "watch"]
# Nega esplicita: non puo leggere i secrets (sovrascritta da DENY)
# NOTA: in RBAC Kubernetes non esiste DENY esplicita - usa Gatekeeper per questo
---
# SRE: read su tutto + write limitato ai namespace di produzione
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: sre-cluster-reader
subjects:
- kind: Group
name: "platform-sre"
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-reader
apiGroup: rbac.authorization.k8s.io
# Verifica i permessi di un utente/serviceaccount
kubectl auth can-i create pods --as=system:serviceaccount:production:api-server-sa
kubectl auth can-i delete secrets --as=developer-user -n production
kubectl auth can-i '*' '*' --as=alice # lista tutto quello che alice puo fare
ServiceAccount: 자동 토큰 제한
기본적으로 각 포드는 유효한 ServiceAccount 토큰을 자동으로 마운트합니다. 포드에서는 Kubernetes API를 호출하지 않습니다. 이 토큰은 쓸모가 없지만 공격 표면을 증가시킵니다.
# serviceaccount-minimal.yaml
# ServiceAccount dedicata per ogni applicazione (mai usare default)
apiVersion: v1
kind: ServiceAccount
metadata:
name: api-server-sa
namespace: production
annotations:
# Su AWS EKS: delega i permessi IAM tramite IRSA
eks.amazonaws.com/role-arn: "arn:aws:iam::123456789:role/ApiServerRole"
automountServiceAccountToken: false # non montare il token automaticamente
---
# Deployment che usa la ServiceAccount
apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
namespace: production
spec:
template:
spec:
serviceAccountName: api-server-sa
automountServiceAccountToken: false # ridondante ma esplicito
containers:
- name: api-server
image: my-registry/api-server:1.2.0
---
# Se il Pod deve chiamare l'API K8s, usa un token proiettato con scadenza
# invece del token auto-mounted (che non scade mai)
apiVersion: v1
kind: Pod
spec:
serviceAccountName: api-server-sa
volumes:
- name: api-token
projected:
sources:
- serviceAccountToken:
path: token
expirationSeconds: 3600 # scade ogni ora
audience: kubernetes.default.svc
containers:
- name: api-server
volumeMounts:
- name: api-token
mountPath: /var/run/secrets/kubernetes.io/serviceaccount
readOnly: true
포드 보안 표준(PSS)
Pod 보안 표준은 더 이상 사용되지 않는 PodSecurityPolicies(K8s 1.25에서 제거됨)를 대체합니다. 이는 레이블을 통해 네임스페이스 수준에서 적용 가능한 세 가지 보안 수준을 정의합니다.
- 권한: 제한 없음(kube-system과 같은 시스템 네임스페이스에만 해당)
- 기준: 가장 악명 높고 위험한 구성(특권 컨테이너, 호스트 경로, 호스트 네트워크)을 방지합니다.
- 제한된: 최대 보안을 위한 현재 모범 사례(비루트, seccomp, 기능 삭제)
각 레벨에는 세 가지 모드가 있습니다: enforce (포드를 차단함), audit
(로그를 기록하지만 차단하지는 않음), warn (사용자에게 경고를 표시합니다).
네임스페이스에 포드 보안 표준 적용
# Applica PSS al namespace tramite label
kubectl label namespace production \
pod-security.kubernetes.io/enforce=restricted \
pod-security.kubernetes.io/enforce-version=latest \
pod-security.kubernetes.io/audit=restricted \
pod-security.kubernetes.io/audit-version=latest \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/warn-version=latest
# Per namespace di sistema che richiedono pod privilegiati
kubectl label namespace kube-system \
pod-security.kubernetes.io/enforce=privileged
# Testa cosa succederebbe se applicassi restricted a un namespace esistente
kubectl label --dry-run=server --overwrite namespace production \
pod-security.kubernetes.io/enforce=restricted
제한된 수준의 포드 호환
# pod-restricted-compliant.yaml
# Un Pod che rispetta tutte le regole del livello Restricted
apiVersion: v1
kind: Pod
metadata:
name: secure-pod
namespace: production
spec:
securityContext:
runAsNonRoot: true # non girare come root
runAsUser: 1000 # UID non-root
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault # profilo seccomp di default
supplementalGroups: [1000]
containers:
- name: app
image: my-registry/api-server:1.2.0
securityContext:
allowPrivilegeEscalation: false # fondamentale: previene sudo
readOnlyRootFilesystem: true # filesystem immutabile
capabilities:
drop:
- ALL # rimuovi tutte le Linux capabilities
# add: [] - non aggiungere nessuna capability
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
volumeMounts:
- name: tmp
mountPath: /tmp # se l'app scrive su /tmp, monta un volume
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir: {}
- name: cache
emptyDir: {}
automountServiceAccountToken: false
OPA Gatekeeper: Kubernetes용 정책 엔진
포드 보안 표준은 고정된 제어 세트를 다루고 있습니다. OPA 게이트키퍼는 다음을 허용합니다. 다음을 사용하여 사용자 정의 정책을 정의하려면 레고, OPA의 언어입니다. API 서버에 대한 모든 요청을 가로채는 승인 웹훅으로 구현됩니다. 정의된 정책에 대해 유효합니다.
OPA 게이트키퍼 설치
# Installa Gatekeeper con Helm
helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
helm install gatekeeper gatekeeper/gatekeeper \
--namespace gatekeeper-system \
--create-namespace \
--version 3.17.0 \
--set auditInterval=30 \
--set constraintViolationsLimit=100 \
--set logLevel=INFO
# Verifica installazione
kubectl get pods -n gatekeeper-system
ConstraintTemplate: 승인된 레지스트리의 이미지만
# constraint-template-registry.yaml
# Il template definisce lo schema e la logica in Rego
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8sallowedrepos
annotations:
description: "Richiede che le immagini provengano solo da registry approvati"
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[_]
not starts_with_allowed(container.image)
msg := sprintf("Il container '%v' usa l'immagine '%v' che non proviene da un registry approvato", [container.name, container.image])
}
violation[{"msg": msg}] {
container := input.review.object.spec.initContainers[_]
not starts_with_allowed(container.image)
msg := sprintf("L'initContainer '%v' usa l'immagine '%v' non approvata", [container.name, container.image])
}
starts_with_allowed(image) {
repo := input.parameters.repos[_]
startswith(image, repo)
}
---
# Constraint: applica la policy con i parametri specifici
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sAllowedRepos
metadata:
name: require-approved-registry
spec:
enforcementAction: deny # deny|warn|dryrun
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- gatekeeper-system
- monitoring
parameters:
repos:
- "registry.company.internal/"
- "gcr.io/company-project/"
- "public.ecr.aws/company/"
ConstraintTemplate: 필수 자원 제한
# constraint-template-resource-limits.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8srequiredresources
spec:
crd:
spec:
names:
kind: K8sRequiredResources
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8srequiredresources
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.cpu
msg := sprintf("Container '%v': cpu limit obbligatorio ma mancante", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.limits.memory
msg := sprintf("Container '%v': memory limit obbligatorio ma mancante", [container.name])
}
violation[{"msg": msg}] {
container := input.review.object.spec.containers[_]
not container.resources.requests.cpu
msg := sprintf("Container '%v': cpu request obbligatoria ma mancante", [container.name])
}
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
name: require-resource-limits
spec:
enforcementAction: deny
match:
kinds:
- apiGroups: [""]
kinds: ["Pod"]
excludedNamespaces:
- kube-system
- gatekeeper-system
ConstraintTemplate: 컨테이너 루트 없음
# constraint-template-no-root.yaml
apiVersion: templates.gatekeeper.sh/v1
kind: ConstraintTemplate
metadata:
name: k8spsphostnamespace
spec:
crd:
spec:
names:
kind: K8sPSPHostNamespace
targets:
- target: admission.k8s.gatekeeper.sh
rego: |
package k8spsphostnamespace
violation[{"msg": msg}] {
input.review.object.spec.hostPID == true
msg := "hostPID non e consentito"
}
violation[{"msg": msg}] {
input.review.object.spec.hostIPC == true
msg := "hostIPC non e consentito"
}
violation[{"msg": msg}] {
input.review.object.spec.hostNetwork == true
not input.review.object.metadata.annotations["policy.company.internal/exempt-hostnetwork"]
msg := "hostNetwork non e consentito senza annotation di esenzione"
}
# Verifica violazioni esistenti nel cluster
kubectl get constraints
kubectl describe k8srequiredresources require-resource-limits
# Nella sezione Status.Violations vedrai tutti i Pod non conformi
감사 로깅
Kubernetes는 구조화된 감사 로그에 API 서버에 대한 모든 요청을 기록할 수 있습니다. 이는 규정 준수 및 사고 조사에 필수적입니다.
# audit-policy.yaml - configurazione dell'audit log
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
# Registra tutto sui Secrets a livello RequestResponse (incluso il contenuto)
- level: RequestResponse
resources:
- group: ""
resources: ["secrets"]
# Registra le modifiche a RBAC
- level: RequestResponse
resources:
- group: "rbac.authorization.k8s.io"
resources: ["roles", "clusterroles", "rolebindings", "clusterrolebindings"]
# Registra exec nei Pod (potenziale accesso malevolo)
- level: Request
resources:
- group: ""
resources: ["pods/exec", "pods/portforward", "pods/proxy"]
# Per tutto il resto: registra solo i metadata (chi ha fatto cosa, quando)
- level: Metadata
omitStages:
- RequestReceived
# Kube-apiserver config (aggiungi all'avvio di kube-apiserver):
# --audit-log-path=/var/log/kubernetes/audit.log
# --audit-log-maxage=30
# --audit-log-maxbackup=10
# --audit-log-maxsize=100
# --audit-policy-file=/etc/kubernetes/audit-policy.yaml
Kubernetes 체크리스트 강화
보안 강화 체크리스트
- RBAC: 각 ServiceAccount 및 사용자 그룹에 대한 최소 권한 원칙
- 서비스 계정:
automountServiceAccountToken: false서버 API를 호출하지 않는 모든 포드에서 - 토큰 만료: 프로젝션된 토큰을 사용하세요.
expirationSeconds영구 토큰 대신 - 포드 보안 표준:
restricted모든 프로덕션 네임스페이스에 걸쳐baseline준비 중 - 루트 없음:
runAsNonRoot: trueeallowPrivilegeEscalation: false모든 컨테이너에 - 불변 파일 시스템:
readOnlyRootFilesystem: true+ /tmp 및 /cache에 대한 볼륨 비어 있는Dir - 리소스 제한: 모든 컨테이너의 CPU 및 메모리 제한(적용을 위한 게이트키퍼)
- 승인된 레지스트리: 승인되지 않은 레지스트리의 이미지를 차단하는 게이트키퍼 제약
- 네트워크 정책: 모든 프로덕션 네임스페이스에 대한 기본 거부(1조 참조)
- 기미: Secret Kubernetes plain 대신 외부 Secret Manager(AWS Secrets Manager, Vault)를 사용하세요.
- 감사 로그: SIEM 또는 로그 수집기에서 활성화되고 중앙 집중화됨
- 기타: 암호화 구성으로 미사용 암호화
- 서버 API: 익명 액세스 비활성화, 승인 컨트롤러 활성화
일반적인 보안 실수
- 워크로드의 기본 네임스페이스: 기본 네임스페이스에는 기본적으로 허용적인 RBAC가 있습니다. 워크로드에는 항상 전용 네임스페이스를 사용하세요.
- CI/CD 파이프라인용 ClusterAdmin: GitOps 파이프라인에는 클러스터 관리자가 필요하지 않습니다. 대상 네임스페이스에 필요한 최소 권한으로 ServiceAccount를 만듭니다.
- 환경 변수로서의 비밀: 환경 변수는 다음에서 볼 수 있습니다.
kubectl describe pod그리고 로그에서; 보안 비밀 또는 외부 보안 관리자가 마운트한 파일 사용 - 이미지의 최신 태그: 최신 태그가 변경될 수 있습니다. 재현성과 보안을 보장하려면 항상 SHA256 다이제스트 또는 불변 태그를 사용하세요.
- 기본 이미지 CVE 무시: Trivy, Snyk 또는 Grype를 사용하여 정기적으로 이미지를 스캔합니다. 중요한 CVE가 포함된 Ubuntu 기본 이미지로 인해 클러스터 강화 작업이 취소됩니다.
결론 및 다음 단계
Kubernetes 클러스터의 보안은 계층화된 프로세스입니다. RBAC가 액세스를 제어합니다. API에 대한 Pod 보안 표준은 Pod 수준에서 위험한 구성을 방지합니다. OPA Gatekeeper를 사용하면 회사 정책을 버전이 지정된 코드로 인코딩하고 감사 가능. 이러한 도구 중 어느 것도 단독으로 충분하지 않습니다. 함께 그들은 하나를 이룬다 심층 방어를 통해 공격 표면을 대폭 줄입니다.
보안을 더욱 강화하고 시스템을 구현하기 위한 다음 단계 컨테이너 시스템 호출을 실시간으로 모니터링하는 Falco와 같은 런타임 보안 도구 비정상적인 동작에 대한 경고(프로덕션의 컨테이너에서 열린 쉘, 읽기 자격 증명 파일, 예상치 못한 네트워크 연결).
Kubernetes at Scale 시리즈의 향후 기사
이전 기사
관련 시리즈
- DevSecOps — 코드형 정책, CI/CD 파이프라인의 보안 검색
- 쿠버네티스 네트워킹 — 워크로드를 격리하는 네트워크 정책
- 플랫폼 엔지니어링 — 팀을 위한 자동 가드레일과 같은 보안







