Skip to content
ioob.dev
Go back

Kubernetes 입문 10편 — RBAC과 보안: 최소 권한의 원칙

· 10분 읽기
Kubernetes 시리즈 (10/12)
  1. Kubernetes 입문 1편 — Kubernetes란
  2. Kubernetes 입문 2편 — 클러스터 구조
  3. Kubernetes 입문 3편 — Pod
  4. Kubernetes 입문 4편 — 컨트롤러
  5. Kubernetes 입문 5편 — 서비스와 네트워킹
  6. Kubernetes 입문 6편 — Ingress와 Gateway API
  7. Kubernetes 입문 7편 — ConfigMap과 Secret
  8. Kubernetes 입문 8편 — 스토리지: PV, PVC, StorageClass
  9. Kubernetes 입문 9편 — 리소스 관리와 오토스케일링
  10. Kubernetes 입문 10편 — RBAC과 보안: 최소 권한의 원칙
  11. Kubernetes 입문 11편 — 관측성: 로그, 메트릭, 추적
  12. Kubernetes 입문 12편 — Helm과 패키지 관리
Table of contents

Table of contents

기본값은 왜 위험한가

쿠버네티스를 처음 세팅하면 모든 것이 “일단 돌아가게” 설정되어 있다. 네임스페이스 안의 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는 컨테이너가 어떤 환경에서 실행되는지를 통제한다.

인증과 인가의 차이

먼저 용어를 정리하고 가자.

쿠버네티스에는 두 종류의 주체가 있다. 사용자(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에서 권한을 정의하는 리소스가 RoleClusterRole이다. 둘의 차이는 범위다.

네임스페이스 내 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"]

세 가지 키워드를 이해하면 된다.

전체 동작을 보려면 쿠버네티스 문서나 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-sabackend 네임스페이스의 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: {}

중요한 포인트들을 짚어보면 다음과 같다.

이미지 보안

마지막으로 이미지 자체의 보안도 챙겨야 한다. 코드는 아무리 잘 써도, 베이스 이미지에 CVE가 있으면 취약점이 고스란히 들어간다.

실무에서 권장되는 프랙티스 몇 가지.

# 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-----

체크리스트로 정리

새 네임스페이스를 만들 때 이 순서로 훑어보면 웬만한 건 챙겨진다.

  1. 전용 ServiceAccount를 만들고 Pod에 지정한다. default SA는 쓰지 않는다
  2. 토큰 자동 마운트를 끈다. API를 정말 쓰는 Pod만 켠다
  3. Role과 RoleBinding으로 필요한 최소 권한만 부여한다
  4. NetworkPolicy default-deny를 걸고, 필요한 통신만 화이트리스트로 연다
  5. Pod Security Standardsrestricted enforce로 적용한다
  6. 이미지 스캐닝과 서명 검증을 CI/CD 파이프라인에 넣는다
  7. Secret은 외부 시크릿 매니저(7편 참고)와 연동한다

처음 도입할 때는 “굳이 이렇게까지?” 싶어도, 한 번 침해 사고를 겪고 나면 기본값이 얼마나 위험했는지 실감한다. 초기에 조금 귀찮더라도 단단하게 묶어두는 게 장기적으로 훨씬 편하다.


다음 편에서는 운영 관점에서 가장 중요한 관측성을 다룬다. 로그는 어떻게 수집하고, 메트릭은 어떻게 모으고, 분산 추적은 어떻게 붙이는지, 그리고 kubectl로 디버깅할 때 쓰는 필수 명령어들을 정리한다.

11편: 관측성 — 로그, 메트릭, 추적


Related Posts

Share this post on:

Comments

Loading comments...


Previous Post
Kubernetes 입문 9편 — 리소스 관리와 오토스케일링
Next Post
Kubernetes 입문 11편 — 관측성: 로그, 메트릭, 추적