본문 바로가기
데브옵스 이야기/Kubernetes

쿠버네티스 인증인가에 대하여...

by lakescript 2026. 3. 6.
728x90

 

 

kubernetes Control Plane의 인증(AuthX)은 [요청자가 누구인가]를 확인하는 과정이고, 인가(AuthZ)는 [그 요청자가 해당 리소스에 대해 실행하려는 동작을 할 권한이 있는가]를 판단하는 과정입니다.

 

 

모든 API 요청은 이 두 단계를 통과해야 admission을 통해 ETCD에 반영되며, 인증 실패시 401, 인가 실패시 403 HTTP 상태 코드가 반환됩니다.

 

 

이러한 쿠버네티스의 인증/인가를 직접 실습해보면서 알아보겠습니다. (기초 주의)

 

실습 환경 구성

실습을 진행할 디렉토리(폴더) 생성

mkdir /k8s-auth-stury && cd /k8s-auth-study

 

실습은 kind를 통해 local 머신에서 kubernetes clsuter를 생성하여 진행할 것 이기에, 해당 설정 파일이나 관련 명세 파일을 생성할 디렉토리를 생성합니다.

 

kind cluster 생성

cat > kind-auth-study.yaml << 'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
# Kubernetes 1.34 버전 지정
nodes:
  - role: control-plane
    image: kindest/node:v1.34.0
  - role: worker
    image: kindest/node:v1.34.0
EOF

 

control plane으로 사용할 node와 worker node를 분리하고, k8s 최신 버전인 1.34버전의 cluster를 생성합니다.

 

kind create cluster --config kind-auth-study.yaml --name auth-study

 

보통 생성이 완료되기까지 1~2분 정도 소요됩니다.

 

Cluster 생성 확인

kubectl get node -o wide

 

그 후 위 명령어를 통해 node들이 잘 생성되었는지 확인해보겠습니다. 

 

cluster 정보 확인

kubectl cluster-info

 

위 명령어는 현재 kubectl이 어떤 k8s cluster에 연결되어 있는지, cluster의 주요 서비스가 어디에서 실행되고 있는지 보여줍니다.

 

 

사진을 보면 같은 `kubectl` 명령인데 `node` 를 조회하는 명령했을 때와 `cluster-info` 명령의 결과가 다른 것을 확인하실 수 있습니다.

 

조금 자세히 살펴보겠습니다.

 

Kubernetes Control Plane 주소

 

위 실행 결과에서 첫 줄을 살펴보겠습니다.

Kubernetes control plane is running at https://127.0.0.1:53122

 

Kubernetes API 서버(kube-apiserver)의 주소를 의미합니다.

 

`kubectl` -> `https://127.0.0.1:53122`  -> `kube-apiserver`

 

위와 같이 요청이 진행되는 것인데, 아래의 명령도 내부적으로는 `api server`에 요청을 보내는 것입니다.

kubectlget pods
kubectlget nodes
kubectl apply-f deployment.yaml

 

즉, 모든 요청은 결국 `api server` 의 REST API로 전달됩니다.

 

127.0.0.1

 

여기서 보여지는 IP는 로컬 루프백 주소로 kind를 통해 로컬 머신에서 실행중인 kubernetes clsuter입니다.

 

더보기

kind의 경우 호출 경로

kubectl
   ↓
localhost:53122 (포트포워딩)
   ↓
docker container
   ↓
kube-apiserver

 

Docker 컨테이너 안의 API 서버를 local 포트로 노출한 것입니다.

 

또한, 53122 port는 host에서 접근 가능한 API 서버 포트입니다. 

host:53122 → container:6443

 

kind cluster 내부에서는 kube-apiserver가 기본적으로 6443 port를 사용하는데, 실제 포트 매핑은 위와 같습니다.

 

 

CoreDNS is running at
https://127.0.0.1:53122/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

 

위 문장은 cluster DNS 서비스(CoreDNS)에 접근할 수 있는 API 경로를 보여줍니다.

 

보통 coreDNS는 service 이름을 clusterIP로 변환하고, Pod간 service discovery를 수행하는 cluster 내부 DNS 서버입니다.

 

 

