CI/CD 파이프라인이 운영 배포 중 403 Forbidden을 내며 멈췄습니다. 반대로 권한을 넓게 주면 실수로 모든 Namespace를 수정할 수 있는 위험이 생깁니다. RBAC은 Kubernetes API 접근을 필요한 만큼만 허용하는 운영 보안의 기본입니다.
CI/CD 파이프라인에서 새벽 3시에 배포가 실패했습니다. 로그를 보니 Error from server (Forbidden): deployments.apps is forbidden: User "system:serviceaccount:ci:ci-sa" cannot list resource "deployments" in API group "apps" in the namespace "production". 파이프라인을 고치기 위해 급하게 cluster-admin ClusterRole을 바인딩했습니다. 배포는 성공했지만, 이제 CI 봇이 클러스터의 모든 것을 할 수 있게 되었습니다. 보안 감사에서 이 설정이 발견됐고, 즉각 수정 요청이 들어왔습니다.
Kubernetes RBAC(Role-Based Access Control)은 "누가(Subject) 어떤 리소스에(Resource) 무엇을(Verb) 할 수 있는가"를 제어합니다. 올바르게 설계된 RBAC는 사고가 발생했을 때 피해 범위를 제한하는 가장 효과적인 방어선입니다. CI 봇은 자신이 배포하는 네임스페이스의 Deployment만 수정할 수 있어야 하고, 로그 수집 에이전트는 pods를 읽을 수만 있어야 합니다. 이 원칙을 최소 권한(Principle of Least Privilege)이라고 합니다.
- 1RBAC 핵심 4종: Role, ClusterRole, RoleBinding, ClusterRoleBinding
- 2ServiceAccount: 파드에 부여하는 K8s ID
- 3최소 권한 원칙으로 권한 설계
- 4kubectl auth can-i로 권한 시뮬레이션
- 5403 Forbidden 디버깅 워크플로
- 6CI/CD ServiceAccount 권한 설계 실전
kubectl auth can-i '*' '*' --all-namespaceskubectl create namespace rbac-demo && kubectl create namespace productionkubectl get clusterroles | grep -v system | head -10kubectl get clusterrolebindings | grep -v system | head -10RBAC 핵심 개념: 4가지 리소스
CI/CD 봇이 새벽에 배포 실패 알림을 보냅니다. 로그를 보니 403 Forbidden입니다. 급하게 cluster-admin을 부여하면 배포는 되지만 보안 감사에서 지적을 받게 됩니다. 반대로 너무 좁게 주면 다음 배포에서 또 같은 오류가 납니다. RBAC는 "누가 어떤 리소스에 무엇을 할 수 있는가"를 명시적으로 선언하는 시스템입니다. Role과 ClusterRole은 권한의 내용을 정의하고, RoleBinding과 ClusterRoleBinding은 그 권한을 특정 주체에게 연결합니다. 이 네 가지 리소스의 관계를 이해하면 최소 권한 원칙을 정확하게 설계할 수 있습니다.
RBAC는 권한 정의(Role/ClusterRole)와 권한 부여(RoleBinding/ClusterRoleBinding)로 나뉩니다.

