시작하며
프로젝트의 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들이 보안 설정 없이 실행되고 있었거든요.
발견한 문제점:
- Root 권한으로 실행되는 Pod들 (runAsUser: 0)
- istio-cni-node-* (4개)
- ztunnel-* (4개)
- alloy-* (4개)
- SecurityContext가 완전히 비어있는 Pod들 ({})
- ArgoCD 관련 대부분의 Pod
- Kyverno의 모든 컴포넌트 (아이러니하게도 보안 정책 도구인데!)
- AWS 관련 시스템 컴포넌트들
- 제대로 설정된 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
이렇게 하면:
- Git에는 암호화되지 않은 비밀값이 저장되지 않음
- AWS Secrets Manager에서 중앙 관리
- 자동으로 1시간마다 동기화
- 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
모니터링 중인 메트릭:
- 보안 메트릭
- SecurityContext 위반 (Kyverno PolicyReport)
- 리소스 제한 초과
- 비정상적인 네트워크 트래픽
- 성능 메트릭
- CPU/메모리 사용률
- HPA 스케일링 이벤트
- Pod 재시작 횟수
- 가용성 메트릭
- 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 |