또한, URL 끝에 있는 `/proxy`는 API Server Proxy 기능을 의미합니다.

 

kubectl
   ↓
kube-apiserver
   ↓
CoreDNS Service

 

위와 같이 api server가 CoreDNS 서비스로 요청을 Proxy(api server가 중간에서 요청을 대신 전달)하는 구조입니다.

 

Proxy URL?

kubectl
 ↓
kube-apiserver
 ↓
CoreDNS Service
 ↓
CoreDNS Pod

 

일반적인 클러스터 내부 흐름은 위와 같습니다.

 

여기서 중요한 점은 API Server는 관여하지 않고 `kube-proxy`, `iptables/ipvs`, `CNI등`이 담당하는데요.

https://127.0.0.1:53122/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy

 

하지만 `kubectl cluster-info` 에서 나오는 URL은 Service에 직접 접근하는 것이 아니라 API Server의 REST API를 통해 접근하는 방식입니다.

 

여기서 "왜 `cluster-info` 명령을 했을 때 `CoreDNS servcie` 의 `ClusterIP`가 아니라 api server가 proxy 역할을 할까? 라는 질문이 생각났습니다.

 

일반 접근
 client -> CoreDNS​

api server proxy 접근
client -> api serve -> CoreDNS

 

이유는 클러스터 외부에서는 `ClusterIP`나 `Pod IP`에 직접 접근할 수 없다는 점입니다. 이 IP들은 클러스터 네트워크 내부에서만 라우팅됩니다.

 

반면 `api Server`는 외부에 노출된 `endpoint`가 있기 때문에, 클라이언트가 `api Server`를 거쳐 내부 `Service`에 접근할 수 있습니다. 그렇기 때문에 클러스터 내부에 대해 디버깅을 한다거나, 인증/RBAC 적용된 접근을 할 때에 유용합니다.

 

API Server 요청 흐름

kubectl → kubeconfig (인증 정보) → API Server → Authentication → Authorization → Admission → etcd

 

모든 kubernetes API 요청은 반드시 API Server를 거치게 됩니다. kubectl은 kubeconfig에 저장된 인증정보를 사용하여 HTTPS로 API Server에 요청을 보냅니다.

 

API Server는 요청을 받으면 Authentication(인증) → Authorization(인가) → Admission를 순서대로 처리한 뒤, 유효한 요청만 etcd에 저장합니다.

 

 

인증(Authentication)

API 서버는 여러 인증 방식을 지원하며, 요청에 포함된 인증 정보 형태를 확인하여 승인합니다. 


예를 들어 `Authorization: Bearer <token>` 헤더가 있으면 `Bearer 토큰 인증`을 진행하게 되고, 클라이언트 인증서가 있으면 `X.509 인증`을 시도합니다. 이때 한 요청에 여러 방식이 적용될 수는 없고, 가장 먼저 통과된 방식의 인증이 사용됩니다.

 

인증 방식

방식 설명
X.509 Client Certificate 클라이언트 인증서로 사용자 식별
Bearer Token JWT 토큰 또는 정적 토큰
ServiceAccount Pod 내부에서 사용하는 토큰
OIDC 외부 IdP 연동
Webhook 외부 서비스에 인증 위임

 

 

 

실습 1 

service account 및 pod 생성

service account 생성

kubectl create serviceaccount test-sa -n default

 

k8s에서 인증은 service account 기반으로 진행되기 때문에, service account를 생성합니다.

 

service account 확인

kubectl get serviceaccount test-sa -n default -o yaml

 

 

token 발급

kubectl create token test-sa -n default --duration=1h

 

JWT 형식의 토큰을 해당 service account에 발급 받습니다.

 

 

 

Pod 생성

cat > sa-test-pod.yaml << 'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: sa-test-pod
  namespace: default
spec:
  serviceAccountName: test-sa
  containers:
    - name: curl
      image: curlimages/curl:latest
      command: ["sleep", "3600"]
EOF

 

실습을 진행할 pod를 생성합니다. 위에서 생성한 service account인 `test-sa` 를 연결시켜줍니다.

 