Subject (누가) Binding (연결) Role (무엇을)
───────────────── ───────────────── ──────────────────
User RoleBinding Role (네임스페이스)
Group ────────► ClusterRoleBinding ────► ClusterRole (클러스터 전체)
ServiceAccount
Verb (동작) 종류:
get,list,watch— 읽기create,update,patch— 쓰기delete,deletecollection— 삭제*— 모든 동작
Resource 예시:
- 네임스페이스 리소스:
pods,deployments,services,configmaps,secrets - 클러스터 리소스:
nodes,persistentvolumes,namespaces,clusterroles
# Role 예시: 특정 네임스페이스의 pods 읽기만 허용
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: pod-reader
namespace: rbac-demo # 이 네임스페이스에서만 유효
rules:
- apiGroups: [""] # "" = core API group (pods, services, configmaps 등)
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
# ClusterRole 예시: 모든 네임스페이스의 노드 정보 읽기
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: node-reader
rules:
- apiGroups: [""]
resources: ["nodes"]
verbs: ["get", "list", "watch"]
- apiGroups: ["metrics.k8s.io"]
resources: ["nodes", "pods"]
verbs: ["get", "list"]
# 빌트인 ClusterRole 확인 (재사용 권장)
kubectl describe clusterrole view # 읽기 전용
kubectl describe clusterrole edit # 읽기/쓰기 (secrets 제외)
kubectl describe clusterrole admin # 네임스페이스 전체 관리
kubectl describe clusterrole cluster-admin # 클러스터 전체 관리
ServiceAccount와 RoleBinding: 파드에 권한 부여
파드 안에서 실행되는 배포 자동화 도구나 모니터링 에이전트는 Kubernetes API를 직접 호출해야 하는 경우가 있습니다. 그런데 이 파드에 사람 계정의 자격증명을 넣으면 보안 사고 시 피해 범위가 클러스터 전체로 확대됩니다. ServiceAccount는 파드 전용 K8s 내부 ID입니다. 필요한 권한만 Role로 정의하고 ServiceAccount에 바인딩하면, 그 파드는 설계된 동작만 수행할 수 있고 나머지는 모두 거부됩니다. 인프라를 변경해도 파드를 재배포할 필요 없이 바인딩만 수정하면 됩니다.
ServiceAccount는 파드(애플리케이션)에 할당되는 Kubernetes 내부 ID입니다. RoleBinding으로 ServiceAccount에 Role을 연결합니다.

