13-2부에서 API 서버 권한을 좁히며, 그 판정이 실제로 어떻게 도는지를 이 클러스터 실물로 확인하며 정리한 기록이다. 본편은 조치만 다루고, 원리는 여기에 둔다.
클러스터로의 모든 요청은 API 서버 하나로 간다
클러스터의 상태 — 파드 목록, Deployment, Secret, NetworkPolicy — 는 etcd 에만 있다. 그리고 etcd 에 읽고 쓰는 것은 API 서버 하나뿐이다. 파드를 만들려면 etcd 에 그 기록이 들어가야 하고, 그것을 할 수 있는 주체가 API 서버밖에 없어서 사람이든 ArgoCD 든 kubelet 이든 전부 API 서버에 요청을 보낸다.
kubectl 은 그 API 서버에 HTTP 요청을 보내는 클라이언트다. kubectl get pods -n app 에 -v=6(kubectl 의 로그 상세도 옵션)을 붙이면 kubectl 이 실제로 보내는 요청이 보인다.
GET https://10.0.0.11:6443/api/v1/namespaces/app/pods?limit=500 200 OK 31ms
6443 에서 듣고 있는 프로세스가 API 서버다. 이 클러스터에서는 API 서버가 노드마다 도는 k3s 서버 프로세스 안에 들어 있어, 세 노드가 모두 그 포트를 연다. kubectl get pods -n kube-system 에 apiserver 파드가 안 보이는 이유가 이것이다 — 파드가 아니라 systemd 서비스(k3s) 안의 컴포넌트다.
파드도 같은 API 서버에 요청을 보낸다. ArgoCD 는 배포하려고, Grafana 의 대시보드 보조 컨테이너는 ConfigMap 을 읽으려고, Alloy 는 파드 목록을 수집하려고 부른다. 파드가 API 서버를 부른다는 것은 파드에도 신원이 있고 그 신원에 권한이 붙어 있다는 뜻이다.
API 서버는 요청 하나를 두 단계로 판정한다.
| 단계 | API 서버가 정하는 것 | 정하지 않는 것 |
|---|---|---|
| 인증(Authentication) | 요청을 보낸 것이 누구인지 | 그 신원이 무엇을 할 수 있는지 |
| 인가(Authorization) | 그 신원이 이 요청을 해도 되는지 | — |
인증이 끝난 시점에 권한은 0 이다. API 서버가 아는 것은 요청을 보낸 쪽의 이름뿐이고, 그 이름이 무엇을 할 수 있는지는 인가가 클러스터 안의 오브젝트에서 찾는다.
사람의 신원 — 인증서와 kubeconfig
kubectl get pods 한 번에 일어나는 일이 여섯이다.
0 (준비) k3s 가 노드에 만들어 둔 접속용 파일(kubeconfig)을 데스크탑으로 복사해 둔다
1 kubectl 이 그 파일을 읽는다
2 kubectl 이 API 서버에 연결하면서 자기 인증서를 내민다. API 서버도 자기 인증서를
내밀고, 양쪽이 각자 가진 CA 로 상대의 인증서를 검증한다
3 API 서버가 kubectl 의 인증서에 적힌 이름(CN)과 그룹(O)을 읽는다 ← 인증 끝
4 API 서버가 그 이름을 가리키는 연결(RoleBinding·ClusterRoleBinding)을 찾는다
5 API 서버가 그 연결이 가리키는 규칙 묶음(Role·ClusterRole)으로 판정한다 ← 인가 끝
kubeconfig — 접속에 필요한 것을 담은 파일
kubeconfig 는 파일 이름이 아니라 형식의 이름이다. 노드에서는 k3s.yaml, 데스크탑에서는 config 인데 둘 다 kubeconfig 다. kubectl 만 읽는 것도 아니다 — helm 도 ArgoCD CLI 도 같은 형식을 같은 규칙으로 읽는다.
이 파일은 k3s 가 만든다. k3s 를 노드에 설치하면 k3s 가 기동하면서 /etc/rancher/k3s/k3s.yaml 을 스스로 만들고, 그 안에 관리자 인증서와 개인키를 넣는다.

