실전 Kubernetes 보안: 4C 모델을 프로덕션 클러스터에 적용하며 배운 것들

2026. 1. 4. 21:19·Network

시작하며

프로젝트의 EKS 클러스터를 운영하면서 가장 걱정되었던 부분이 보안이었습니다. "우리 클러스터는 안전할까?"라는 질문에 자신 있게 답할 수 없었죠.

Kubernetes 보안 모범 사례를 검색하면 수많은 문서와 도구들이 나오지만, 막상 실제로 적용하려니 어디서부터 시작해야 할지, 어떤 도구를 써야 할지 막막했습니다. SonarQube, Trivy, Kyverno, kube-bench... 이 모든 걸 다 적용해야 하는 걸까요?

이 글에서는 제가 실제 프로덕션 클러스터의 보안 상태를 점검하고 개선한 과정을 공유하려고 합니다. Kubernetes 4C 보안 모델(Code, Container, Cluster, Cloud)을 기준으로 하나씩 실제 명령어를 실행하며 문제점을 찾고 해결한 이야기입니다.


현재 상태 진단: "우리 클러스터는 안전할까?"

첫 번째 점검: SecurityContext 확인

가장 먼저 확인한 것은 Pod들이 어떤 권한으로 실행되고 있는지였습니다. 특히 root 권한으로 실행되는 Pod가 있는지 확인해봤습니다.

# root로 실행되거나 SecurityContext가 없는 Pod 찾기
kubectl get pods --all-namespaces -o jsonpath='{range .items[*]}{.metadata.name}{"\t"}{.spec.containers[*].securityContext.runAsUser}{"\n"}{end}' | grep -E "(^.*\t$|.*\t0$)"

결과를 보고 놀랐습니다. 생각보다 많은 Pod들이 보안 설정 없이 실행되고 있었거든요.

발견한 문제점:

  1. Root 권한으로 실행되는 Pod들 (runAsUser: 0)
    • istio-cni-node-* (4개)
    • ztunnel-* (4개)
    • alloy-* (4개)
  2. SecurityContext가 완전히 비어있는 Pod들 ({})
    • ArgoCD 관련 대부분의 Pod
    • Kyverno의 모든 컴포넌트 (아이러니하게도 보안 정책 도구인데!)
    • AWS 관련 시스템 컴포넌트들
  3. 제대로 설정된 Pod들
    • 다행히 애플리케이션 서비스들은 잘 설정되어 있었습니다
    • auth-service, user-service 등은 runAsNonRoot: true와 적절한 보안 설정 포함

두 번째 점검: 리소스 제한 확인

다음으로 리소스 제한이 없는 Pod들을 찾아봤습니다.

# Resource Limits가 없는 Pod 찾기
kubectl get pods --all-namespaces -o json | jq '.items[] | select(.spec.containers[].resources.limits == null) | .metadata.name'

역시나 많은 시스템 컴포넌트들이 리소스 제한 없이 실행되고 있었습니다. 이는 리소스 고갈 공격에 취약할 수 있는 상태였죠.


Container 보안: SecurityContext와 이미지 관리

SecurityContext 적용 전략

모든 Pod에 보안 설정을 적용하기 위해 Helm Chart의 values.yaml을 다음과 같이 구성했습니다.

# =============================================================================
# Security Configuration
# =============================================================================
podSecurityContext:
  runAsNonRoot: true  # root 권한 금지
  runAsUser: 1000     # 특정 UID로 실행
  fsGroup: 1000       # 파일시스템 그룹
  seccompProfile:
    type: RuntimeDefault  # Seccomp 프로필 적용

securityContext:
  allowPrivilegeEscalation: false  # 권한 상승 방지
  capabilities:
    drop:
      - ALL  # 모든 Linux capabilities 제거
  readOnlyRootFilesystem: false  # Spring Boot는 /tmp 필요
  runAsNonRoot: true
  runAsUser: 1000

Spring Boot vs Go 서비스의 차이점:

Spring Boot 서비스는 Go 서비스보다 특별한 고려사항이 필요했습니다.

# Spring Boot 서비스 (auth-service)
javaResources:
  requests:
    memory: "384Mi"  # Go보다 훨씬 많은 메모리 필요
    cpu: "100m"
  limits:
    memory: "768Mi"
    cpu: "500m"

healthCheck:
  liveness:
    initialDelaySeconds: 90  # 시작 시간이 오래 걸림 (~73초)
    failureThreshold: 5      # 더 많은 재시도 필요

실제 리소스 사용량을 확인해보니 Spring Boot 서비스가 Go 서비스보다 약 20배 많은 메모리를 사용하고 있었습니다.