kubectl apply -f sa-test-pod.yaml

 

생성된 Pod 확인

kubectl get pod sa-test-pod

 

 

 

Pod 내부에 접근하여 API Server 호출

pod 내부 접근

kubectl exec -it sa-test-pod -- sh

 

환경 변수 설정

ls -la /var/run/secrets/kubernetes.io/serviceaccount/

 

위 경로를 통해 연결된 service account의 정보를 확인해보겠습니다.

 

token과 ca.crt, namespace를 확인하실 수 있습니다.

 

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

 

위에서 확인한 token, ca.crt, namespace의 경로를 환경 변수로 설정합니다.

 

API Server 호출

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  --cacert "$CACERT" \
  "https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods"

 

Pod 내부에서 ServiceAccount Token을 이용하여 kubernetes API server를 직접 호출하는 명령어입니다.

즉, kubectl 없이 REST API 로 kube-apiserver에 요청을 직접 보내게 됩니다.
위 명령은 내부적으로는 `GET /api/v1/namespaces/<namespace>/pods`API를 호출하게 됩니다.

 

 

결과로 403 에러가 반환되는데, 이는 인증은 성공했지만 권한이 없다는 내용입니다. (이것은 인가 영역...)

 

인가 (Authorization)

인가는 인증 이후 진행되는 단계로, 이미 확인된 사용자가 요청한 리소스에 대해 해당 동작을 수행할 권한이 있는지를 판단합니다.

특히, kubernetes에서는 모든 요청이 kube-apiserver를 통해 처리되기 때문에, API Server는 인증을 통과한 요청에 대해 권한을 확인합니다. 이후 권한이 허용되면 Admission 단계로 전달하며, 권한이 없는 경우 요청을 즉시 서부하고 403 응답을 반환합니다.

 

또한, Kubernetes는 여러 인가 모드를 지원하지만, 기본적으로 RBAC(Role-Based Access Control) 이 사용됩니다.

 

RBAC(Role-Based Access Control)

RBAC란?

RBAC는 kubernetes의 기본 인가 모델로, 누가(subject) 어떤 리소스에, 어떤 동작(Verbs)을 할 수 있는지를 정의합니다.

k8s에는 사용자 계정이 별도로 존재하지 않으며, RBAC의 Subject는 외부 인증(User, Group)이나 ServiceAccount와 연결합니다.

 

리소스 범위 설명
Role Namespace 특정 네임스페이스 내 권한
ClusterRole Cluster 클러슽 전체 또는 여러 네임스페이스 공통 권한
RoleBinding Namespace Role을 Subject에 연결
ClusterRoleBinding Cluster ClusterRole을 Subject에 연결

 

Role은 권한 정의, Role Binding은 그 권한을 누구에게 부여할지 담당하며, 같은 Role을 여러 User, Group에 재사용할 수 있습니다.

 

Verbs (동작)

API Server가 지원하는 동작(verb)은 REST API의 HTTP 메서드와 대응됩니다.

 

`get` → 단일 리소스 조회

`list` → 목록 조회

`create` → 생성

`update` → 수정(전체)

`patch` → 수정(부분)

`delete` → 삭제

`*` → 모든 동작 허용

 

실습 

RBAC 생성

Role 생성

cat > developer-role.yaml << 'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: default
  name: developer-role
