외부 서버에서 실행 중인 쿠버네티스 Pod에게 요청을 보내기 위해서는 Service 리소스를 이용해야 한다. Service 리소스는 외부의 요청을 노드 내부에서 실행 중인 Pod의 포트와 매핑해 주는 역할을 한다. 따라서 쿠버네티스 클러스터 내부에서 Pod로 띄워진 웹페이지나 백엔드 서버 등은 Service 리소스를 통해 외부와 통신할 수 있다.
외부 서버와 파드 간의 통신, 내부의 파드 간의 통신, kubectl 명령어로 사용자의 리소스 컨트롤을 도와주는 건 도대체 무엇일까? 바로 Kubernetes API이다.
리소스를 제어하는 모든 작업의 중심에는 Kubernetes API 서버가 있다. API 서버는 클러스터의 모든 상태와 설정을 관리하며, 이를 기반으로 사용자는 클러스터의 상태를 조회하고, 리소스를 생성, 업데이트, 삭제할 수 있다. Kubernetes API 서버가 클러스터의 "중추적인 컨트롤 타워" 역할을 수행하는 것이다.
Kubernetes API는 RESTful API로 설계되어 있으며, kubectl 명령어는 이 API를 호출하여 클러스터와 상호작용한다. 예를 들어, kubectl get pods 명령어는 Kubernetes API에 요청을 보내 현재 실행 중인 파드의 정보를 가져오는 방식으로 작동한다.
🤔 그렇다면 Kubernetes API를 이용하여 Postman이나 자바의 코드로 쿠버네티스 마스터 노드에게 HTTP 요청을 보내 Pod를 실행하거나 조회, 업데이트, 삭제 등을 수행할 수 있지 않을까 하는 궁금증이 생기기 마련이다.
이번 글에서는 그 테스트 과정을 간략하게 요약한다.
📚 클러스터 정보 보기
# 쿠버네티스 클러스터 서비스의 엔드 포인트 확인
$ kubectl cluster-info
Kubernetes control plane is running at https://<API_SERVER_IP>:6443
CoreDNS is running at https://<API_SERVER_IP>:6443/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.
- 여기에 나오는 API_SERVER의 주소는 마스터 노드의 주소이다.
- 그렇다면 여기서 이렇게 생각해 볼 수 있다.
- https://<API_SERVER_IP>:6443 이 경로로 요청을 정상적으로 보내고, 보낸 요청에 대한 응답받을 수 있다면 이를 이용해 외부에서 API 요청으로 쿠버네티스를 움직일 수 있지 않을까?
🤓 curl 명령어로 접근하기
$ curl https://10.x.x.x:6443
curl: (60) SSL certificate problem: unable to get local issuer certificate
More details here: https://curl.haxx.se/docs/sslcerts.html
curl failed to verify the legitimacy of the server and therefore could not
establish a secure connection to it. To learn more about this situation and
how to fix it, please visit the web page mentioned above.
- SSL certificate 관련 문제로 접근할 수 없음을 확인할 수 있다.
- 왜 SSL 에러가 발생하는 것일까?
- 그것은 바로 Kubernetes API 서버가 사용하는 인증서가 공식 CA 인증서가 아니기 때문이다.
- 중요한 사실은 Kubernetes API도 일반적인 API 통신과 다르지 않다는 것이다.
- 따라서, 우리에게 필요한 것은 Kubernetes에서 발급한 CA 인증서이다.
- 이 인증서로 SSL 연결을 요청하면 Kubernetes에서는 자신이 발급한 CA 인증서임을 확인하여 연결이 수립되는 방식이다.
- 이를 통해 Kubernetes API에서도 HTTPS 통신을 할 수 있다.
📃 쿠버네티스의 CA 인증서
- Kubernetes 클러스터를 구성할 때, 클러스터의 CA 인증서는 자동으로 생성된다.
- 클라이언트(Postman, Java 애플리케이션 등)가 Kubernetes API 서버와 통신하려면, 클러스터의 CA 인증서를 가져와서 로컬에 설정해야 한다.
- kubectl은 클러스터 생성 시 kubeconfig 파일에 CA 인증서를 포함시킨다.
- kubeconfig 파일을 확인해 보자.
CA 인증서 확인하기
$ cat ~/.kube/config
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: xxxxx
...
- kubeconfig의 certificate-authority-data에는 클러스터가 생성한 자체 CA 인증서가 있다.
CA 인증서 추출하기
$ kubectl config view --minify --raw --output 'jsonpath={..cluster.certificate-authority-data}' | base64 -d > k8s-ca.cert
- ca 인증서를 현재 경로에 저장하는 명령어다.
- --minify: 불필요한 정보를 제거하고 현재 활성화된 클러스터와 사용자 정보만 확인한다.
- --raw: json 형식 그대로 출력한다.
- -- output 'jsonpath': JsonPath 표현식을 이용해 certificate-authority-data만 추출한다.
- | base64 -d: certificate-authority-data 필드는 Base64로 인코딩 된 데이터를 담고 있으므로, 이를 디코딩하여 원본 인증서 형태로 변환한다.
$ sudo curl --cacert k8s-ca.cert https://10.x.x.x:6443
{
"kind": "Status",
"apiVersion": "v1",
"metadata": {},
"status": "Failure",
"message": "forbidden: User \"system:anonymous\" cannot get path \"/\"",
"reason": "Forbidden",
"details": {},
"code": 403
}
# 서버 인증서 우회하기
$ sudo curl -k https://10.x.x.x:6443
- 이 명령어는 위에서 추출한 CA 인증서를 사용하여 요청을 보낸 것이다.
- 성공적으로 통신이 되었다. -> ssl 인증서 문제를 해결했다.
- 그러나, Failure 되었으며 메시지로 접근이 금지되었다고 나온다.
- 403 에러 즉, 권한 때문에 거절당한 것이다.
- 참고로 curl -k로 엔드포인트 요청을 보내면 CA 인증서 확인 없이 요청이 가능하다.(우회)
👮🏻♂️ Kubernetes API Authorization
- 만약 외부에서 아무나 API 서버에 접근할 수 있다면 쿠버네티스 클러스터가 위험해진다.
- 예를 들면 모든 파드를 삭제하거나, 중요한 데이터 유출 같은 보안 문제가 발생할 수 있다.
- 그래서 Kubernetes는 요청의 권한을 확인한다.
- CA 인증서가 "안전한 연결"을 보장한다면, API 토큰은 "누가 요청했는가"를 확인한다.
- API 토큰은 Kubernetes에서 발급하는 작은 문자열로 요청자가 누구인지 증명하는 "ID 카드"와 같다.
- 쿠버네티스에서 누구인지 확인하는 것(권한)과, 암호화된 토큰에 관련된 리소스는 각각 ServiceAccount와 Secret이 있다.
🤝 ServiceAccount와 Secret
자동 생성되는 Secret (기본 동작)
- Kubernetes 1.24 이전에는 ServiceAccount가 생성될 때 자동으로 Secret이 생성되었다.
- 1.24 이전 버전의 Secret은 해당 ServiceAccount와 연결된 토큰 정보를 포함하고 있으며 API 요청 시 인증에 사용되었다.
Kubernetes 1.24 이상 (BoundServiceAccountToken 활성화)
- Kubernetes 1.24부터는 BoundServiceAccountTokenVolume 기능이 기본적으로 활성화되었다.
- 이제는 ServiceAccount에 Secret이 자동으로 생성되지 않고, 대신 Pod 내부에서 토큰 볼륨 마운트를 통해 토큰이 제공된다.
- 따라서, ServiceAccount만 생성한 경우 Secret이 생성되지 않는다.
나의 경우 1.32 버전이므로 Secret을 명시적으로 생성해야 한다.
- 나에게 필요한 것은 특정 ServiceAccount의 토큰이다.
- 이제부터 나올 내용은 쿠버네티스 리소스에 대한 학습이 되어있지 않다면 이해가 어려울 수 있다.
간단하게 이야기하면 이렇다.
- ServiceAccount는 쿠버네티스 클러스터에서 API 요청을 할 수 있는 계정이다.
- Role(ClusterRole)은 리소스에 대한 접근 권한을 명시하는 것이다.(ex. Pod 생성과 조회만 가능함을 명시)
- 이 둘을 합치면 특정 계정에 특정 역할(권한)이 부여되는 것이다.
- 이를 도와주는 것이 RoleBinding(ClutserRoleBinding)이다.
- RoleBinding(ClutserRoleBinding)은 계정과 계정의 접근 권한이 명시된 Role을 매핑한다.
- Role과 ClusterRole의 차이는 특정 노드에서만 유효한 권한인지, 클러스터 전체에 유효한 권한인지이다.
- Role은 특정 노드에서만 유효한 권한이며, ClusterRole의 권한 범위는 전체이다.
- 즉, Secret으로 ServiceAccount의 API 토큰을 발급하여 외부에서 해당 토큰을 사용하여 HTTP 요청을 보낸다.
- 해당 토큰의 주인인 ServiceAccount에게 부여된 권한에 한해 Kubernetes에게 API 요청을 보낼 수 있는 것이다.
- 이를 순서대로 진행해 보자.
1. ServiceAccount 확인(or 생성)
$ kubectl get sa
NAME SECRETS AGE
default 0 51m
# 직접 생성
kubectl create serviceaccount <service-account-name> --namespace=<namespace>
- sa는 Service Account의 약자이며 기본적으로 default라는 이름의 서비스 어카운트가 생성된다.
- 이걸 그대로 활용하든가 별도의 서비스 어카운트를 생성하자.
- 나는 API 요청을 사용해야 하는 서비스의 이름으로 sa 계정을 만들었다.
2. ClusterRole 생성
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: springboot-api-request-clusterrole # clusterrole 이름
rules:
- apiGroups: [""] # ""은 core API 그룹을 의미
resources: ["pods"] # 컨트롤 가능한 리소스
verbs: ["get", "list", "watch"] # 컨트롤 가능한 동작
- 외부에서 kubernetes API로 쿠버네티스를 컨트롤할 예정이기 때문에 해당 API가 필요로 하는 리소스와 동작만을 정의한 제한적인 Role을 생성하는 것이 좋다.
- 예를 들어, 파드를 조회하고 생성만 가능하도록 역할을 제한한다.
🚨 보안문제
$ kubectl describe clusterrole cluster-admin
Name: cluster-admin
Labels: kubernetes.io/bootstrapping=rbac-defaults
Annotations: rbac.authorization.kubernetes.io/autoupdate: true
PolicyRule:
Resources Non-Resource URLs Resource Names Verbs
--------- ----------------- -------------- -----
*.* [] [] [*]
- cluster-admin 등 기본으로 생성되어 있는 Role을 활용하려고 한다면 주의해야 한다.
- 해당 role은 모든 노드의 모든 리소스 및 동작에 접근할 수 있는 권한이 부여된다.
- 이 Role과 매핑되어 있는 Service Account의 토큰이 유출된다면, 외부 api 요청으로 인한 심각한 서버 보안 문제가 발생할 가능성이 있다.
- 테스트에만 사용하고, 실무에서는 사용하지 말자.
3. ServiceAccount & Clusterrole(Role) 바인딩
$ kubectl create clusterrolebinding <binding-name> --clusterrole=<clusterrole-name> --serviceaccount=<namespace>:<serviceaccount-name>
# 출력
clusterrolebinding.rbac.authorization.k8s.io/<binding-name> created
- 접근 가능한 리소스, 수행할 동작이 담겨있는 clusterrole(role)과 해당 역할이 부여될 serviceaccount를 바인딩한다.
- 일반 Role의 경우 clusterrolebinding 대신 rolebinding을 사용하면 된다.
- 이 작업을 마치면 Service Account - CluterRoleBinding(RoleBinding) - ClusterRole(Role) 간의 매핑이 이루어지며 특정 sa에 대한 접근 권한이 설정되게 된다.
4. Secret 생성
apiVersion: v1
kind: Secret
metadata:
name: service-account-token # Secret 이름
annotations:
kubernetes.io/service-account.name: "default" # 토큰을 생성할 Service Account의 이름
type: kubernetes.io/service-account-token
- Secret 리소스는 보안에 관련된 토큰 등을 생성해주는 리소스이다.
- Service Account token을 만들어주는 빌트인 타입이 있어 이를 활용했다.
- 이제 이 Secret의 토큰을 HTTP 요청 헤더에 싣으면 끝이다. API URL이나 쿼리마라미터, 요청 바디에 특정 ServiceAccount를 사용하겠다고 명시할 필요가 없다.
- kubernetes.io/service-account-token 타입의 토큰 자체가 사용하고자 하는 Service Account와 매핑되어있기 때문이다.
5. 생성된 Secret 확인
$ kubectl get secret
NAME TYPE DATA AGE
service-account-token kubernetes.io/service-account-token 3 6s
- get 명령어를 통해 생성된 Secret을 확인할 수 있다.
- 이제 Service Account의 토큰을 활용하여, 외부에서 해당 계정이 보유한 접근권한(clusterrole or role) 내의 api 요청을 진행할 수 있다.
6. Secret의 토큰 사용 준비
$ kubectl get secret <SECRET_NAME> -o jsonpath="{.data.token}" | base64 -d
# 환경 변수에 담기
$ TOKEN=$(kubectl get secret <SECRET_NAME> -o jsonpath="{.data.token}" | base64 -d)
# 출력 해보기
$ echo $TOKEN
- 위 명령어에 Secret 리소스 이름을 붙여 리소스 내의 token을 출력해 보자.
- 아니면 환경변수에 담아서 echo 명령어로 출력해 볼 수 있다.
- 아래 테스트를 위해서는 환경변수에 담자.
7. 로컬 테스트
# 로컬 테스트
sudo curl --cacert k8s-ca.cert -H "Authorization: Bearer $TOKEN" https://10.xxx.xxx.xxx:6443
# 출력
{
"paths": [
"/.well-known/openid-configuration",
"/api",
"/api/v1",
"/apis"
...
- 위 명령어로 CA 인증서와 토큰을 가지고 정상적으로 요청을 수행할 수 있음을 확인했다.
- 참고로 위 테스트는 cluster-admin을 활용하여 API 루트 경로까지 전부 오픈한 채 진행한 테스트이다. pod 등 일부만 접속 가능하게 설정한 Service Account의 토큰으로 테스트하면 실패할 것이다.
- 고로, 자신이 허용한 API url을 가지고 테스트해 보자. 아래는 Kubernetes 공식 문서에서 제공하는 API Reference이다.
- Postman이나 스프링부트의 자바 코드 등으로 테스트할 때에도 같은 방법으로 테스트하면 된다.
8. 포스트맨 테스트
🚫 허용되지 않은 API 접근 시
- Clusterrole에 명시한 접근 리소스와 접근 동작에 벗어난 요청을 했을 시에는 403 에러와 함께 해당 Service Account로는 요청한 리소스 동작을 수행할 수 없다고 친절히 응답해 준다.
✅ 허용된 API 접근 시
- Clusterrole에 명시한 API 요청을 보내게 되면 정상적으로 작동함을 확인할 수 있다.
🤯 고민해 볼 사항
- 최신 Kubernetes 방식은 주로 Pod 내에서 동적으로 토큰을 갱신하는 방식이다.
- 외부에서 쿠버네티스 API를 호출하는 경우에는 적합하지 않다.
- 그렇다고 외부에서 API 요청을 통해 파드를 생성하거나 삭제한다는 이유로 토큰을 발급하여 사용하면 토큰 노출이라는 위험을 안고 가게 된다.
- 따라서 보안을 위해 외부에서 요청을 보내는 상황에서는 쿠버네티스 API 토큰을 안전하게 관리하고 주기적으로 갱신하는 방법을 고려해야 한다.
- 토큰이 유출되지 않도록 관리하고, 자동으로 재발급받는 메커니즘을 설정하는 것이다.
JWT 사용 시의 클라이언트 방식대로 생각해 보기
- Access, RefreshToken처럼 토큰을 수동으로 갱신하고 이를 Spring Boot 애플리케이션에 안전하게 적용하는 방법을 생각해 볼 수 있겠다.
- 즉, 쿠버네티스 서비스 계정의 토큰을 재발급받은 후, 이를 애플리케이션에 동적으로 적용하는 것이다.
참고
https://kubernetes.io/docs/concepts/configuration/secret/#serviceaccount-token-secrets
https://coffeewhale.com/apiserver
'[DevOps] > Kubernetes' 카테고리의 다른 글
😡 1 node(s) had untolerated taint(node.kubernetes.io/disk-pressure:)의 원인 파악과 해결 (0) | 2025.01.15 |
---|---|
Docker image -> Containerd image로 변환하는 방법 (2) | 2025.01.07 |
쿠버네티스 설치 및 노드 연결 완벽 정리 (Oracle Linux or Ubuntu, containerd, flannel) (5) | 2024.12.19 |
CKA 기출 문제 복기 및 접근법 정리[24.11.24 응시] (0) | 2024.11.27 |
CKA 시험 접수 -> 응시 -> 합격 후기(1트 합격) (9) | 2024.11.26 |