kubectl top pods -n wealist-prod
# auth-service (Java):  3m CPU, 280Mi 메모리
# user-service (Go):    2m CPU, 12Mi 메모리

Kyverno를 이용한 정책 강제

보안 설정을 수동으로 관리하는 것은 실수하기 쉽습니다. 그래서 Kyverno를 사용해 클러스터 레벨에서 정책을 강제했습니다.

# 현재 적용된 정책 확인
kubectl get clusterpolicies

NAME                      ADMISSION   BACKGROUND   READY   AGE
require-resource-limits   true        true         True    2d22h

Policy Report를 확인해보니 모든 Deployment가 정책을 준수하고 있었습니다.

kubectl get policyreports -n wealist-prod

# 모든 서비스 PASS ✅
# PASS   FAIL   WARN
# 1      0      0

배운 점:

Kyverno는 설치만으로는 의미가 없습니다. 실제로 ClusterPolicy를 정의하고 적용해야 효과가 있어요. 처음에는 Kyverno를 설치만 하고 정책은 하나도 만들지 않아서 의미가 없었습니다.


Cluster 보안: 네트워크 정책과 mTLS

Network Policy로 서비스 간 격리

서비스 메시 없이도 기본적인 네트워크 격리는 필수입니다.

# 적용된 Network Policy 확인
kubectl get networkpolicies -n wealist-prod

# 7개의 서비스 각각에 정책 적용됨
auth-service, user-service, board-service...

각 서비스는 필요한 통신만 허용하도록 설정했습니다.

# auth-service Network Policy 예시
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: auth-service
  namespace: wealist-prod
spec:
  podSelector:
    matchLabels:
      app.kubernetes.io/name: auth-service
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - podSelector:
            matchLabels:
              app.kubernetes.io/part-of: wealist
      ports:
        - protocol: TCP
          port: 8080
  egress:
    - to:
        - namespaceSelector: {}  # Redis, 외부 서비스 등
      ports:
        - protocol: TCP
          port: 6379  # Redis

Istio Ambient 모드: 투명한 mTLS

프로젝트에서는 Istio의 Ambient 모드를 사용하고 있습니다. 전통적인 Sidecar 방식과 달리 훨씬 가벼운 방식이죠.

# PeerAuthentication으로 mTLS 강제
kubectl get peerauthentication -n wealist-prod

NAME      MODE     AGE
default   STRICT   3d  # ← 모든 통신이 암호화됨

Ambient 모드의 장점:

전통적인 Sidecar 방식:
Pod당 메모리 = App Container (512Mi) + Istio Sidecar (128Mi) = 640Mi
18개 Pod × 128Mi = 2.3Gi 추가 메모리

Ambient 모드:
Pod당 메모리 = App Container (512Mi) 만!
Ztunnel: 노드당 1개 (4노드 × 3Mi = 12Mi)
→ 약 2.29Gi 메모리 절약! 💰

실제로 ztunnel Pod들을 확인해보니 정말 가볍게 실행되고 있었습니다.

kubectl top pods -n istio-system -l app=ztunnel

NAME            CPU   MEMORY
ztunnel-bv9m4   1m    3Mi
ztunnel-lppbf   1m    3Mi
ztunnel-mgbp7   1m    3Mi
ztunnel-pnlm2   1m    3Mi

하지만 한계도 있습니다:

Ambient 모드에서는 전통적인 디버깅 방법이 작동하지 않았습니다.

# 이 명령어가 작동하지 않음
istioctl proxy-config secret ztunnel-bv9m4 -n istio-system

Error: config dump has no configuration type...

이유는 ztunnel이 Envoy가 아니라 Rust로 작성된 경량 프록시이기 때문입니다. 대신 다음 방법으로 확인할 수 있었습니다.

# PeerAuthentication 정책 확인
kubectl get peerauthentication default -n wealist-prod -o yaml

# 서비스 레벨에서 mTLS 확인
istioctl x describe service auth-service -n wealist-prod

발견한 보안 공백: AuthorizationPolicy 미적용

여기서 중요한 문제를 발견했습니다.

kubectl get authorizationpolicies --all-namespaces
No resources found  # ← 문제!

현재는 암호화만 되고 있고, 접근 제어는 없는 상태였습니다. 즉, 모든 서비스가 서로를 호출할 수 있었죠.

예를 들어:

  • ✅ user-service → auth-service (당연히 필요)
  • ❌ storage-service → auth-service (필요 없는데 가능함)

이는 다음 단계에서 개선할 예정입니다.


