개요

지금까지 Part 1~7에서 K8s 클러스터 구축, 모니터링, 무중단 업그레이드를 다뤘습니다. 이제 실제로 운영할 때 반드시 필요한 보안을 구현해봅시다.

보안 없는 K8s는 불안전한 서버와 같습니다. 특히 다음 세 가지가 필수입니다:

  1. RBAC - 누가 뭘 할 수 있는지 정의
  2. 네트워크 정책 - 어떤 Pod끼리 통신할 수 있는지 제어
  3. Pod 보안 - 컨테이너가 어떤 권한으로 실행되는지 제한

1. RBAC (Role-Based Access Control)

개념

K8s RBAC는 역할 기반 접근 제어입니다. “누가(Subject) 어떤 자원(Resource)에 어떤 행동(Verb)을 할 수 있는가"를 정의합니다.

4가지 핵심 개념:

  • Role: 권한의 모음 (특정 네임스페이스)
  • ClusterRole: 권한의 모음 (전체 클러스터)
  • RoleBinding: Role을 사용자/SA에 연결
  • ClusterRoleBinding: ClusterRole을 사용자/SA에 연결

실전 예제: 앱 배포용 ServiceAccount

 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
43
44
45
46
47
48
49
50
51
52
# 1. 앱 배포용 네임스페이스
apiVersion: v1
kind: Namespace
metadata:
  name: app-ns

---
# 2. 앱 배포용 ServiceAccount (Pod가 API 호출할 때 사용)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: app-deployer
  namespace: app-ns

---
# 3. Role: Pod와 Deployment만 관리 가능
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: app-deployer-role
  namespace: app-ns
rules:
# Pod 조회, 생성, 수정, 삭제
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# Deployment 조회, 생성, 수정, 삭제
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]

# ConfigMap 조회 (앱 설정 읽기)
- apiGroups: [""]
  resources: ["configmaps"]
  verbs: ["get", "list"]

---
# 4. RoleBinding: ServiceAccount에 Role 연결
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: app-deployer-binding
  namespace: app-ns
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: app-deployer-role
subjects:
- kind: ServiceAccount
  name: app-deployer
  namespace: app-ns

적용하기

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# YAML 적용
kubectl apply -f rbac-example.yaml

# 권한 확인
kubectl get rolebindings -n app-ns
kubectl describe rolebinding app-deployer-binding -n app-ns

# 특정 SA의 권한 테스트
kubectl auth can-i get pods \
  --as=system:serviceaccount:app-ns:app-deployer \
  -n app-ns
# 결과: yes

# Pod 생성 권한 테스트
kubectl auth can-i create pods \
  --as=system:serviceaccount:app-ns:app-deployer \
  -n app-ns
# 결과: yes

# Secret 접근 권한 테스트 (정의하지 않음)
kubectl auth can-i get secrets \
  --as=system:serviceaccount:app-ns:app-deployer \
  -n app-ns
# 결과: no

Pod에서 ServiceAccount 사용

 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
apiVersion: apps/v1
kind: Deployment
metadata:
  name: app-deployer-pod
  namespace: app-ns
spec:
  replicas: 1
  selector:
    matchLabels:
      app: app-deployer
  template:
    metadata:
      labels:
        app: app-deployer
    spec:
      serviceAccountName: app-deployer  # ← ServiceAccount 지정
      containers:
      - name: app
        image: nginx:alpine
        volumeMounts:
        - name: token
          mountPath: /var/run/secrets/kubernetes.io/serviceaccount
          readOnly: true
      volumes:
      - name: token
        projected:
          sources:
          - serviceAccountToken:
              audience: https://kubernetes.default.svc.cluster.local
              expirationSeconds: 3600
              path: token

2. 네트워크 정책 (Network Policies)

개념

기본적으로 K8s의 모든 Pod은 서로 통신할 수 있습니다. 네트워크 정책으로 “어떤 Pod끼리만 통신하게 할 것인가"를 정의합니다.

네트워크 정책이 필요한 이유:

  • 앱 간 불필요한 통신 차단
  • 데이터베이스 접근 제한
  • 외부로부터의 접근 제어

실전 예제: 다계층 아키텍처

Frontend (nginx)
    ↓
API Server (app-api)
    ↓
Database (postgres) - Frontend는 직접 접근 불가
  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
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# 1. Namespace
apiVersion: v1
kind: Namespace
metadata:
  name: multi-tier

---
# 2. Frontend Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: frontend
  namespace: multi-tier
spec:
  replicas: 2
  selector:
    matchLabels:
      tier: frontend
  template:
    metadata:
      labels:
        tier: frontend
    spec:
      containers:
      - name: nginx
        image: nginx:alpine
        ports:
        - containerPort: 80

---
# 3. API Server Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: api-server
  namespace: multi-tier
