# 클러스터 안팎에서 Kubernetes API에 인증하기 - in-cluster 토큰과 kubeconfig

Kubernetes API를 호출하는 서비스를 만들면 실행 위치가 두 가지가 됩니다. 개발자 노트북에서는 kubeconfig로, 클러스터 안에서는 서비스 어카운트 토큰으로 인증합니다. 두 경로를 하나의 코드로 다루면서 자격 증명을 안전하게 유지하려면 각 방식이 실제로 무엇을 읽는지 알아야 합니다.

이 글에서는 파드 안 인증이 어떤 파일과 환경변수에 의존하는지, 최신 버전의 토큰이 자동 회전한다는 사실이 클라이언트 구현에 무엇을 요구하는지, 그리고 두 경로를 함께 지원하는 구성 방법을 정리합니다.

# 1. 파드 안에서의 인증 재료

파드에는 기본적으로 서비스 어카운트가 연결되고, 컨테이너 파일시스템에 세 파일이 놓입니다.

경로 내용
/var/run/secrets/kubernetes.io/serviceaccount/token API 서버에 보낼 베어러 토큰
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt API 서버 인증서를 검증할 CA
/var/run/secrets/kubernetes.io/serviceaccount/namespace 파드가 속한 네임스페이스

API 서버 주소는 환경변수로 주어집니다.

KUBERNETES_SERVICE_HOST
KUBERNETES_SERVICE_PORT_HTTPS

클러스터 내부 주소는 default 네임스페이스의 kubernetes 서비스로 게시되며 kubernetes.default.svc로 접근할 수 있습니다.

정리하면 파드 안 인증은 토큰(누구인가) + CA(상대가 진짜인가) + 주소(어디로) 세 조각의 조합입니다. 셋 다 파일이나 환경변수로 주어지므로, 애플리케이션에 자격 증명을 심을 이유가 없습니다.

# 파드 안에서 직접 확인
TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
curl --cacert /var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
     -H "Authorization: Bearer $TOKEN" \
     "https://$KUBERNETES_SERVICE_HOST:$KUBERNETES_SERVICE_PORT_HTTPS/api/v1/namespaces/default/pods"

Go 클라이언트에서는 이 과정을 rest.InClusterConfig()가 대신합니다. 문서 표현대로 "handles API host discovery and authentication automatically" 입니다.

# 2. 토큰은 만료되고 자동 회전한다

여기가 실무에서 가장 자주 사고가 나는 지점입니다. v1.22 이후 Kubernetes는 TokenRequest API로 발급한 수명이 짧은 토큰을 projected 볼륨으로 마운트합니다.

In v1.22 and later, Kubernetes gets a short-lived, automatically rotating token using the TokenRequest API and mounts the token as a projected volume.

그리고 회전 주체는 kubelet입니다.

Projected tokens expire automatically, and the kubelet rotates the token before it expires.

토큰 파일의 내용은 프로세스 수명 중에 바뀝니다. 기동 시 한 번 읽어 메모리에 들고 있는 구현은 시간이 지나면 401 Unauthorized를 받기 시작합니다. 며칠 뒤 갑자기 실패하는 유형의 장애가 여기서 나옵니다.

구현 결과
시작 시 토큰 문자열을 읽어 헤더에 고정 회전 후 401. 하면 안 됨
매 요청 전 파일을 다시 읽음 정상
공식 클라이언트 라이브러리 사용 라이브러리가 재읽기를 처리

직접 HTTP 클라이언트를 구현했다면 파일을 다시 읽는 경로를 반드시 넣어야 합니다. 공식 클라이언트를 쓰는 것이 가장 안전한 선택입니다.

과거 방식(서비스 어카운트 Secret에 담긴 영구 토큰)은 문서가 분명히 권장하지 않는다고 밝힙니다.

Service Account Token Secrets (not recommended): ... These tokens don't expire and don't rotate. ... This method is not recommended anymore, especially at scale, because of the risks associated with static, long-lived credentials.

만료도 회전도 없는 토큰이 어딘가에 남아 있다면 유출돼도 무효화할 수단이 없는 자격 증명입니다.

# 3. 두 실행 위치를 하나의 코드로

로컬에서는 kubeconfig, 클러스터에서는 in-cluster를 쓰되 분기는 한 곳에만 둡니다.

import (
    "k8s.io/client-go/kubernetes"
    "k8s.io/client-go/rest"
    "k8s.io/client-go/tools/clientcmd"
)