Cloud 보안: AWS 통합과 Secret 관리

External Secrets Operator로 안전한 비밀키 관리

민감한 정보를 Git에 저장하지 않기 위해 External Secrets Operator와 AWS Secrets Manager를 연동했습니다.

# ExternalSecret 리소스 확인
kubectl get externalsecrets -n wealist-prod

NAME                    STORE                 REFRESH   STATUS
wealist-shared-secret   aws-secrets-manager   1h        SecretSynced ✅
wealiist-livekit-keys   aws-secrets-manager   1h        SecretSynced ✅

작동 방식:

apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: wealist-shared-secret
  namespace: wealist-prod
spec:
  refreshInterval: 1h  # 1시간마다 AWS와 동기화
  secretStoreRef:
    kind: ClusterSecretStore
    name: aws-secrets-manager
  target:
    name: wealist-shared-secret
    creationPolicy: Owner
  data:
    - secretKey: REDIS_HOST
      remoteRef:
        key: wealist/prod/shared  # AWS Secrets Manager 경로
        property: REDIS_HOST

이렇게 하면:

  1. Git에는 암호화되지 않은 비밀값이 저장되지 않음
  2. AWS Secrets Manager에서 중앙 관리
  3. 자동으로 1시간마다 동기화
  4. Secret 로테이션이 쉬움

++ ESO관련한 EKS 세팅 방식기록

https://squash29.tistory.com/84

RBAC: 최소 권한 원칙

cluster-admin 권한을 가진 주체를 확인했습니다.

kubectl get clusterrolebindings -o json | jq '.items[] | select(.roleRef.name=="cluster-admin")'

다행히 system:masters 그룹과 eks:addon-manager만 전체 권한을 가지고 있었습니다. 각 서비스는 자체 ServiceAccount를 사용하고 있었죠.

# 각 서비스마다 전용 ServiceAccount
serviceAccount:
  create: true
  automount: true
  name: ""  # 자동 생성

가용성: HPA와 리소스 관리

HPA로 자동 스케일링

모든 주요 서비스에 Horizontal Pod Autoscaler를 적용했습니다.

kubectl get hpa -n wealist-prod

NAME              TARGETS                         MINPODS   MAXPODS   REPLICAS
auth-service      cpu: 3%/70%                     2         10        2
user-service      cpu: 8%/70%, memory: 19%/90%    2         10        2
board-service     cpu: 12%/70%, memory: 18%/90%   1         10        1

HPA 설정의 핵심:

autoscaling:
  enabled: true
  minReplicas: 2  # 최소 2개로 고가용성 확보
  maxReplicas: 10
  targetCPUUtilizationPercentage: 70
  targetMemoryUtilizationPercentage: 80
  behavior:
    scaleDown:
      stabilizationWindowSeconds: 300  # 5분간 지켜본 후 축소
      policies:
        - type: Percent
          value: 50  # 한 번에 50%만 축소
          periodSeconds: 60
    scaleUp:
      stabilizationWindowSeconds: 0  # 즉시 확장
      policies:
        - type: Percent
          value: 100  # 필요시 2배로 확장
          periodSeconds: 30

왜 이렇게 설정했을까?

  • ScaleUp은 즉시: 트래픽이 급증하면 빠르게 대응
  • ScaleDown은 천천히: 순간적인 트래픽 감소로 인한 불필요한 축소 방지
  • MinReplicas 2개: 한 Pod가 죽어도 서비스 유지

실제 HPA의 상세 정보를 확인해보니 제대로 작동하고 있었습니다.

kubectl describe hpa auth-service -n wealist-prod

Metrics:   cpu: 3% (3m) / 70%
Deployment pods: 2 current / 2 desired
Conditions:
  ScalingActive   True   ValidMetricFound
  ScalingLimited  True   TooFewReplicas  # ← minReplicas에 도달

Pod Disruption Budget으로 고가용성 보장

노드 유지보수나 업데이트 시에도 서비스가 중단되지 않도록 PDB를 설정했습니다.

podDisruptionBudget:
  enabled: true
  minAvailable: 1  # 최소 1개는 항상 실행 중이어야 함

이제 노드를 drain할 때도 최소 1개의 Pod는 유지되므로, 무중단 배포가 가능합니다.


실제 리소스 사용량 분석

현재 클러스터의 리소스 사용 현황을 확인해봤습니다.

kubectl top pods -n wealist-prod

가장 많은 리소스를 사용하는 Top 5:

Pod CPU 메모리 비고