데스크탑의 ~/.kube/config 는 그 파일을 복사한 것이고, 복사하면서 server: 를 127.0.0.1:6443 에서 10.0.0.11:6443 으로 고쳤다 — 원본 그대로면 kubectl 이 데스크탑 자신에게 접속하려 한다.

데스크탑 파일의 전문이다(인증서 값만 줄였다).
apiVersion: v1
kind: Config
clusters: # 접속할 클러스터 목록
- cluster:
server: https://10.0.0.11:6443 # 요청을 보낼 주소
certificate-authority-data: LS0tLS1… # 상대(API 서버)의 인증서를 검증할 CA
name: default # contexts 가 이 이름으로 지목한다
users: # 신원 목록
- name: default # contexts 가 이 이름으로 지목한다
user:
client-certificate-data: LS0tLS1… # API 서버에 내밀 인증서. 안에 이름(CN)·그룹(O)
client-key-data: LS0tLS1… # 그 인증서가 자기 것임을 증명하는 개인키
contexts: # 클러스터 + 신원 조합 목록
- context: {cluster: default, user: default}
name: default
current-context: default # kubectl 이 지금 쓸 조합
clusters 와 users 는 목록이고 항목마다 name 이 있다. contexts 가 그 이름들을 짝지어 "이 클러스터에 이 신원으로"를 만들고, current-context 가 그중 하나를 가리킨다. 클러스터 여러 개와 자격 여러 개를 한 파일에 두고 kubectl config use-context 로 바꿔 쓰는 구조다. 이 파일은 항목이 하나씩뿐이라 셋 다 default 다.
kubectl 이 요청 하나에 실제로 쓰는 값은 넷이고, 각 값이 쓰이는 단계가 갈린다.
clusters 아래 — 붙을 상대에 관한 것
server 2단계 kubectl 이 여기로 연결한다
certificate-authority-data 2단계 kubectl 이 API 서버의 인증서를 이것으로 검증한다
users 아래 — 자기 신원에 관한 것
client-certificate-data 2단계 kubectl 이 이것을 내민다
3단계 API 서버가 이 안의 이름(CN)과 그룹(O)을 읽는다
client-key-data 2단계 내민 인증서가 자기 것임을 증명한다
4단계·5단계에 쓰이는 값 없다
4단계와 5단계에 쓸 것이 이 파일에 하나도 없다 — 그 둘은 확정된 이름으로 무엇을 할 수 있는지를 정하는 단계이고, 그 내용은 클러스터 안의 오브젝트에 있다. kubeconfig 에는 권한이 한 글자도 없다.
키 쌍과 서명
키를 만들면 개인키와 공개키가 짝으로 나온다. 개인키로 만든 값은 짝 공개키로만 확인되고, 공개키로는 그 값을 만들 수 없다. 이 한 방향 성질이 아래 전부의 바탕이다.
개인키로 어떤 데이터에 대해 계산한 값이 서명이다. 공개키를 가진 쪽은 그것으로 "이 서명을 만든 것은 짝 개인키를 가진 쪽"이라고 확정할 수 있다. 개인키 자체는 보내지 않고 서명한 값만 보낸다.
인증서 — 이름과 그룹, 공개키, 발급자의 서명
users 아래 client-cetificate-data 인증서는 파일이고 안에 셋이 들어 있다. - 이름, 소속 그룹 + 공개키 + 서명
이름(subject)과 공개키는 인증서 주인의 것이고, issuer의 서명은 발급해 준 쪽 즉 서버가 해준 것이다. users 아래의 client-certificate-data 를 열면 그대로 보인다.
grep -m1 "client-certificate-data:" ~/.kube/config | sed 's/.*: *//' | base64 -d \
| openssl x509 -noout -subject -pubkey -issuer
subject = O=system:masters, CN=system:admin 주인의 이름(CN)과 소속 그룹(O)
-----BEGIN PUBLIC KEY----- 주인의 공개키 — 짝 개인키는 client-key-data
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEs7FI5SW1…
-----END PUBLIC KEY-----
issuer = CN=k3s-client-ca@1784987649 이 인증서에 서명한 쪽
subject 만 있으면 인증서가 성립하지 않는다. O=system:masters 라고 적은 파일은 누구나 만들 수 있어서, 다른 주체가 그 내용에 해 준 서명이 함께 들어간다. issuer 가 그 서명을 한 쪽이다.
CA — 인증서에 서명해 주는 주체
인증서에 서명해 주는 쪽이 CA(Certificate Authority)다. CA 도 키 쌍을 갖고 자기 개인키로 남의 인증서 내용에 서명하고, 받는 쪽은 CA 의 공개키가 있어야 그 서명을 확인할 수 있다. certificate-authority-data가 client-ca 즉, 사람이 쓰는 클라이언트 인증서.
kubeconfig 안에 인증서가 둘 있었다.
users 아래 client-certificate-data 는 내가 내미는 인증서고,
clusters 아래 certificate-authority-data 는 상대를 검증하는 인증서다.
client-certificate-data는 client-ca 가 서명해 준 내 인증서고,
certificate-authority-data는 server-ca 가 자기한테 서명한 자기 인증서다.
자기 서명이라 그 위에 확인해 줄 것이 없고, 그래서 검증의 출발점이 된다.
k3s 는 설치되면서 CA 를 두 벌 만들어 노드의 /var/lib/rancher/k3s/server/tls/ 에 둔다.
| CA | 무엇에 서명하나 | 누가 검증에 쓰나 | 어디에 있나 |
|---|---|---|---|
client-ca |
사람이 쓰는 클라이언트 인증서 | API 서버가 kubectl 을 검증할 때 | 노드에만 |
server-ca |
API 서버 자신의 인증서 | kubectl 이 API 서버를 검증할 때 | kubeconfig 의 certificate-authority-data |
kubeconfig 에 든 CA 가 server-ca 인 이유는 검증을 각자 상대에게 하기 때문이다 — kubectl 이 검증할 상대는 API 서버다.
연결 한 번에 일어나는 검증
kubectl 이 API 서버에 두 가지를 보낸다.
자기 인증서 [ 이름(CN)·그룹(O) + 자기 공개키 + client-ca 가 만든 서명 ]
서명한 값 하나 자기 개인키로, 이번 연결에서 오간 값에 서명한 것
두 번째 서명은 인증서에 하는 서명이 아니다. 이번 연결에서 양쪽이 주고받은 값에 하는 서명이고, 그 값에 매번 달라지는 난수가 들어 있어 지난 연결의 서명을 다시 쓸 수 없다. 개인키는 그 계산에만 쓰이고 밖으로 나가지 않는다.
API 서버는 받은 둘(본인 인증서, 서명)을 각각 확인한다.
client-ca 의 공개키로 인증서의 서명을 확인한다
→ 이 인증서는 client-ca 가 발급한 것이 맞다
인증서 안의 공개키로 서명한 값을 확인한다
→ 이 인증서를 내민 쪽이 짝 개인키를 가진 주인이 맞다
앞엣것만 하면 인증서를 복사해 온 사람도 통과한다 — 인증서는 공개돼도 되는 값이라서다. API 서버도 같은 것을 반대 방향으로 한다. 자기 인증서와 서명한 값을 보내고, kubectl 은 certificate-authority-data(server-ca)로 그 인증서를, 인증서 안의 서버 공개키로 서명한 값을 확인한다.
검증이 끝나면 API 서버가 인증서의 CN 을 사용자 이름으로, O 를 그룹으로 읽는다. 인증은 거기까지다.
그래서 사용자를 추가한다는 것은 k3s 의 client-ca 로 인증서를 하나 더 발급한다는 뜻이 된다.
관리자 O=system:masters, CN=system:admin
viewer O=cgv:viewers, CN=viewer 같은 CA 로 서명하고 O 만 다르게 한다
여기서 둘이 따라 나온다.
client-ca의 개인키를 가진 쪽은 아무 이름·아무 그룹으로나 인증서를 찍을 수 있다. 노드의client-ca.key가 그 키이고, 그것으로O=system:masters를 찍으면 그 순간 전권이다. 노드에 들어갈 수 있다는 것이 곧 클러스터 전권이라, 관리 접근을 좁히는 것이 RBAC 보다 앞선다.- 인증서에는 폐기 목록이 없다. 발급한 인증서가 유출돼도 취소할 방법이 만료뿐이라, 유효기간을 짧게 잡아야 회수까지의 시간이 줄어든다.
곁가지 — config.yaml 은 kubeconfig 와 다른 파일이다
이름이 비슷한데 방향이 반대다. config.yaml 은 k3s 에게 주는 입력이고, kubeconfig(k3s.yaml)는 k3s 가 만들어 내는 출력이다.
config.yaml |
k3s.yaml (= kubeconfig) |
|
|---|---|---|
| 자리 | 노드의 /etc/rancher/k3s/config.yaml |
노드의 /etc/rancher/k3s/k3s.yaml |
| 누가 만드나 | 사람이 쓴다 | k3s 가 기동하며 생성한다 |
| 언제 읽히나 | k3s 서비스가 시작할 때 | kubectl 이 쓸 때마다 |
| 내용 | 클러스터를 어떻게 세울지 | 어디에 붙고 누구로 붙을지 |
| 저장소 관리 | 한다 | 안 한다 — 자격이라 넣을 수 없다 |
config.yaml 이 kubeconfig 의 생성을 정하는 자리가 둘 있다. tls-san 에 적은 이름들이 서버 인증서에 들어가고 — 신·구 노드 주소를 둘 다 넣어 둬서 주소 이전 뒤에도 인증서가 깨지지 않았다 — write-kubeconfig-mode: 0644 는 k3s.yaml 의 파일 퍼미션을 정한다. 0644 는 노드에 로그인할 수 있는 사용자면 누구나 읽는다는 뜻이고, 읽으면 관리자 자격을 그대로 얻는다.
인가 — 이름에는 힘이 없다
인증이 끝난 시점에 확정된 것은 이름뿐이다. 그 이름으로 무엇을 할 수 있는지는 RBAC(Role-Based Access Control)이 정하고, 그 내용은 kubeconfig 가 아니라 클러스터 안의 오브젝트에 적혀 있다.
같은 CA 로 발급했다고 놓고 그룹만 바꿔 판정을 걸면 이렇게 나온다.
| 인증서의 그룹(O) | get pods -A |
get secrets |
|---|---|---|
system:masters |
yes | yes |
cgv:viewers |
yes | no |
| 존재하지 않는 이름 | no | no |
세 번째 줄이 인증과 인가가 갈리는 자리다. 인증서가 유효해 신원은 확정되는데 아무것도 못 한다 — 그 이름을 가리키는 오브젝트가 없어서다. 거꾸로도 성립한다: 클러스터에서 그 오브젝트를 지우면 kubeconfig 파일은 그대로인데 권한이 사라진다.
system:masters 가 전권인 이유도 그 이름이 특별해서가 아니라, 그 이름을 가리키는 오브젝트(RBAC)가 있어서다.
ClusterRoleBinding cluster-admin ← Group system:masters → ClusterRole cluster-admin
규칙 묶음과 연결
RBAC 오브젝트는 둘로 나뉜다.
- 규칙 묶음 — 무엇을 어떻게 할 수 있는지 적은 것.
Role과ClusterRole - 연결 — 그 규칙 묶음을 누구에게 붙이는 것.
RoleBinding과ClusterRoleBinding
규칙 묶음에는 "누구"가 없고, 연결에 "누구"(subjects)와 "어느 묶음"(roleRef)이 있다. 규칙 묶음만 만들고 연결하지 않으면 아무 일도 일어나지 않는다.
kind: ClusterRoleBinding
metadata: {name: cgv-viewer}
subjects: # 누구에게
- kind: Group
name: cgv:viewers
roleRef: # 어느 규칙 묶음을
kind: ClusterRole
name: view
넷 중 어느 조합이냐가 적용 범위를 정한다.
| 규칙 묶음 | 연결 | 적용 범위 |
|---|---|---|
Role |
RoleBinding |
그 네임스페이스에서만 |
ClusterRole |
RoleBinding |
그 네임스페이스에서만 — 같은 규칙을 여러 곳에 재사용할 때 |
ClusterRole |
ClusterRoleBinding |
모든 네임스페이스에서 |
Role |
ClusterRoleBinding |
성립하지 않는다 |
ClusterRole 이라는 이름이 "클러스터 전체 적용"을 뜻하지 않는다. 범위를 정하는 것은 연결 쪽(RoleBinding)이다 — 같은 ClusterRole 도 RoleBinding 으로 묶으면 한 네임스페이스에서만 적용되고, 규칙이 아무리 좁아도 ClusterRoleBinding 으로 묶이면 범위가 클러스터 전체가 된다.
Role 과 RoleBinding 은 metadata.namespace 를 갖고 그 값이 곧 적용 범위이고, ClusterRole 과 ClusterRoleBinding 에는 그 필드 자체가 없다.
규칙 하나는 셋으로 이뤄진다.
- apiGroups: [""] # "" 는 core 그룹(pods·services·secrets 등)
resources: [configmaps, secrets] # 무엇을
verbs: [get, list, watch] # 어떻게
get 은 이름을 지목해 하나를, list 는 그 종류 전부를 가져온다. list 는 오브젝트 본문을 담아 돌려주므로 Secret 에 대한 list 는 값을 읽는 것과 같다.
NetworkPolicy 와 같은 성질이 하나 있다 — 거부 규칙이 없고 허용의 합집합이다. 한 주체에 여러 연결이 붙으면 가장 넓은 것이 적용된다. 좁히려면 넓은 쪽을 없애야 하고, 좁은 것을 하나 더 붙이는 것으로는 되지 않는다.
파드의 신원 — ServiceAccount
파드는 사람처럼 인증서를 들 수 없다. 쿠버네티스가 파드용 신원을 직접 발급하고, 그 신원은 오브젝트로 존재한다 — ServiceAccount 다.
파드가 spec.serviceAccountName 으로 하나를 고르면 그 ServiceAccount 의 토큰이 파드 안에 파일로 들어간다. 파드가 API 서버를 부를 때 그 파일이 요청 헤더에 실리고, API 서버는 토큰을 검증해 system:serviceaccount:<네임스페이스>:<이름> 으로 신원을 확정한다.
파드가 침해되면 그 토큰이 함께 넘어간다 — API 서버를 부를 일이 없는 파드는 automountServiceAccountToken: false 로 토큰을 아예 넣지 않는다.
인가는 사람과 같은 기계다. 연결의 subjects 에 Group 대신 ServiceAccount 가 오는 것만 다르고, roleRef 가 규칙 묶음을 가리키는 것은 같다.
| 사람 | 파드 | |
|---|---|---|
| 신원 이름 | 인증서의 CN 과 O (kubeconfig) |
system:serviceaccount:<ns>:<이름> (SA) |
| 클러스터에 오브젝트로 있나 | 없다 — kubectl get user 는 없는 명령 |
있다 — kubectl get sa |
| 신원을 증명하는 수단 | 인증서 + 개인키 | ServiceAccount 토큰 |
| 그 수단을 누가 발급하나 | k3s 의 client-ca 로 사람이 발급한다 |
쿠버네티스가 자동으로 |
| 어디에 보관되나 | ~/.kube/config |
파드 안에 파일로 |
| 회수 방법 | 없다 — 만료를 기다린다 | 있다 — ServiceAccount 를 지우면 토큰이 무효가 된다 |
마지막 줄이 운영에서 갈리는 자리다. 사람 인증서는 유출되면 만료까지 손쓸 수 없어 유효기간을 짧게 발급하고, ServiceAccount 는 클러스터 안 오브젝트라 지우면 그 순간 끝난다.
'HomeLab' 카테고리의 다른 글
| 노트북 홈서버 k3s 구축기 - 13-3. 외부 공개와 실서비스 스펙 (0) | 2026.08.30 |
|---|---|
| 노트북 홈서버 k3s 구축기 - 13-2. 클러스터 내부 보안과 감시 (0) | 2026.08.27 |
| 노트북 홈서버 k3s 구축기 - 13-1. 네트워크 격리 (0) | 2026.08.26 |
| 번외 — 컨테이너 메모리와 런타임 메모리 (0) | 2026.08.17 |
| 번외 — 노드 메모리와 쿠버네티스 메모리 (0) | 2026.08.15 |