spec:
  replicas: 2
  selector:
    matchLabels:
      tier: api
  template:
    metadata:
      labels:
        tier: api
    spec:
      containers:
      - name: api
        image: python:3.11-slim
        ports:
        - containerPort: 8000

---
# 4. Database StatefulSet (간단한 예제)
apiVersion: apps/v1
kind: StatefulSet
metadata:
  name: database
  namespace: multi-tier
spec:
  serviceName: database
  replicas: 1
  selector:
    matchLabels:
      tier: database
  template:
    metadata:
      labels:
        tier: database
    spec:
      containers:
      - name: postgres
        image: postgres:16-alpine
        ports:
        - containerPort: 5432

---
# 5. Service (내부 통신용)
apiVersion: v1
kind: Service
metadata:
  name: api-service
  namespace: multi-tier
spec:
  selector:
    tier: api
  ports:
  - port: 8000
    targetPort: 8000
  clusterIP: None

---
apiVersion: v1
kind: Service
metadata:
  name: database-service
  namespace: multi-tier
spec:
  selector:
    tier: database
  ports:
  - port: 5432
    targetPort: 5432
  clusterIP: None

---
# 6. 기본: 모든 트래픽 차단
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny
  namespace: multi-tier
spec:
  podSelector: {}  # 모든 Pod
  policyTypes:
  - Ingress
  - Egress

---
# 7. Frontend → API 통신 허용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-to-api
  namespace: multi-tier
spec:
  podSelector:
    matchLabels:
      tier: api
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: frontend
    ports:
    - protocol: TCP
      port: 8000

---
# 8. API → Database 통신 허용
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: api-to-database
  namespace: multi-tier
spec:
  podSelector:
    matchLabels:
      tier: database
  policyTypes:
  - Ingress
  ingress:
  - from:
    - podSelector:
        matchLabels:
          tier: api
    ports:
    - protocol: TCP
      port: 5432

---
# 9. Frontend 외부 트래픽 허용 (80 포트)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: frontend-ingress
  namespace: multi-tier
spec:
  podSelector:
    matchLabels:
      tier: frontend
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector: {}  # 모든 네임스페이스 (또는 특정 CIDR)
    ports:
    - protocol: TCP
      port: 80

---
# 10. 모든 Pod의 DNS 통신 허용 (coreDNS)
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns-egress
  namespace: multi-tier
spec:
  podSelector: {}
  policyTypes:
  - Egress
  egress:
  # DNS (coreDNS)
  - to:
    - namespaceSelector:
        matchLabels:
          name: kube-system
    ports:
    - protocol: UDP
      port: 53
  # API Server
  - to:
    - namespaceSelector: {}
    ports:
    - protocol: TCP
      port: 443

적용하고 테스트

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 적용
kubectl apply -f network-policy-example.yaml

# 네트워크 정책 확인
kubectl get networkpolicies -n multi-tier

# Frontend Pod에서 API 접근 테스트
kubectl exec -it <frontend-pod> -n multi-tier -- sh
# curl http://api-service:8000
# → 성공 (정책으로 허용됨)

# Frontend Pod에서 Database 접근 테스트
# kubectl exec -it <frontend-pod> -n multi-tier -- sh
# nc -zv database-service 5432
# → 실패 (정책으로 차단됨) ✓

# API Pod에서 Database 접근 테스트
# kubectl exec -it <api-pod> -n multi-tier -- sh
# nc -zv database-service 5432
# → 성공 (정책으로 허용됨) ✓

3. Pod 보안 (Pod Security Policy / Pod Security Standards)

개념

Pod 보안은 컨테이너가 “어떤 권한으로” 실행되는지를 제한합니다.

주요 제어사항:

  • 컨테이너가 root로 실행되는지 여부
  • 특권 컨테이너(privileged) 사용 제한
  • 호스트 네트워크 접근 제한
  • 파일 시스템 읽기 전용 여부

참고: Pod Security Policy(PSP)는 deprecated되었고, **Pod Security Standards (PSS)**로 대체되었습니다.

실전 예제: Pod Security Standards

  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
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
# 1. Namespace에 보안 레이블 추가
apiVersion: v1
kind: Namespace
metadata:
  name: secure-app
  labels:
    # Pod Security Standards 레이블
    pod-security.kubernetes.io/enforce: restricted  # 가장 엄격
    pod-security.kubernetes.io/audit: restricted
    pod-security.kubernetes.io/warn: restricted

---
# 2. 보안이 강화된 Pod 정의
apiVersion: v1
kind: Pod
metadata:
  name: secure-app
  namespace: secure-app