rules:
  - apiGroups: [""]
    resources: ["pods", "pods/log"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
  - apiGroups: [""]
    resources: ["configmaps", "secrets"]
    verbs: ["get", "list", "watch"]
  - apiGroups: ["apps"]
    resources: ["deployments"]
    verbs: ["get", "list", "watch", "create", "update", "patch", "delete"]
EOF

 

 

Rolebinding 생성

cat > developer-rolebinding.yaml << 'EOF'
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: developer-binding
  namespace: default
subjects:
  - kind: ServiceAccount
    name: test-sa
    namespace: default
roleRef:
  kind: Role
  name: developer-role
  apiGroup: rbac.authorization.k8s.io
EOF

 

위에서 생성한 test-sa serviceaccount에 developer-role이라는 role을 binding합니다.

 

 

Role 및 Rolebinding 적용

kubectl apply -f developer-role.yaml

 

 

kubectl apply -f developer-rolebinding.yaml

 

 

권한 테스트

허용된 권한 테스트

kubectl auth can-i create pod --as=system:serviceaccount:default:test-sa -n default

 

test-sa가 pod 생성가능한지 auth can-i 명령을 통해 확인해보겠습니다.

 

 

kubectl auth can-i delete deployment --as=system:serviceaccount:default:test-sa -n default

 

마찬 가지로 deployment를 삭제할 수 있는지도 확인해보겠습니다.

 

 

거부되는 권한 테스트

kubectl auth can-i get clusterrole --as=system:serviceaccount:default:test-sa

 

test-sa가 clusterrole 조회가 가능한지 확인해보겠습니다.

 

 

Pod 내부에서 RBAC 적용 확인

pod 내부 접근

kubectl exec -it sa-test-pod -- sh

 

위에서 실습을 했었던 것 처럼 sa-test-pod 내부로 접근합니다.

 

환경 변수 설정

TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)
CACERT=/var/run/secrets/kubernetes.io/serviceaccount/ca.crt
NAMESPACE=$(cat /var/run/secrets/kubernetes.io/serviceaccount/namespace)

 

 

API Server 호출

curl -s -o /dev/null -w "%{http_code}\n" \
  -H "Authorization: Bearer $TOKEN" \
  --cacert "$CACERT" \
  "https://kubernetes.default.svc/api/v1/namespaces/$NAMESPACE/pods"

 

아까와 다르게 200이 반환된 것을 확인하실 수 있습니다.

 

 

 


위 실습을 통해 Kubernetes에서 API 요청이 처리되는 전체 흐름과 인증(Authentication) 및 인가(Authorization)의 역할을 직접 확인할 수 있었습니다.

 

Kubernetes에서는 모든 요청이 kubectl 또는 Pod 내부 애플리케이션을 통해 kube-apiserver의 REST API로 전달되며, API Server는 요청을 처리하기 전에 Authentication → Authorization → Admission의 순서를 반드시 거치게 됩니다.

 

먼저 Authentication 단계에서는 요청자가 누구인지 검증하고, 이후 Authorization 단계에서는 해당 사용자가 특정 리소스에 대해 요청한 동작을 수행할 권한이 있는지를 판단합니다.

 

실습에서는 ServiceAccount와 JWT 토큰을 이용해 Pod 내부에서 직접 API Server를 호출해 보았고, 초기에는 권한이 없어 403 응답(인가 실패)이 발생하는 것을 확인했습니다. 이후 Role과 RoleBinding을 통해 RBAC 권한을 부여하자 동일한 요청이 200 응답으로 변경되며 정상적으로 리소스 조회가 가능해졌습니다.

 

이를 통해 위에서 설명한 인증은 “누구인가”를 확인하는 과정이며, 인가는 “무엇을 할 수 있는가”를 결정하는 과정이라는 점과, RBAC이 실제 클러스터 접근 권한을 제어하는 핵심 메커니즘이라는 것을 확인할 수 있습니다.

 

지금 실습에서는 ServiceAccount 토큰을 사용해 인증을 진행했지만, 실제 운영 환경에서는 사람이 직접 토큰을 발급받아 사용하는 경우는 많지 않습니다. 실제 업무에서는 Keycloak, Google, Okta 같은 인증 서비스(IdP) 를 통해 로그인하고, 그 결과로 발급된 JWT 토큰을 Kubernetes에 전달하여 인증을 처리합니다.

 

마찬가지로 Kubernetes 내부에서는 이 토큰을 검증해 사용자가 누구인지 확인(Authentication) 하고, 토큰에 포함된 사용자나 그룹 정보를 기반으로 RBAC을 통해 어떤 작업을 할 수 있는지 판단(Authorization) 합니다. 


그렇기 때문에 추후에는 OIDC(OpenID Connect) 를 이용해 Kubernetes를 외부 로그인 시스템과 연결하는 방법도 살펴볼 예정입니다.

 

728x90