Kubernetes 환경에서 가장 중요한 10가지 보안 위험과 이를 완화하는 방법.
OWASP Kubernetes Top 10은 Kubernetes 환경에서 가장 중요한 보안 위험을 식별합니다. Kubernetes는 컨테이너 오케스트레이션의 사실상 표준이 되었지만 유연성과 복잡성으로 인해 수많은 보안 문제가 발생합니다. 이 가이드에서는 워크로드 잘못된 구성, RBAC 문제, 비밀 관리, 네트워크 분할 등을 다룹니다.
안전하지 않은 워크로드 구성은 가장 일반적인 Kubernetes 보안 문제입니다. 과도한 권한, 쓰기 가능한 파일 시스템 또는 리소스 제한 없이 루트로 실행되는 컨테이너는 심각한 공격 표면을 만듭니다. 기본 구성은 안전하지 않은 경우가 많으므로 명시적으로 강화해야 합니다.
루트 권한과 호스트 액세스 권한이 있는 손상된 컨테이너는 컨테이너 샌드박스를 탈출하고, 호스트 파일 시스템에 액세스하고, 다른 워크로드로 피벗할 수 있습니다. 잘못 구성된 포드에서 권한을 에스컬레이션하면 전체 클러스터가 손상될 수 있습니다.
apiVersion: v1 kind: Pod metadata: name: insecure-app spec: containers: - name: app image: myapp:latest # Mutable tag! securityContext: privileged: true # Full host access! runAsUser: 0 # Running as root! volumeMounts: - mountPath: /host name: host-fs volumes: - name: host-fs hostPath: path: / # Entire host filesystem!
apiVersion: v1 kind: Pod metadata: name: secure-app spec: securityContext: runAsNonRoot: true seccompProfile: type: RuntimeDefault containers: - name: app image: myapp@sha256:abc123... # Immutable digest securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true runAsUser: 1000 capabilities: drop: ["ALL"] resources: limits: cpu: "500m" memory: "256Mi" requests: cpu: "100m" memory: "128Mi"
Kubernetes RBAC(역할 기반 액세스 제어)는 강력하지만 복잡합니다. 과도한 권한이 부여된 역할(특히 클러스터-관리자 바인딩, 와일드카드 권한, 과도한 서비스 계정 권한)은 클러스터 리소스, 비밀 및 워크로드에 대한 무단 액세스를 허용합니다.
과도한 권한이 있는 서비스 계정에 대한 액세스 권한을 얻은 공격자는 비밀을 나열하고, 권한이 있는 Pod를 생성하고, 배포를 수정하고, 클러스터 관리자로 에스컬레이션할 수 있습니다. 와일드카드 RBAC 규칙은 Kubernetes에서 루트 액세스 권한을 부여하는 것과 동일합니다.
# ClusterRoleBinding granting cluster-admin to a service account apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRoleBinding metadata: name: app-admin-binding subjects: - kind: ServiceAccount name: my-app # App SA with cluster-admin! namespace: default roleRef: kind: ClusterRole name: cluster-admin # Full cluster control! apiGroup: rbac.authorization.k8s.io
# Scoped Role with minimum required permissions apiVersion: rbac.authorization.k8s.io/v1 kind: Role metadata: name: app-role namespace: my-app-ns rules: - apiGroups: [""] resources: ["configmaps"] verbs: ["get", "list"] # Read-only, specific resources resourceNames: ["app-config"] # Named resource only --- apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: app-role-binding namespace: my-app-ns subjects: - kind: ServiceAccount name: my-app namespace: my-app-ns roleRef: kind: Role name: app-role apiGroup: rbac.authorization.k8s.io
Kubernetes 비밀은 기본적으로 암호화되지 않고 base64로만 인코딩됩니다. 일반 ConfigMap, 환경 변수 또는 암호화되지 않은 보안 비밀에 민감한 데이터(API 키, 데이터베이스 비밀번호, TLS 인증서)를 저장하면 API 액세스 권한이 있는 모든 사람에게 노출됩니다. 유휴 암호화가 활성화되지 않은 경우 비밀은 etcd에도 표시됩니다.
노출된 비밀을 통해 공격자는 데이터베이스, 클라우드 계정 및 외부 서비스에 액세스할 수 있습니다. 암호화 없이 etcd에 저장된 비밀은 etcd 액세스 권한이 있는 사람이라면 누구나 읽을 수 있습니다. 환경 변수 비밀은 포드 사양 및 프로세스 목록에 표시됩니다.
# Secret in plain ConfigMap — visible to anyone apiVersion: v1 kind: ConfigMap metadata: name: app-config data: DB_PASSWORD: "SuperSecret123!" # Plaintext password! API_KEY: "sk-live-abc123xyz" # API key in ConfigMap! --- apiVersion: v1 kind: Pod metadata: name: app spec: containers: - name: app image: myapp:latest envFrom: - configMapRef: name: app-config # All values as env vars!
# Use External Secrets Operator with a vault backend apiVersion: external-secrets.io/v1beta1 kind: ExternalSecret metadata: name: app-secrets spec: refreshInterval: 1h secretStoreRef: name: vault-backend kind: ClusterSecretStore target: name: app-secrets data: - secretKey: db-password remoteRef: key: secret/myapp/db property: password --- apiVersion: v1 kind: Pod metadata: name: app spec: containers: - name: app image: myapp@sha256:abc123... volumeMounts: - name: secrets mountPath: /etc/secrets readOnly: true # Mount as read-only files volumes: - name: secrets secret: secretName: app-secrets
클러스터 수준 정책이 없으면 안전하지 않은 워크로드 배포를 방지하는 가드레일이 없습니다. PSS(Pod Security Standards), 승인 컨트롤러 및 OPA Gatekeeper 또는 Kyverno와 같은 정책 엔진은 모든 네임스페이스에 걸쳐 보안 기준을 적용하는 데 필수적입니다.
정책 시행 없이 모든 개발자는 권한 있는 컨테이너를 배포하거나, 호스트 경로 볼륨을 사용하거나, 보안 제어를 비활성화할 수 있습니다. 잘못 구성된 단일 워크로드로 인해 전체 클러스터가 손상될 수 있습니다. 수동 검토를 통해 모든 위반 사항을 포착할 수는 없습니다.
#!/bin/bash # No admission control — any pod spec is accepted # No Pod Security Standards configured # Anyone can deploy a privileged container kubectl run hacker-pod --image=alpine \ --overrides='{"spec":{"containers":[{ "name":"hack", "image":"alpine", "securityContext":{"privileged":true}, "command":["nsenter","--target","1","--mount","--uts","--ipc","--net","--pid","--","bash"] }]}}' # No policy blocks this — now has host root access!
# Enforce Pod Security Standards with Kyverno apiVersion: kyverno.io/v1 kind: ClusterPolicy metadata: name: disallow-privileged spec: validationFailureAction: Enforce background: true rules: - name: deny-privileged match: any: - resources: kinds: ["Pod"] validate: message: "Privileged containers are not allowed." pattern: spec: containers: - securityContext: privileged: "false" - name: require-run-as-non-root match: any: - resources: kinds: ["Pod"] validate: message: "Containers must run as non-root." pattern: spec: securityContext: runAsNonRoot: true
기본적으로 Kubernetes 클러스터의 모든 Pod는 아무런 제한 없이 서로 통신할 수 있습니다. 이 플랫 네트워크 아키텍처는 손상된 포드가 다른 포드, 서비스 또는 Kubernetes API 서버에 도달할 수 있음을 의미합니다. 네트워크 정책은 마이크로 세분화를 구현하는 데 필수적입니다.
네트워크 분할이 없으면 측면 이동이 쉽지 않습니다. 손상된 프런트엔드 포드는 백엔드 데이터베이스, 내부 서비스 및 메타데이터 API(169.254.169.254)에 직접 액세스할 수 있습니다. 이는 네트워크 계층에서 최소 권한 원칙을 위반합니다.
# No NetworkPolicy — all pods can communicate freely apiVersion: v1 kind: Namespace metadata: name: production # No default deny policy # No network policies at all # Any pod can reach: # - Other pods in any namespace # - Kubernetes API server # - Cloud metadata API (169.254.169.254) # - External internet
# Default deny all ingress and egress apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: default-deny-all namespace: production spec: podSelector: {} policyTypes: ["Ingress", "Egress"] --- # Allow only frontend to backend communication apiVersion: networking.k8s.io/v1 kind: NetworkPolicy metadata: name: allow-frontend-to-backend namespace: production spec: podSelector: matchLabels: app: backend policyTypes: ["Ingress"] ingress: - from: - podSelector: matchLabels: app: frontend ports: - port: 8080 protocol: TCP
Kubernetes 제어 평면 구성 요소(API 서버, etcd, kubelet, 대시보드) 및 애플리케이션 서비스는 실수로 인터넷이나 신뢰할 수 없는 네트워크에 노출될 수 있습니다. 노출된 대시보드, 인증되지 않은 kubelet, 공개적으로 액세스 가능한 API 서버는 일반적인 공격 벡터입니다.
노출된 Kubernetes 구성요소는 클러스터 관리에 대한 직접적인 액세스를 제공합니다. 인증되지 않은 kubelet API는 컨테이너 실행을 허용합니다. 기본 자격 증명이 포함된 노출된 대시보드는 클러스터 관리자 액세스 권한을 부여합니다. 공개 etcd 액세스는 비밀을 포함한 모든 클러스터 데이터를 노출합니다.
# Kubernetes Dashboard exposed via LoadBalancer apiVersion: v1 kind: Service metadata: name: kubernetes-dashboard namespace: kubernetes-dashboard spec: type: LoadBalancer # Public internet access! ports: - port: 443 targetPort: 8443 selector: k8s-app: kubernetes-dashboard # Dashboard bound to cluster-admin service account # No authentication required — full cluster access!
# Dashboard accessible only via kubectl proxy apiVersion: v1 kind: Service metadata: name: kubernetes-dashboard namespace: kubernetes-dashboard spec: type: ClusterIP # Internal only ports: - port: 443 targetPort: 8443 selector: k8s-app: kubernetes-dashboard --- # Access via: kubectl proxy, then browse to: # http://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/ # Or use kubectl port-forward for secure access
Kubernetes 클러스터는 API 서버, etcd, kubelet, kube-proxy, CoreDNS 및 타사 추가 기능(수신 컨트롤러, 서비스 메시, 모니터링) 등 다양한 구성 요소로 구성됩니다. 잘못 구성되었거나 패치가 적용되지 않은 구성 요소로 인해 취약점이 발생합니다. 기본 구성은 강화되지 않은 경우가 많습니다.
취약한 클러스터 구성 요소는 원격 코드 실행, 권한 상승 또는 서비스 거부에 악용될 수 있습니다. kubelet, Ingress 컨트롤러 또는 CNI 플러그인의 CVE는 컨테이너 이스케이프 및 클러스터 인수로 이어질 수 있습니다. 오래된 구성 요소에는 알려진 취약점이 축적됩니다.
# kubelet with insecure configuration # /var/lib/kubelet/config.yaml apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration authentication: anonymous: enabled: true # Anyone can access kubelet API! webhook: enabled: false # No authorization! authorization: mode: AlwaysAllow # All requests permitted! readOnlyPort: 10255 # Unauthenticated read port open!
# Hardened kubelet configuration apiVersion: kubelet.config.k8s.io/v1beta1 kind: KubeletConfiguration authentication: anonymous: enabled: false # Deny anonymous access webhook: enabled: true # Use API server for auth x509: clientCAFile: /etc/kubernetes/pki/ca.crt authorization: mode: Webhook # API server authorizes requests readOnlyPort: 0 # Disable read-only port protectKernelDefaults: true eventRecordQPS: 5
클라우드 환경(AWS, GCP, Azure)에서 실행되는 Kubernetes 클러스터는 노드에 연결된 클라우드 메타데이터 API 및 IAM 역할에 액세스할 수 있습니다. 포드를 손상시킨 공격자는 이를 사용하여 클러스터에서 클라우드 계정으로 권한을 승격하고 S3 버킷, 데이터베이스 및 기타 클라우드 서비스에 액세스할 수 있습니다.
노드 수준 IAM 역할은 해당 노드의 모든 포드에 상속됩니다. 손상된 포드는 메타데이터 API(169.254.169.254)를 쿼리하여 클라우드 자격 증명을 얻은 다음 노드 역할이 허용하는 모든 클라우드 리소스에 액세스할 수 있습니다. 이를 통해 Kubernetes에서 더 넓은 클라우드 환경으로의 측면 이동이 가능해집니다.
# Node IAM role with excessive permissions resource "aws_iam_role_policy_attachment" "node_admin" { role = aws_iam_role.node_role.name policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess" # All pods on this node inherit admin access! } # No IMDS restrictions — any pod can get node credentials # curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Use IAM Roles for Service Accounts (IRSA) resource "aws_iam_role" "app_role" { name = "my-app-role" assume_role_policy = jsonencode({ Version = "2012-10-17" Statement = [{ Effect = "Allow" Principal = { Federated = aws_iam_openid_connect_provider.eks.arn } Action = "sts:AssumeRoleWithWebIdentity" Condition = { StringEquals = { "${aws_iam_openid_connect_provider.eks.url}:sub" = "system:serviceaccount:my-ns:my-app" } } }] }) } # Pod-level scoped IAM via service account annotation # Only this specific pod gets these specific permissions
Kubernetes는 인증서, 토큰, OIDC, 웹훅 인증 등 다양한 인증 메커니즘을 지원합니다. 손상된 인증에는 정적 토큰 사용, kubeconfig 파일 공유, 인증서 순환 안 함, ID 공급자와의 통합 실패 등이 포함됩니다. 서비스 계정 토큰 자동 탑재로 인해 불필요한 공격 표면이 생성됩니다.
도난당하거나 유출된 kubeconfig 파일과 서비스 계정 토큰은 직접적인 클러스터 액세스를 제공합니다. 만료되지 않는 정적 토큰은 지속적인 액세스를 제공합니다. 중앙 집중식 ID 관리가 없으면 퇴사한 팀원의 액세스 권한을 취소하는 것이 어렵고 오류가 발생하기 쉽습니다.
# Static token file for API server authentication # /etc/kubernetes/token-auth-file.csv # token,user,uid,"groups" # abc123,admin,1,"system:masters" # Static token, never expires! # Pod with auto-mounted service account token apiVersion: v1 kind: Pod metadata: name: app spec: # automountServiceAccountToken defaults to true! # Token mounted at /var/run/secrets/kubernetes.io/serviceaccount/token containers: - name: app image: myapp:latest # App doesn't need K8s API access but has a token anyway
# Disable auto-mounted token for pods that don't need API access apiVersion: v1 kind: ServiceAccount metadata: name: my-app namespace: my-app-ns automountServiceAccountToken: false # No auto-mount --- # For pods that need API access, use bound tokens with expiry apiVersion: v1 kind: Pod metadata: name: api-consumer spec: serviceAccountName: api-consumer-sa automountServiceAccountToken: false containers: - name: app image: myapp@sha256:abc123... volumeMounts: - name: token mountPath: /var/run/secrets/tokens readOnly: true volumes: - name: token projected: sources: - serviceAccountToken: expirationSeconds: 3600 # 1-hour expiry audience: api-server
Kubernetes는 API 서버 요청에 대한 감사 이벤트를 생성하지만 감사 로깅이 비활성화되거나 잘못 구성되는 경우가 많습니다. 적절한 로깅 및 모니터링이 없으면 무단 비밀 액세스, RBAC 변경, 컨테이너 이스케이프와 같은 악의적인 활동이 감지되지 않습니다. 컨테이너 및 노드 수준의 런타임 보안 모니터링이 필수적입니다.
감사 로깅이 없으면 공격자는 감지되지 않은 채 작업합니다. 백도어 서비스 계정을 생성하고, 비밀을 추출하고, 기록 없이 워크로드를 수정할 수 있습니다. 무슨 일이, 언제, 누구에 의해 발생했는지 확인할 수 있는 감사 추적이 없으면 사고 대응이 심각하게 저하됩니다.
# API server with no audit logging configured # kube-apiserver flags: # --audit-log-path="" # No audit log! # --audit-policy-file="" # No audit policy! # No runtime security monitoring # No alerting on suspicious activities # Container logs not collected centrally # Default log retention — logs lost on pod restart
# Comprehensive audit policy apiVersion: audit.k8s.io/v1 kind: Policy rules: # Log all secret access at Metadata level - level: Metadata resources: - group: "" resources: ["secrets"] # Log RBAC changes at RequestResponse level - level: RequestResponse resources: - group: rbac.authorization.k8s.io resources: ["clusterroles", "clusterrolebindings", "roles", "rolebindings"] # Log pod exec/attach (potential attacks) - level: Request resources: - group: "" resources: ["pods/exec", "pods/attach", "pods/portforward"] # Default: log at Metadata level - level: Metadata
| ID | 취약점 | 심각성 | 주요 완화 |
|---|---|---|---|
| K01 | 안전하지 않은 워크로드 구성 | Critical | 루트가 아닌 읽기 전용 FS, 삭제 기능, 리소스 제한 |
| K02 | 과도한 권한 부여 | Critical | 네임스페이스 범위 RBAC, 와일드카드 없음, 감사 바인딩 |
| K03 | 비밀 관리 실패 | Critical | 외부 비밀, 저장 암호화, 파일 마운트 |
| K04 | 정책 집행 부족 | High | 포드 보안 표준, Kyverno/OPA Gatekeeper |
| K05 | 네트워크 분할 누락 | High | 기본 거부 NetworkPolicy, 마이크로 세분화 |
| K06 | 과도하게 노출된 구성요소 | High | ClusterIP 서비스, kubectl 프록시, 방화벽 규칙 |
| K07 | 잘못 구성된 클러스터 구성 요소 | High | CIS 벤치마크, 익명 인증 비활성화, 패치 관리 |
| K08 | 클러스터에서 클라우드로의 측면 이동 | Critical | IRSA/워크로드 아이덴티티, 블록 메타데이터 API, IMDSv2 |
| K09 | 손상된 인증 메커니즘 | Critical | OIDC 통합, 토큰 바인딩, 자동 마운트 비활성화 |
| K10 | 부적절한 로깅 및 모니터링 | Medium | 감사 로깅, Falco/Tetragon, SIEM 통합 |