spec:
  securityContext:
    runAsNonRoot: true        # root 실행 금지
    runAsUser: 1000           # UID 1000으로 실행
    fsGroup: 2000             # 파일 그룹
    seccompProfile:           # Seccomp 프로필
      type: RuntimeDefault
  containers:
  - name: app
    image: python:3.11-slim
    ports:
    - containerPort: 8000
    securityContext:
      allowPrivilegeEscalation: false  # 권한 상승 금지
      readOnlyRootFilesystem: true     # 루트 FS 읽기 전용
      capabilities:
        drop:
        - ALL              # 모든 Linux capability 제거
        add:
        - NET_BIND_SERVICE # 필요한 것만 추가
    resources:
      requests:
        memory: "128Mi"
        cpu: "100m"
      limits:
        memory: "256Mi"
        cpu: "500m"
    volumeMounts:
    - name: tmp
      mountPath: /tmp     # 쓰기 가능한 영역
    - name: app-data
      mountPath: /app/data
  volumes:
  - name: tmp
    emptyDir: {}
  - name: app-data
    emptyDir: {}

---
# 3. 보안이 적용된 Deployment
apiVersion: apps/v1
kind: Deployment
metadata:
  name: secure-deployment
  namespace: secure-app
spec:
  replicas: 2
  selector:
    matchLabels:
      app: secure-app
  template:
    metadata:
      labels:
        app: secure-app
    spec:
      securityContext:
        runAsNonRoot: true
        runAsUser: 1000
        fsGroup: 2000
      containers:
      - name: app
        image: nginx:1.25-alpine
        ports:
        - containerPort: 8080
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
            add:
            - NET_BIND_SERVICE
        resources:
          requests:
            memory: "64Mi"
            cpu: "50m"
          limits:
            memory: "128Mi"
            cpu: "200m"
        volumeMounts:
        - name: cache
          mountPath: /var/cache/nginx
        - name: run
          mountPath: /var/run
        - name: tmp
          mountPath: /tmp
      volumes:
      - name: cache
        emptyDir: {}
      - name: run
        emptyDir: {}
      - name: tmp
        emptyDir: {}

---
# 4. Pod Disruption Budget (선택사항이지만 권장)
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: secure-app-pdb
  namespace: secure-app
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: secure-app

적용 및 확인

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
# 적용
kubectl apply -f pod-security-example.yaml

# Namespace 레이블 확인
kubectl get namespace secure-app --show-labels

# Pod 보안 설정 확인
kubectl get pod secure-app -n secure-app -o yaml | grep -A 10 securityContext

# 제한된 권한으로 실행되는지 확인
kubectl exec -it secure-app -n secure-app -- id
# uid=1000 gid=2000 groups=2000
# → root가 아님 ✓

# 루트 FS가 읽기 전용인지 테스트
kubectl exec -it <pod-name> -n secure-app -- touch /test.txt
# Read-only file system (에러) ✓

# Capability 확인
kubectl exec -it <pod-name> -n secure-app -- grep Cap /proc/1/status
# CapEff: 0000000000000000  (모두 제거됨) ✓

4. 실전 보안 체크리스트

클러스터 레벨

  • RBAC 활성화됨 (--authorization-mode=RBAC)
  • API 서버 인증 (TLS 설정)
  • 암호화 저장 (etcd 암호화)
  • 감사 로깅 (API 호출 기록)

네임스페이스 레벨

  • 네트워크 정책 적용
  • Pod 보안 표준 설정
  • Resource Quota 설정
  • Network Policies 테스트됨

Pod 레벨

  • non-root 사용자로 실행
  • readOnlyRootFilesystem: true
  • allowPrivilegeEscalation: false
  • capability drop: ALL
  • resource limits 설정

5. 실제 적용 시나리오

다중 팀 환경 RBAC

 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
# 팀 A: 앱 배포만
apiVersion: v1
kind: ServiceAccount
metadata:
  name: team-a
  namespace: team-a-ns

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: deployer
  namespace: team-a-ns
rules:
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list", "create", "update", "patch"]
- apiGroups: [""]
  resources: ["services"]
  verbs: ["get", "list"]

---
# 팀 B: 로그만 조회 (read-only)
apiVersion: v1
kind: ServiceAccount
metadata:
  name: team-b
  namespace: team-b-ns

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: viewer
  namespace: team-b-ns
rules:
- apiGroups: [""]
  resources: ["pods", "pods/log"]
  verbs: ["get", "list"]
- apiGroups: ["apps"]
  resources: ["deployments"]
  verbs: ["get", "list"]

마무리

K8s 보안은 “한 번 설정하고 끝"이 아닙니다. 지속적으로 모니터링하고 업데이트해야 합니다.

다음 Part 9에서 다룰 예정인 것들:

  • 패스워드 관리 (Secrets)
  • 이미지 보안 (레지스트리, 스캔)
  • 감시 및 모니터링
  • 백업 및 재해 복구

이 글이 도움이 되었으면 좋겠습니다. 실전에서 만나는 문제가 있으면 언제든 물어봐주세요!