# 1. ServiceAccount 생성
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci-sa
namespace: ci
---
# 2. CI가 production 네임스페이스의 Deployment를 관리할 Role
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: deploy-manager
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments", "replicasets"]
verbs: ["get", "list", "watch", "create", "update", "patch"]
- apiGroups: [""]
resources: ["pods"]
verbs: ["get", "list", "watch"]
---
# 3. ci 네임스페이스의 ci-sa에 production의 deploy-manager Role 부여
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deploy-binding
namespace: production # 이 네임스페이스에서 권한 적용
subjects:
- kind: ServiceAccount
name: ci-sa
namespace: ci # ServiceAccount가 있는 네임스페이스
roleRef:
kind: Role
name: deploy-manager
apiGroup: rbac.authorization.k8s.io
운영 리소스 직접 패치
안전한 실행 조건: 변경 내용을 코드에 반영할 계획이 있고 영향 범위를 검토했을 때만 실행하세요.
실행 전 반드시 확인
- 현재 컨텍스트와 Namespace가 의도한 대상인지 확인했는가
- 운영 트래픽이나 상태 저장 데이터에 미치는 영향을 확인했는가
- 되돌릴 매니페스트, 백업, 또는 복구 절차가 준비되어 있는가
kubectl patch deployment my-app -n production위 항목을 모두 확인한 후 복사할 수 있습니다
kubectl apply -f ci-rbac.yaml
# 파드에 ServiceAccount 지정
# (지정 안 하면 네임스페이스의 default ServiceAccount 사용)
kubectl patch deployment my-app -n production \
--type='json' \
-p='[{"op":"add","path":"/spec/template/spec/serviceAccountName","value":"ci-sa"}]'
kubectl auth can-i: 권한 시뮬레이션
403 Forbidden이 발생했을 때, 또는 권한을 배포 전에 검증하고 싶을 때 사용합니다.
# 현재 사용자 권한 확인
kubectl auth can-i list pods -n production
# yes
# 다른 주체로 가장해서 확인 (--as)
kubectl auth can-i list pods \
--as=system:serviceaccount:ci:ci-sa \
-n production
# no ← 권한 없음
# 어떤 권한이 있는지 전체 확인
kubectl auth can-i --list \
--as=system:serviceaccount:ci:ci-sa \
-n production
# Resources Non-Resource URLs Resource Names Verbs
# deployments.apps [] [] [get list watch update patch]
# pods [] [] [get list watch]
# 특정 동작 확인
kubectl auth can-i create deployments \
--as=system:serviceaccount:ci:ci-sa \
-n production
# yes
kubectl auth can-i delete nodes \
--as=system:serviceaccount:ci:ci-sa
# no ← 클러스터 레벨 권한 없음
실습: 읽기 전용 사용자 설정
# 읽기 전용 ServiceAccount 생성
kubectl create serviceaccount readonly-user -n rbac-demo
# 빌트인 view ClusterRole을 RoleBinding으로 연결 (네임스페이스 범위)
kubectl create rolebinding readonly-binding \
-n rbac-demo \
--clusterrole=view \
--serviceaccount=rbac-demo:readonly-user
# 권한 검증
kubectl auth can-i list pods \
--as=system:serviceaccount:rbac-demo:readonly-user \
-n rbac-demo
# yes
kubectl auth can-i delete pods \
--as=system:serviceaccount:rbac-demo:readonly-user \
-n rbac-demo
# no ← 삭제 권한 없음
kubectl auth can-i list pods \
--as=system:serviceaccount:rbac-demo:readonly-user \
-n production
# no ← 다른 네임스페이스 접근 불가
Jenkins/GitHub Actions 파이프라인이 쿠버네티스 배포 단계에서 실패합니다. 로그에 Error from server (Forbidden): deployments.apps is forbidden이 출력됩니다.
# 1단계: 에러 메시지에서 주체(Subject)와 동작 파악
# "User "system:serviceaccount:ci:jenkins-sa" cannot update resource
# "deployments" in API group "apps" in the namespace "production""
# → 주체: ci 네임스페이스의 jenkins-sa ServiceAccount
# → 동작: production 네임스페이스의 deployments 업데이트
# 2단계: ServiceAccount 존재 여부 확인
kubectl get serviceaccount jenkins-sa -n ci
# Error from server (NotFound) ← SA 자체가 없음!
# 또는
# NAME SECRETS AGE
# jenkins-sa 0 5m
# 2-1: SA가 없다면 생성
kubectl create serviceaccount jenkins-sa -n ci
# 3단계: 현재 바인딩된 권한 확인
kubectl get rolebinding,clusterrolebinding -A \
| grep jenkins-sa
# (아무 출력 없음 — 바인딩이 없음)
# 4단계: 필요한 최소 권한 파악 후 Role 생성
cat <<EOF | kubectl apply -f -
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: jenkins-deployer
namespace: production
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "update", "patch"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: jenkins-deploy-binding
namespace: production
subjects:
- kind: ServiceAccount
name: jenkins-sa
namespace: ci
roleRef:
kind: Role
name: jenkins-deployer
apiGroup: rbac.authorization.k8s.io
EOF
# 5단계: 권한 검증
kubectl auth can-i update deployments \
--as=system:serviceaccount:ci:jenkins-sa \
-n production
# yes ← 이제 가능
kubectl auth can-i delete deployments \
--as=system:serviceaccount:ci:jenkins-sa \
-n production
# no ← 삭제는 불가 (최소 권한 원칙)
# 6단계: Kubeconfig에 ServiceAccount 토큰 설정 (Jenkins)
# K8s 1.24+ 에서는 토큰을 수동으로 생성해야 함
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: Secret
metadata:
name: jenkins-sa-token
namespace: ci
annotations:
kubernetes.io/service-account.name: jenkins-sa
type: kubernetes.io/service-account-token
EOF
TOKEN=$(kubectl get secret jenkins-sa-token -n ci \
-o jsonpath='{.data.token}' | base64 -d)
# Jenkins에 이 TOKEN 값을 Kubernetes credential로 등록
echo "Token: ${TOKEN:0:30}..."
- NAME—조회 대상 리소스 이름이 예상한 대상과 일치하는지 확인합니다.
- STATUS/READY—Running, Ready, Available처럼 정상 상태를 나타내는 필드가 있는지 봅니다.
- RESTARTS/EVENTS—재시작 횟수나 Warning 이벤트가 증가하지 않는지 확인합니다.
예방 패턴: CI/CD 파이프라인 구성 시 ServiceAccount, Role, RoleBinding을 GitOps 방식으로 관리하고, 배포 전에 kubectl auth can-i --list로 권한을 검증하는 단계를 파이프라인에 포함하세요. 절대로 cluster-admin을 임시방편으로 사용하지 마세요.
시나리오: 멀티팀 클러스터에서 팀별 네임스페이스 권한 설계
스타트업이 성장해 팀이 세 개(frontend, backend, data)로 분리됐습니다. 각 팀은 자신의 네임스페이스만 관리할 수 있어야 하고, 서로의 작업을 방해할 수 없어야 합니다.
# 1단계: 팀별 네임스페이스 생성
for team in frontend backend data; do
kubectl create namespace $team
done
# 2단계: 팀별 ServiceAccount 생성
for team in frontend backend data; do
kubectl create serviceaccount "${team}-admin" -n $team
done
# 3단계: 각 팀에 자신의 네임스페이스 admin 권한 부여
# 빌트인 admin ClusterRole을 각 네임스페이스 내에서만 적용
for team in frontend backend data; do
kubectl create rolebinding "${team}-admin-binding" \
-n $team \
--clusterrole=admin \
--serviceaccount="${team}:${team}-admin"
done
# 4단계: 팀 간 격리 검증
kubectl auth can-i create deployments \
--as=system:serviceaccount:frontend:frontend-admin \
-n frontend
# yes ← 자신의 네임스페이스
kubectl auth can-i create deployments \
--as=system:serviceaccount:frontend:frontend-admin \
-n backend
# no ← 다른 팀 네임스페이스 접근 불가
# 5단계: 공통 읽기 권한 (팀 간 파드 상태 공유 필요 시)
# 모든 팀이 다른 팀의 파드를 읽기 전용으로 볼 수 있도록
for team in frontend backend data; do
for target in frontend backend data; do
if [ "$team" != "$target" ]; then
kubectl create rolebinding "${team}-view-${target}" \
-n $target \
--clusterrole=view \
--serviceaccount="${team}:${team}-admin"
fi
done
done
# 6단계: ResourceQuota로 팀별 리소스 제한
for team in frontend backend data; do
cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ResourceQuota
metadata:
name: team-quota
namespace: $team
spec:
hard:
pods: "20"
requests.cpu: "4"
requests.memory: "8Gi"
limits.cpu: "8"
limits.memory: "16Gi"
EOF
done
실무 포인트: 직접 Role을 만드는 것보다 빌트인 ClusterRole(view, edit, admin)을 RoleBinding으로 네임스페이스 범위에서 사용하는 것이 관리가 간편합니다. 새 팀원 온보딩, 서비스 계정 추가 등 반복 작업은 스크립트화해두면 휴먼 에러를 줄일 수 있습니다.
핵심 요약
| 리소스 | 범위 | 용도 |
|---|---|---|
| Role | 네임스페이스 | 특정 네임스페이스 리소스 권한 정의 |
| ClusterRole | 클러스터 전체 | 모든 네임스페이스 또는 클러스터 리소스 권한 정의 |
| RoleBinding | 네임스페이스 | Subject에 Role/ClusterRole 연결 (네임스페이스 내) |
| ClusterRoleBinding | 클러스터 전체 | Subject에 ClusterRole 연결 (전체 범위) |
| 디버깅 명령어 | 용도 |
|---|---|
kubectl auth can-i <verb> <resource> -n <ns> | 현재 사용자 권한 확인 |
kubectl auth can-i --list --as=<subject> -n <ns> | 특정 주체 전체 권한 나열 |
kubectl get rolebinding,clusterrolebinding -A | grep <name> | 바인딩 찾기 |
kubectl describe rolebinding <name> -n <ns> | 바인딩 상세 확인 |