파드가 k8s API Server에 요청을 보낼 때 — ConfigMap을 읽거나, 다른 파드 목록을 조회하거나, Deployment를 수정하는 등 — 그 요청이 허용되는지 판단하려면 “이 요청을 보낸 게 누구인가"를 알아야 한다. 사람에게 사용자 계정이 있듯, 파드에는 ServiceAccount가 있다.
ServiceAccount가 필요한 이유
많은 서비스는 API Server를 직접 호출하지 않는다. 하지만 다음 경우에는 반드시 필요하다.
- CI/CD 파이프라인이 Deployment를 업데이트한다
- Operator나 컨트롤러가 파드나 서비스를 감시하고 수정한다
- 앱이 자신이 실행 중인 노드 정보를 조회한다
- Prometheus가 파드의 메트릭 엔드포인트 목록을 k8s API에서 가져온다
- Vault Agent가 API Server에서 파드의 신원을 검증한다
파드에 ServiceAccount를 명시하지 않으면 네임스페이스의 default ServiceAccount가 자동 할당된다. 이 기본 ServiceAccount는 권한이 없으므로 대부분의 API 호출이 차단된다.
ServiceAccount 토큰
파드가 시작되면 ServiceAccount에 연결된 JWT 토큰이 파드 안의 고정 경로(/var/run/secrets/kubernetes.io/serviceaccount/token)에 자동 마운트된다. API Server에 요청할 때 이 토큰을 Authorization: Bearer <token> 헤더에 담아 보내면 API Server가 신원을 확인한다.
파드 → API Server 요청 → API Server가 토큰 검증
↓
RBAC: 이 ServiceAccount에
이 작업이 허용돼 있나?
↓
허용 → 응답 / 거부 → 403
RBAC — 역할 기반 접근 제어
RBAC(Role-Based Access Control)은 “누가 어떤 리소스에 어떤 작업을 할 수 있는가"를 정의하는 체계다. 네 가지 오브젝트로 구성된다.
Role: 특정 네임스페이스 안에서 허용할 권한을 정의한다.
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: production
rules:
- apiGroups: [""] # "" = core API 그룹
resources: ["pods"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get"]
verbs는 허용할 동작이다. get, list, watch, create, update, patch, delete가 있다. "*"는 전체 허용.
ClusterRole: Role과 같지만 특정 네임스페이스가 아니라 클러스터 전체 또는 네임스페이스가 없는 리소스(Node, PersistentVolume 등)에 대한 권한을 정의한다.
RoleBinding: Role을 특정 대상(ServiceAccount, 사용자, 그룹)에 연결한다. “이 네임스페이스에서 이 Role을 이 ServiceAccount에게 준다"는 것이다.
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: read-pods
namespace: production
subjects:
- kind: ServiceAccount
name: my-app
namespace: production
roleRef:
kind: Role
name: pod-reader
apiGroup: rbac.authorization.k8s.io
ClusterRoleBinding: ClusterRole을 클러스터 전체 범위로 대상에 연결한다.
전체 구성 예시
# 1. ServiceAccount 생성
apiVersion: v1
kind: ServiceAccount
metadata:
name: my-app
namespace: production
---
# 2. 필요한 권한을 Role로 정의
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: my-app-role
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
- apiGroups: [""]
resources: ["secrets"]
resourceNames: ["my-app-secret"] # 특정 Secret만 접근 허용
verbs: ["get"]
---
# 3. Role을 ServiceAccount에 연결
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: my-app-rolebinding
namespace: production
subjects:
- kind: ServiceAccount
name: my-app
namespace: production
roleRef:
kind: Role
name: my-app-role
apiGroup: rbac.authorization.k8s.io
---
# 4. Deployment에서 ServiceAccount 지정
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
serviceAccountName: my-app # 이 파드는 my-app ServiceAccount로 실행된다
containers:
- name: app
image: my-app:1.0.0
최소 권한 원칙
RBAC 설계의 핵심 원칙은 최소 권한(least privilege) 이다. 파드가 실제로 필요한 것만 정확히 허용하고, 나머지는 차단한다.
흔한 실수는 편의를 위해 cluster-admin 권한을 주거나, "*" 와일드카드로 모든 리소스에 모든 권한을 주는 것이다. 그 파드가 침해되면 공격자가 클러스터 전체를 제어할 수 있다.
resourceNames로 특정 오브젝트만 지정할 수도 있다. “이 파드는 이 Secret 하나만 읽을 수 있다"처럼 리소스 종류뿐 아니라 특정 인스턴스까지 제한하는 것이 더 엄격한 권한 제어다.
automountServiceAccountToken 비활성화
API Server를 전혀 호출하지 않는 파드라면 토큰 마운트 자체를 꺼두는 것이 좋다.
spec:
automountServiceAccountToken: false
토큰이 없으면 파드가 침해되어도 공격자가 API Server에 접근할 수 없다. Deployment의 spec.template.spec에 설정하면 해당 파드에 적용되고, ServiceAccount에 설정하면 그 계정을 쓰는 모든 파드에 적용된다.
트레이드오프
RBAC을 제대로 설정하면 파드 침해의 폭발 반경(blast radius)을 줄일 수 있다. 반면 서비스마다 ServiceAccount와 Role과 RoleBinding을 만들고 관리하는 것은 번거롭다. 서비스가 많아질수록 이 오브젝트들이 쌓여 관리 부담이 된다.
현실적인 전략은 최소한 두 가지를 지키는 것이다. 첫째, API Server를 호출하지 않는 파드는 automountServiceAccountToken: false. 둘째, API Server를 호출하는 파드는 default ServiceAccount가 아닌 전용 ServiceAccount를 만들고, 필요한 권한만 정확히 정의한다. 이 두 가지만 지켜도 기본 보안 수준이 크게 올라간다.