func newClient(kubeconfigPath string) (*kubernetes.Clientset, error) {
    cfg, err := rest.InClusterConfig()
    if err == rest.ErrNotInCluster {
        // 클러스터 밖: kubeconfig 사용
        cfg, err = clientcmd.BuildConfigFromFlags("", kubeconfigPath)
    }
    if err != nil {
        return nil, err
    }
    cfg.QPS, cfg.Burst = 20, 40      // 기본값은 낮다. 목록 조회가 많으면 조정
    return kubernetes.NewForConfig(cfg)
}

rest.ErrNotInCluster로 구분하는 것이 핵심입니다. 환경변수 유무를 직접 검사하거나 os.Stat으로 토큰 파일을 확인하는 방식은 조건이 늘어나면 어긋납니다.

# 4. 인증 방식별 비교

방식 자격 증명 회전 적합한 위치
in-cluster 토큰 projected 서비스 어카운트 토큰 자동 클러스터 내부 워크로드
kubeconfig - 클라이언트 인증서 client-certificate/client-key 수동(만료 시 재발급) 관리자 로컬, 부트스트랩
kubeconfig - exec 플러그인 외부 도구가 토큰 발급 도구가 처리 매니지드 클러스터(클라우드 CLI 연동)
정적 Secret 토큰 영구 토큰 없음 권장하지 않음

클라이언트 인증서 방식은 만료가 조용히 다가온다는 점이 함정입니다. 만료일을 미리 확인해 두는 편이 좋습니다.

kubectl config view --raw -o jsonpath='{.users[0].user.client-certificate-data}' \
  | base64 -d | openssl x509 -noout -enddate

또 하나 자주 놓치는 것은 인증(authentication)과 인가(authorization)가 별개라는 점입니다. 토큰이 유효해도 RBAC가 없으면 403입니다. 401과 403을 구분해서 읽어야 원인을 빨리 찾습니다.

# 5. 최소 권한 서비스 어카운트

파드에 붙는 기본 서비스 어카운트를 그대로 쓰지 말고, 워크로드 전용 계정에 필요한 동사만 부여합니다. API를 호출하지 않는 파드라면 토큰 마운트 자체를 끄는 것이 가장 확실합니다.

apiVersion: v1
kind: ServiceAccount
metadata:
  name: workspace-controller
  namespace: platform
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: workspace-controller
  namespace: platform
rules:
  - apiGroups: [""]
    resources: ["pods", "services", "persistentvolumeclaims"]
    verbs: ["get", "list", "watch", "create", "delete"]
  - apiGroups: [""]
    resources: ["secrets"]        # Helm 릴리스 저장 등으로 필요한 경우만
    verbs: ["get", "list", "create", "update", "delete"]
# API를 호출하지 않는 워크로드
spec:
  automountServiceAccountToken: false

권한이 실제로 어떻게 평가되는지는 auth can-i로 확인할 수 있습니다.

kubectl auth can-i create pods \
  --as=system:serviceaccount:platform:workspace-controller -n platform

# 6. 트러블슈팅

증상 원인 해결
배포 직후엔 되다가 며칠 뒤 401 토큰을 메모리에 캐시 매 요청 시 파일 재읽기 또는 공식 클라이언트 사용
401 Unauthorized 토큰 자체가 무효·만료 토큰 경로·마운트 여부 확인
403 Forbidden 인증은 됐으나 RBAC 부족 auth can-i로 확인 후 Role 보완
x509 인증서 검증 실패 CA 미지정 또는 잘못된 CA ca.crt 사용 여부 확인
로컬에선 되는데 파드에서 실패 kubeconfig 경로 분기 오류 rest.ErrNotInCluster 기준으로 분기
목록 조회에서 지연·throttling 클라이언트 QPS/Burst 기본값 rest.Config에서 상향, 캐시·informer 검토

# 7. 마무리

  • 파드 안 인증은 토큰·CA·주소 세 조각이며 전부 파일과 환경변수로 주어집니다. 코드에 자격 증명을 넣을 이유가 없습니다.
  • v1.22 이후 토큰은 수명이 짧고 kubelet이 자동 회전합니다. 기동 시 한 번 읽어 캐시하면 며칠 뒤 401이 납니다.
  • 만료도 회전도 없는 정적 Secret 토큰은 공식적으로 권장되지 않습니다.
  • 인증과 인가는 별개입니다. 401은 자격 증명, 403은 RBAC 문제로 나눠서 보세요.

# 참고