prometheus 34m 325Mi 모니터링 필수
auth-service (x2) 3m ~280Mi Spring Boot
loki 11m 99Mi 로그 수집
grafana 7m 74Mi 대시보드
alloy (x4) 8-10m 47-57Mi 메트릭 수집

흥미로운 발견:

Go로 작성된 서비스들은 정말 가볍습니다.

  • user-service (Go): 2m CPU, 12Mi 메모리
  • board-service (Go): 3m CPU, 12Mi 메모리

반면 Spring Boot 서비스는:

  • auth-service (Java): 3m CPU, 280Mi 메모리

메모리 사용량이 약 20배 차이가 납니다. 이것이 리소스 제한을 다르게 설정한 이유입니다.


모니터링: Grafana + Prometheus + Loki

보안 정책을 적용했으면 실제로 작동하는지 모니터링해야 합니다.

# 모니터링 스택 확인
kubectl get pods -n wealist-prod | grep -E "(grafana|prometheus|loki)"

grafana-xxx           1/1   Running
prometheus-xxx        1/1   Running
loki-xxx              1/1   Running

모니터링 중인 메트릭:

  1. 보안 메트릭
    • SecurityContext 위반 (Kyverno PolicyReport)
    • 리소스 제한 초과
    • 비정상적인 네트워크 트래픽
  2. 성능 메트릭
    • CPU/메모리 사용률
    • HPA 스케일링 이벤트
    • Pod 재시작 횟수
  3. 가용성 메트릭
    • Pod Readiness
    • 서비스 응답 시간
    • 에러율

Grafana에서 주시하는 대시보드:

  • Kubernetes Cluster Overview
  • Pod Resource Usage
  • HPA Metrics
  • Network Policy Violations (향후 추가 예정)

배운 점과 앞으로의 계획

배운 점

1. "완벽한 보안"은 없다, 지속적인 개선만 있을 뿐

처음에는 모든 보안 도구를 다 적용해야 한다고 생각했습니다. 하지만 실제로는:

  • 현재 환경에 맞는 도구 선택
  • 단계적으로 적용
  • 지속적으로 모니터링

이것이 더 중요했습니다.

2. 시스템 컴포넌트 vs 애플리케이션 서비스

모든 Pod에 동일한 보안 정책을 적용할 수는 없습니다.

  • Istio CNI나 ztunnel은 노드 레벨 작업이 필요해서 root 권한 필요
  • 하지만 애플리케이션 서비스는 반드시 non-root로 실행
  • Kyverno 정책도 네임스페이스별로 차별화 필요

3. Spring Boot vs Go: 리소스 관리의 중요성

언어 선택에 따라 리소스 요구사항이 크게 달라집니다.

  • Go: 가볍고 빠른 시작
  • Spring Boot: 더 많은 메모리, 느린 시작

각각에 맞는 설정이 필요합니다.

앞으로의 계획

1. AuthorizationPolicy 적용 (우선순위 1)

현재 가장 큰 보안 공백입니다.

# 계획: auth-service는 user-service만 접근 가능
apiVersion: security.istio.io/v1
kind: AuthorizationPolicy
metadata:
  name: auth-service-policy
  namespace: wealist-prod
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: auth-service
  action: ALLOW
  rules:
  - from:
    - source:
        principals: 
        - "cluster.local/ns/wealist-prod/sa/user-service"
        - "cluster.local/ns/wealist-prod/sa/board-service"

2. kube-bench로 CIS 벤치마크 점검

클러스터가 업계 표준을 준수하는지 확인할 예정입니다.

kubectl apply -f https://raw.githubusercontent.com/aquasecurity/kube-bench/main/job.yaml
kubectl logs job/kube-bench

3. 정기적인 보안 점검 자동화

매주 자동으로 실행되는 점검 스크립트:

  • SecurityContext 검증
  • 리소스 제한 확인
  • Network Policy 검증
  • Secret 만료일 확인

4. Grafana 대시보드 강화

보안 메트릭을 한눈에 볼 수 있는 대시보드 구성:

  • Kyverno Policy Violations
  • Pod Security Score
  • Network Traffic Anomalies

마치며

Kubernetes 보안은 한 번 설정하고 끝나는 것이 아니라, 지속적으로 점검하고 개선하는 과정입니다.

이번 작업을 통해 저는:

  • 실제 클러스터의 보안 상태를 정확히 파악했습니다
  • 우선순위를 정해 단계적으로 개선했습니다
  • 모니터링을 통해 지속적으로 확인하는 체계를 만들었습니다

완벽하지는 않지만, 처음보다는 훨씬 안전한 클러스터가 되었습니다. 그리고 무엇보다 **"우리 클러스터는 안전할까?"**라는 질문에 이제는 자신 있게 답할 수 있게 되었습니다.

