K8s ServiceAccount 토큰 보안

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로 마이그레이션해야 합니다.

위로 스크롤
WordPress Appliance - Powered by TurnKey Linux