Table of contents
- 기본값은 왜 위험한가
- 인증과 인가의 차이
- ServiceAccount — Pod의 신분증
- Role vs ClusterRole
- RoleBinding — 주체와 권한을 묶다
- ClusterRoleBinding — 전역 권한
- 실전 패턴 — CI/CD가 쓸 SA 만들기
- NetworkPolicy — Pod 간 통신 제어
- Pod Security Standards — 컨테이너 실행 정책
- 이미지 보안
- 체크리스트로 정리
기본값은 왜 위험한가
쿠버네티스를 처음 세팅하면 모든 것이 “일단 돌아가게” 설정되어 있다. 네임스페이스 안의 Pod들은 서로 자유롭게 통신하고, Pod 안의 프로세스는 root로 돌고, ServiceAccount는 기본값이 붙어있다. 공부할 땐 편하지만, 실제 운영에 그대로 놔두면 위험하다.
한 서비스가 뚫리면 클러스터 전체로 번지는 “lateral movement” 공격이 쉬워진다. 컨테이너 탈출 취약점이 있으면 호스트 노드까지 장악당할 수 있다. 누군가 kubectl 권한을 얻으면 기본 설정에선 뭐든 할 수 있다.
쿠버네티스 보안의 기본은 최소 권한(Least Privilege)이다. “이 주체는 이 리소스에 이 동작만 할 수 있다”고 좁혀두는 것. 이번 편에서는 그 좁히는 도구들을 살펴본다.
flowchart LR
A[주체<br/>User / SA] -->|어떤 권한?| B[RBAC]
C[Pod<br/>네트워크 통신] -->|어디까지 허용?| D[NetworkPolicy]
E[컨테이너<br/>실행 환경] -->|어떻게 제한?| F[Pod Security]
세 축이다. RBAC은 누가 뭘 할 수 있는지, NetworkPolicy는 누가 누구와 통신할 수 있는지, Pod Security Standards는 컨테이너가 어떤 환경에서 실행되는지를 통제한다.
인증과 인가의 차이
먼저 용어를 정리하고 가자.
- 인증(Authentication): “너 누구냐?” — 주체가 누구인지 확인
- 인가(Authorization): “뭘 할 수 있는데?” — 그 주체에게 권한이 있는지 확인
쿠버네티스에는 두 종류의 주체가 있다. 사용자(User)와 ServiceAccount(SA)다. 사람은 kubeconfig나 OIDC로 인증하고, Pod 내부의 프로세스는 ServiceAccount로 인증한다.
API 서버는 요청이 들어올 때마다 이 순서를 밟는다.
sequenceDiagram
participant C as 클라이언트
participant A as API 서버
participant Au as Authenticator
participant Az as Authorizer
participant Ad as Admission
participant E as etcd
C->>A: 요청 (토큰/인증서)
A->>Au: 누구냐?
Au-->>A: user=alice, groups=[dev]
A->>Az: alice가 이 작업 가능?
Az-->>A: 허용/거부
A->>Ad: 리소스 스펙 검증/변형
Ad-->>A: OK
A->>E: 저장
E-->>A: OK
A-->>C: 응답
인가 단계에서 쓰이는 메커니즘이 여러 개 있지만, 실무에서는 거의 RBAC(Role-Based Access Control, 역할 기반 접근 제어)이다. 사용자/ServiceAccount를 역할(Role)에 묶고, 역할에 리소스에 대한 권한을 부여하는 방식이다. “누가(subject) 어떤 리소스에(resource) 어떤 동작(verb)을 할 수 있는가”를 선언적으로 정의한다.
ServiceAccount — Pod의 신분증
Pod에 serviceAccountName을 지정하지 않으면 네임스페이스의 default SA가 자동으로 붙는다. 이 SA의 토큰이 /var/run/secrets/kubernetes.io/serviceaccount/token에 마운트되어, Pod 내부에서 API 서버와 통신할 때 쓰인다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: app-sa
namespace: backend
automountServiceAccountToken: true
애플리케이션이 쿠버네티스 API를 직접 호출할 일이 없다면, 토큰 자동 마운트를 꺼두는 게 좋다.
apiVersion: v1
kind: Pod
metadata:
name: app
spec:
serviceAccountName: app-sa
automountServiceAccountToken: false # 필요 없으면 끈다
containers:
- name: app
image: myapp:1.0
왜 꺼야 할까? 컨테이너가 뚫렸을 때 공격자가 API 서버를 호출할 수 있는 티켓이 그대로 손에 들어가기 때문이다. 실제로 앱이 쿠버네티스 API를 쓸 일이 없는 일반 웹서버라면 토큰을 마운트할 이유가 없다.
Role vs ClusterRole
RBAC에서 권한을 정의하는 리소스가 Role과 ClusterRole이다. 둘의 차이는 범위다.
- Role: 특정 네임스페이스 안의 리소스에 대한 권한
- ClusterRole: 클러스터 전역 리소스(Node, PV, CRD 정의 등)에 대한 권한
네임스페이스 내 Pod에 읽기 권한만 주는 Role은 이렇게 쓴다.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: backend
rules:
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
세 가지 키워드를 이해하면 된다.
- apiGroups: 리소스가 속한 API 그룹. 코어는
"", Deployment는"apps", Ingress는"networking.k8s.io" - resources: 리소스 종류.
pods,pods/log(로그 조회),pods/exec(명령 실행) - verbs: 허용 동작.
get,list,watch,create,update,patch,delete
전체 동작을 보려면 쿠버네티스 문서나 kubectl api-resources -o wide로 확인할 수 있다.
RoleBinding — 주체와 권한을 묶다
Role은 “이런 권한이 있다”만 정의한다. 실제로 누구에게 줄지 연결해주는 게 RoleBinding이다.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: app-sa-pod-reader
namespace: backend
subjects:
- kind: ServiceAccount
name: app-sa
namespace: backend
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
이제 app-sa는 backend 네임스페이스의 Pod를 get/list/watch할 수 있다. 생성도 삭제도 할 수 없다. 정확히 그 두 동사만 가능하다.
RoleBinding은 ClusterRole도 참조할 수 있다. “이 ClusterRole에 정의된 권한을, 이 네임스페이스 범위에서만 쓰게 해줘”라는 조합이다. 자주 쓰는 view, edit, admin 같은 기본 ClusterRole을 특정 네임스페이스에서만 부여할 때 쓴다.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: alice-edit-on-backend
namespace: backend
subjects:
- kind: User
name: alice@example.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole # ClusterRole을 참조하지만
name: edit # backend 네임스페이스 범위로만 적용됨
apiGroup: rbac.authorization.k8s.io
ClusterRoleBinding — 전역 권한
ClusterRoleBinding은 클러스터 전체에서 작동하는 바인딩이다. 신중하게 써야 한다.
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: platform-admins
subjects:
- kind: Group
name: platform-team
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
cluster-admin은 모든 리소스에 모든 동작이 가능한 슈퍼유저다. 이걸 개인에게 함부로 주면 안 된다. kubectl auth can-i --list로 현재 사용자가 뭘 할 수 있는지 확인할 수 있다.
# 내 권한 확인
kubectl auth can-i --list
# 특정 동작이 가능한지 확인
kubectl auth can-i delete pods -n production
kubectl auth can-i '*' '*' --as=system:serviceaccount:backend:app-sa
실전 패턴 — CI/CD가 쓸 SA 만들기
실전에서 가장 자주 만드는 조합은 배포 파이프라인용 SA다. “이 SA는 이 네임스페이스의 Deployment만 업데이트할 수 있다”는 수준으로 좁힌다.
apiVersion: v1
kind: ServiceAccount
metadata:
name: deployer
namespace: backend
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deployer-role
namespace: backend
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch", "update"]
- apiGroups: [""]
resources: ["configmaps", "services"]
verbs: ["get", "list", "watch", "update"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"] # 로그 조회용
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: deployer-binding
namespace: backend
subjects:
- kind: ServiceAccount
name: deployer
namespace: backend
roleRef:
kind: Role
name: deployer-role
apiGroup: rbac.authorization.k8s.io
이 SA의 토큰을 CI 시스템에 등록해두면, 그 파이프라인은 정확히 이 네임스페이스의 Deployment만 업데이트할 수 있다. Secret 조회도, 다른 네임스페이스 접근도 불가능하다. 만약 CI 크레덴셜이 유출되어도 피해 범위가 이 네임스페이스 안으로 제한된다.
NetworkPolicy — Pod 간 통신 제어
기본 설정의 쿠버네티스는 모든 Pod가 서로 통신할 수 있다. 다른 네임스페이스의 Pod에도, 클러스터 외부로도 다 열려 있다. 이건 편하지만, 침해 사고가 터지면 공격자가 옆 Pod로 쉽게 옮겨간다.
NetworkPolicy는 이걸 화이트리스트 방식으로 좁힌다. “이 라벨이 붙은 Pod는 이 라벨이 붙은 Pod에서 오는 트래픽만 받는다” 식이다.
시작은 기본 거부 정책이다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: backend
spec:
podSelector: {} # 네임스페이스의 모든 Pod에 적용
policyTypes:
- Ingress
- Egress
이 정책이 걸린 네임스페이스는 외부에서도 안으로 들어올 수 없고, 안에서도 밖으로 나갈 수 없다. 완전히 격리된 상태가 된다. 여기서부터 필요한 만큼만 열어준다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-frontend-to-api
namespace: backend
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
name: frontend
podSelector:
matchLabels:
app: web
ports:
- protocol: TCP
port: 8080
backend 네임스페이스의 app=api Pod는, frontend 네임스페이스의 app=web Pod에서 오는 8080 트래픽만 받는다. 다른 모든 인바운드는 막힌다.
flowchart LR
A[frontend ns<br/>app=web] -->|8080 OK| B[backend ns<br/>app=api]
C[frontend ns<br/>app=admin] -.->|차단| B
D[other ns] -.->|차단| B
Egress 제어
아웃바운드를 막는 것도 중요하다. 외부 DNS 서버로 데이터를 유출하는 공격을 막거나, 내부 서비스만 호출하도록 강제할 수 있다.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: api-egress
namespace: backend
spec:
podSelector:
matchLabels:
app: api
policyTypes:
- Egress
egress:
# DNS는 허용
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
# DB 네임스페이스의 PostgreSQL만 허용
- to:
- namespaceSelector:
matchLabels:
name: database
podSelector:
matchLabels:
app: postgres
ports:
- protocol: TCP
port: 5432
api Pod는 DNS 조회와 PostgreSQL 접근만 가능하다. 외부 인터넷은 차단되고, 다른 내부 서비스도 막힌다.
중요: NetworkPolicy는 CNI 플러그인이 지원해야 동작한다. Calico, Cilium, Weave Net 등은 지원하지만, Flannel 기본 설정은 지원하지 않는다. 클러스터의 CNI를 먼저 확인하자.
Pod Security Standards — 컨테이너 실행 정책
컨테이너 하나가 root로 돌면서 호스트 파일시스템을 마운트하면, 그 컨테이너를 뚫은 공격자는 노드 전체를 손에 넣는다. 이런 위험한 실행을 막는 게 Pod Security Standards(PSS)다.
예전에 쓰던 PodSecurityPolicy(PSP)가 v1.25에서 제거되고, 그 자리를 PSS와 Pod Security Admission이 대체했다.
세 가지 표준 프로파일이 있다.
| 프로파일 | 설명 |
|---|---|
privileged | 제한 없음. 클러스터 인프라 Pod용 |
baseline | 알려진 권한 상승 벡터만 차단. 일반 애플리케이션의 최소선 |
restricted | 모범 사례 수준. 엔드유저 워크로드 권장 |
네임스페이스에 라벨을 붙여서 적용한다.
apiVersion: v1
kind: Namespace
metadata:
name: backend
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/audit: restricted
enforce는 위반 시 Pod 생성을 거부한다. warn은 경고만 띄우고, audit은 감사 로그를 남긴다. 갑자기 restricted를 enforce로 켜면 기존 Pod들이 거부될 수 있으니, 먼저 warn으로 켜서 충격을 흡수하고 점진적으로 올리는 게 안전하다.
restricted 프로파일이 요구하는 것들
restricted 수준을 맞추려면 Pod 스펙에 이런 설정들이 들어가야 한다.
apiVersion: v1
kind: Pod
metadata:
name: secure-app
spec:
securityContext:
runAsNonRoot: true # root가 아닌 UID로 실행
runAsUser: 1000
runAsGroup: 3000
fsGroup: 2000
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:1.0
securityContext:
allowPrivilegeEscalation: false # setuid 금지
capabilities:
drop:
- ALL # 모든 리눅스 capability 제거
readOnlyRootFilesystem: true # 루트 fs를 읽기 전용으로
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
중요한 포인트들을 짚어보면 다음과 같다.
- non-root 실행: 컨테이너 이미지가 root 사용자로 설정되어 있으면 이 Pod는 뜨지 않는다. Dockerfile에
USER 1000같은 지시어를 추가해야 한다 - 모든 capability 제거: NET_RAW, SYS_ADMIN 같은 리눅스 권한을 전부 벗긴다
- 읽기 전용 루트: 애플리케이션이 루트 파일시스템에 쓸 수 없게 한다. 쓰기가 필요한 곳은 emptyDir을 마운트해서 해결
- seccomp: 시스템 콜을 제한한다.
RuntimeDefault는 런타임의 기본 프로파일 사용
이미지 보안
마지막으로 이미지 자체의 보안도 챙겨야 한다. 코드는 아무리 잘 써도, 베이스 이미지에 CVE가 있으면 취약점이 고스란히 들어간다.
실무에서 권장되는 프랙티스 몇 가지.
- 베이스 이미지 최소화:
alpine,distroless처럼 최소한만 담긴 이미지를 쓴다 - 이미지 스캐닝: Trivy, Grype 같은 도구로 CI 단계에서 CVE 체크
- 서명된 이미지 사용: Cosign, Notation으로 서명하고, Admission Controller(예: Kyverno, Sigstore Policy Controller)로 서명되지 않은 이미지 차단
- 레지스트리 제한: 공식 레지스트리(docker.io, gcr.io, quay.io 등) 외에서 이미지를 못 가져오게 정책 설정
# Kyverno로 서명되지 않은 이미지 차단 (예시)
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: verify-images
spec:
validationFailureAction: Enforce
rules:
- name: check-signature
match:
any:
- resources:
kinds: ["Pod"]
verifyImages:
- imageReferences:
- "registry.company.com/*"
attestors:
- entries:
- keys:
publicKeys: |-
-----BEGIN PUBLIC KEY-----
...
-----END PUBLIC KEY-----
체크리스트로 정리
새 네임스페이스를 만들 때 이 순서로 훑어보면 웬만한 건 챙겨진다.
- 전용 ServiceAccount를 만들고 Pod에 지정한다.
defaultSA는 쓰지 않는다 - 토큰 자동 마운트를 끈다. API를 정말 쓰는 Pod만 켠다
- Role과 RoleBinding으로 필요한 최소 권한만 부여한다
- NetworkPolicy
default-deny를 걸고, 필요한 통신만 화이트리스트로 연다 - Pod Security Standards를
restricted enforce로 적용한다 - 이미지 스캐닝과 서명 검증을 CI/CD 파이프라인에 넣는다
- Secret은 외부 시크릿 매니저(7편 참고)와 연동한다
처음 도입할 때는 “굳이 이렇게까지?” 싶어도, 한 번 침해 사고를 겪고 나면 기본값이 얼마나 위험했는지 실감한다. 초기에 조금 귀찮더라도 단단하게 묶어두는 게 장기적으로 훨씬 편하다.
다음 편에서는 운영 관점에서 가장 중요한 관측성을 다룬다. 로그는 어떻게 수집하고, 메트릭은 어떻게 모으고, 분산 추적은 어떻게 붙이는지, 그리고 kubectl로 디버깅할 때 쓰는 필수 명령어들을 정리한다.




Loading comments...