여러분도 비슷한 고민을 하고 계신가요? 어떤 보안 도구를 사용하고 계신지, 어떤 어려움이 있으신지 댓글로 공유해주세요!


참고 자료

  • https://www.youtube.com/watch?v=Ja4hIwuOSM8&t=1502s 
  • Kubernetes 4C Security Model
  • Kyverno Policy Library
  • Istio Ambient Mesh
  • External Secrets Operator
  • CIS Kubernetes Benchmark

 

 

EKS에서 ArgoCD로 GitOps 구축하기: 순환 의존성 해결부터 Single Sync까지

시작하며최근 프로젝트에서 마이크로서비스 아키텍처를 EKS로 이전하면서, GitOps를 도입하기로 결정했습니다. 하지만 막상 ArgoCD를 설치하려고 하니 예상치 못한 문제들이 하나둘 나타나기 시작

squash29.tistory.com

 

 

해당 동영상을 보고 프로젝트에 적용해보았습니다.


체크리스트: Kubernetes 보안 점검

여러분도 한번 점검해보세요!

Container 보안

  • [ ] 모든 Pod에 SecurityContext 적용
  • [ ] runAsNonRoot: true 설정
  • [ ] readOnlyRootFilesystem 가능한 경우 적용
  • [ ] capabilities.drop: [ALL] 설정

Cluster 보안

  • [ ] Network Policy로 서비스 격리
  • [ ] mTLS 적용 (Istio 또는 Linkerd)
  • [ ] AuthorizationPolicy로 접근 제어
  • [ ] RBAC 최소 권한 원칙 준수

Cloud 보안

  • [ ] ExternalSecret으로 비밀키 관리
  • [ ] Secret 정기 로테이션
  • [ ] IAM 역할 분리
  • [ ] 감사 로그 활성화

가용성

  • [ ] HPA 적용
  • [ ] Resource Limits/Requests 설정
  • [ ] Pod Disruption Budget 설정
  • [ ] Health Check 구성

모니터링

  • [ ] 보안 메트릭 수집
  • [ ] 정기적인 Policy Report 확인
  • [ ] 이상 트래픽 모니터링
  • [ ] 자동화된 알림 설정

 

 

'Network' 카테고리의 다른 글

Istio로 대규모 트래픽 관리하기: 실전 적용 사례와 확장 전략  (2) 2026.01.10
EKS에서 ArgoCD로 GitOps 구축하기: 순환 의존성 해결부터 Single Sync까지  (0) 2026.01.05
GitHub Actions Docker CI 파이프라인 구조 이해하기: 멀티 아키텍처 빌드부터 캐시 전략  (1) 2026.01.03
SealSecret vs AWS Secret Manager  (0) 2026.01.02
Prometheus 디스코드 웹훅  (0) 2025.10.02
'Network' 카테고리의 다른 글
  • Istio로 대규모 트래픽 관리하기: 실전 적용 사례와 확장 전략
  • EKS에서 ArgoCD로 GitOps 구축하기: 순환 의존성 해결부터 Single Sync까지
  • GitHub Actions Docker CI 파이프라인 구조 이해하기: 멀티 아키텍처 빌드부터 캐시 전략
  • SealSecret vs AWS Secret Manager
Ry-
Ry-
  • Ry-
    developer_Ryu
    Ry-
  • 전체
    오늘
    어제
    • 분류 전체보기 (97)
      • AI (14)
      • DB (3)
      • Book (6)
      • Development Environment (2)
        • Mac (2)
      • Frontend (8)
        • Flutter (6)
        • React (1)
        • React-Native (1)
      • Backend (19)
        • Spring (14)
        • Java (1)
        • Node.js (4)
      • Computer Science (27)
        • PS (20)
        • CS (7)
      • Network (12)
      • 이것저것 (3)
  • 블로그 메뉴

    • 링크

    • 공지사항

    • 인기 글

    • 태그

      백준
      구글 로그인
      spring
      redis
      aop
      database
      오블완
      flutter
      티스토리챌린지
      mongodb
      mysql
      chatgpt api
      BOGGLE
      몽고디비
      node.js
      Kakao
      종만북
      Baekjoon
      스프링
      최대 힙
      java
    • 최근 댓글

    • 최근 글

    • hELLO· Designed By정상우.v4.10.6
    Ry-
    실전 Kubernetes 보안: 4C 모델을 프로덕션 클러스터에 적용하며 배운 것들
    상단으로

    티스토리툴바