ServiceAccount가 중요한 이유
Kubernetes에서 모든 Pod는 ServiceAccount를 통해 API Server와 통신합니다. 별도 설정이 없으면 default ServiceAccount가 자동 마운트되는데, 이 기본 설정은 보안상 위험합니다. 불필요한 API 접근 권한이 열려 있고, 토큰이 탈취되면 클러스터 전체가 위협받을 수 있습니다. Kubernetes 1.24부터는 토큰 관리 방식이 크게 변경되어, 이를 정확히 이해하는 것이 필수입니다.
토큰 방식의 변화: Legacy vs Bound
Kubernetes 1.24 이전과 이후의 토큰 관리 방식이 다릅니다:
| 항목 | Legacy (1.23 이전) | Bound Token (1.24+) |
|---|---|---|
| 저장 방식 | Secret에 영구 토큰 저장 | TokenRequest API로 동적 발급 |
| 만료 | 만료 없음 (영구) | 1시간 기본, 자동 갱신 |
| 바인딩 | 없음 | Pod·Secret·Node에 바인딩 |
| 자동 생성 | SA 생성 시 Secret 자동 생성 | Secret 자동 생성 안 함 |
| 보안 | 토큰 탈취 시 영구 사용 가능 | Pod 삭제 시 토큰 무효화 |
# 1.24+에서 Bound Token 확인
kubectl get pod my-pod -o jsonpath='{.spec.volumes[?(@.name=="kube-api-access-*")].projected.sources}'
# Projected Volume으로 마운트됨:
# - ServiceAccountToken (1시간 만료, 자동 갱신)
# - ConfigMap/kube-root-ca.crt
# - Downward API (namespace)
ServiceAccount 생성과 RBAC 바인딩
워크로드별 전용 ServiceAccount를 만들고 최소 권한 원칙으로 RBAC를 설정합니다:
apiVersion: v1
kind: ServiceAccount
metadata:
name: order-service
namespace: production
annotations:
description: "주문 서비스 전용 SA"
automountServiceAccountToken: false # 기본 마운트 비활성화
---
# 필요한 권한만 Role로 정의
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: order-service-role
namespace: production
rules:
- apiGroups: [""]
resources: ["configmaps"]
verbs: ["get", "watch"]
resourceNames: ["order-config"] # 특정 리소스만 허용
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
resourceNames: ["order-db-credentials"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-service-binding
namespace: production
subjects:
- kind: ServiceAccount
name: order-service
namespace: production
roleRef:
kind: Role
name: order-service-role
apiGroup: rbac.authorization.k8s.io
K8s RBAC 권한 관리 심화 글에서 Role/ClusterRole 설계를 더 자세히 다루고 있습니다.
Pod에 ServiceAccount 적용
Deployment에서 ServiceAccount를 지정하고, 필요한 경우에만 토큰을 마운트합니다:
apiVersion: apps/v1
kind: Deployment
metadata:
name: order-service
namespace: production
spec:
template:
spec:
serviceAccountName: order-service
automountServiceAccountToken: true # 이 Pod는 API 접근 필요
containers:
- name: app
image: myregistry/order-service:v3.0
volumeMounts:
- name: token
mountPath: /var/run/secrets/tokens
readOnly: true
volumes:
- name: token
projected:
sources:
- serviceAccountToken:
path: order-token
expirationSeconds: 3600 # 1시간
audience: order-api # 토큰 수신 대상 지정
automountServiceAccountToken 전략
# API 접근이 필요 없는 Pod → 토큰 마운트 비활성화
apiVersion: v1
kind: ServiceAccount
metadata:
name: web-frontend
automountServiceAccountToken: false # SA 레벨에서 비활성화
---
# Pod 레벨에서도 오버라이드 가능
spec:
automountServiceAccountToken: false # Pod 레벨 우선
serviceAccountName: web-frontend
운영 규칙: API Server 접근이 필요 없는 워크로드(웹 프론트엔드, 배치 잡 등)는 반드시 automountServiceAccountToken: false로 설정하세요.
TokenRequest API: 프로그래밍 방식 토큰 발급
Pod 외부에서 또는 커스텀 만료 시간으로 토큰을 발급받을 수 있습니다:
# kubectl로 토큰 발급 (10분 만료)
kubectl create token order-service
--namespace production
--duration 10m
--audience my-api-server
# TokenRequest API 직접 호출
cat <<EOF | kubectl apply -f -
apiVersion: authentication.k8s.io/v1
kind: TokenRequest
metadata:
name: order-service
namespace: production
spec:
audiences: ["https://my-api.example.com"]
expirationSeconds: 600
boundObjectRef:
apiVersion: v1
kind: Secret
name: order-db-credentials # Secret에 바인딩
EOF
토큰 검증과 TokenReview
다른 서비스가 ServiceAccount 토큰을 검증하는 방법:
# 토큰 검증 요청
cat <<EOF | kubectl apply -f -
apiVersion: authentication.k8s.io/v1
kind: TokenReview
spec:
token: "eyJhbGciOiJSUzI1NiIs..."
audiences: ["order-api"]
EOF
# 응답 예시
# status:
# authenticated: true
# user:
# username: system:serviceaccount:production:order-service
# groups:
# - system:serviceaccounts
# - system:serviceaccounts:production
Go/Java 애플리케이션에서 프로그래밍 방식으로 검증:
// Spring Boot에서 SA 토큰 검증 (kubernetes-client)
@Component
public class K8sTokenVerifier {
private final ApiClient apiClient;
public boolean verify(String token, String expectedAudience) {
AuthenticationV1Api authApi =
new AuthenticationV1Api(apiClient);
V1TokenReview review = new V1TokenReview()
.spec(new V1TokenReviewSpec()
.token(token)
.audiences(List.of(expectedAudience)));
V1TokenReview result =
authApi.createTokenReview(review).execute();
return result.getStatus().getAuthenticated();
}
}
클라우드 IAM 연동: IRSA와 Workload Identity
클라우드 리소스(S3, RDS 등)에 접근할 때 static credentials 대신 ServiceAccount를 클라우드 IAM Role에 매핑하는 패턴입니다:
# AWS EKS: IRSA (IAM Roles for Service Accounts)
apiVersion: v1
kind: ServiceAccount
metadata:
name: s3-uploader
namespace: production
annotations:
# AWS IAM Role ARN 매핑
eks.amazonaws.com/role-arn: arn:aws:iam::123456789:role/s3-upload-role
---
# GKE: Workload Identity
apiVersion: v1
kind: ServiceAccount
metadata:
name: gcs-reader
namespace: production
annotations:
# GCP SA 매핑
iam.gke.io/gcp-service-account: gcs-reader@my-project.iam.gserviceaccount.com
# IRSA 설정 검증
kubectl exec -it deploy/s3-uploader -- env | grep AWS
# AWS_ROLE_ARN=arn:aws:iam::123456789:role/s3-upload-role
# AWS_WEB_IDENTITY_TOKEN_FILE=/var/run/secrets/eks.amazonaws.com/serviceaccount/token
# 토큰 내용 확인 (JWT 디코딩)
kubectl exec -it deploy/s3-uploader --
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token |
cut -d. -f2 | base64 -d | jq .
# {
# "aud": ["sts.amazonaws.com"],
# "exp": 1711022400,
# "sub": "system:serviceaccount:production:s3-uploader"
# }
보안 베스트 프랙티스
1. default ServiceAccount 잠금
# 모든 네임스페이스의 default SA에서 토큰 마운트 비활성화
kubectl patch serviceaccount default -n production
-p '{"automountServiceAccountToken": false}'
# OPA/Kyverno 정책으로 강제
apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
name: restrict-automount-sa-token
spec:
validationFailureAction: Enforce
rules:
- name: check-automount
match:
any:
- resources:
kinds: ["Pod"]
validate:
message: "SA token auto-mount must be explicitly set"
pattern:
spec:
automountServiceAccountToken: false
2. 네임스페이스별 SA 격리
# 네임스페이스 간 SA 참조 방지 — RoleBinding은 같은 NS의 SA만 허용
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: order-binding
namespace: production
subjects:
- kind: ServiceAccount
name: order-service
namespace: production # 반드시 같은 네임스페이스
3. 토큰 만료 모니터링
# 장기 유효 토큰(Legacy Secret 기반) 탐지
kubectl get secrets --all-namespaces
-o jsonpath='{range .items[?(@.type=="kubernetes.io/service-account-token")]}{.metadata.namespace}/{.metadata.name}{"n"}{end}'
# 발견되면 삭제하고 TokenRequest API로 전환
kubectl delete secret legacy-token -n production
K8s Pod Security Standards와 함께 적용하면 보안을 더욱 강화할 수 있습니다.
마치며
ServiceAccount는 Kubernetes 보안의 기본 단위입니다. 1.24+ Bound Token 방식을 이해하고, 워크로드별 전용 SA를 만들어 최소 권한만 부여하세요. API 접근이 불필요한 Pod는 automountServiceAccountToken: false로 토큰 마운트를 차단하고, 클라우드 리소스 접근에는 IRSA/Workload Identity를 활용하여 static credentials를 제거하세요. Legacy Secret 기반 토큰은 반드시 TokenRequest API로 마이그레이션해야 합니다.