solve-my-curiosity

TLS handshake + ServiceAccount 본문

K8S

TLS handshake + ServiceAccount

curiosity314 2025. 6. 8. 15:26

목적 : TLS 핸드쉐이크 과정을 통해 https 개념을 알아보고 더 나아가 k8s에서 ServiceAccount의 역할을 알아보자. 

 

TLS 핸드쉐이크는 TCP 3-way handshake를 한 후 TLS handshake를 하고 HTTPS통신을 하는 과정이다. 

 

TCP 3-way handshake는 SYN / SYN+ACK / ACK 이 3과정으로 알 수 있고 

 

TLS handshake는 

 

1) Client Hello
2) Server Hello (+ cert.pem)

3) 인증서 검증 (ca.crt)

4) 키 교환 (공개키 암호화)

5) 세션키 설정

6) Secure 통신 시작

 

이 과정으로 이루어진다. 

 

2)에서 서버가 주는 cert.pem은 서버의 인증서이고

3)에서 클라이언트의 ca.crt는 CA기관의 공개키이다. ca.crt로 cert.pem을 검증해보는 것이고 그 안에서 서버의 공개키를 꺼내서 

4) 클라이언트의 대칭키를 서버의 공개키로 암호화 후 보낸다. 

5) 서버는 서버의 비밀키로 클라이언트의 대칭키를 복호화 하고 세션키를 설정하게 된다. 

 

 이것이 TLS 핸드쉐이크라고 할 수 있다. 

 

하지만 이건 구식 방식이고 

 

💡 참고: 현대적 TLS에서는

  • 키는 서로 협상해서 공유된 비밀로 만든다 (ECDHE)
  • 서버의 cert.pem은 인증만 한다 (진짜 서버 맞는지)
  • 서버의 공개키로는 직접 암호화 안 한다

 

라고 한다...

 

이제는 k8s의 serviceaccount에 대해 알아보자 

serviceaccount의 의미를 계속 헷갈렸었는데 한마디로 말하면 `Pod에게 인격을 주는 것(?)`라고 생각할 수 있을 것 같다.

User가 kube API 서버에 인증을 하고 요청을 하는 것인데 그것이 User가 Pod로 변한다면 

그때 부여하는 것이 serviceaccount이기 때문이다. 

 

https://somaz.tistory.com/221

 

이제 실습을 해보자

NS : test-ns이다. 

 

1) serviceaccount 생성

k create serviceaccount my-sa

 

2) Pod 생성 (serviceAccountName을 적어주는게 포인트이다)

apiVersion: v1
kind: Pod
metadata:
  name: curl-test-pod
  namespace: test-ns
spec:
  serviceAccountName: my-sa
  containers:
  - name: curl
    image: curlimages/curl
    command: ["sleep", "3600"]

 

3) Pod 안에 접속해서 API 서버에 curl 날려보기 

k exec curl-test-pod -it -- sh

~ $ TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

~ $ cat /var/run/secrets/kubernetes.io/serviceaccount/token
eyJhbGciOiJSUzI1NiIsImtpZCI6ImtHd0hEc2doZGo1ZWNOX2psaFI0RFVsTzE2eEFscG9DV0c0TVhieGhiREkifQ.eyJhdWQiOlsiaHR0cHM6Ly9rdWJlcm5ldGVzLmRlZmF1bHQuc3ZjLmNsdXN0ZXIubG9jYWwiXSwiZXhwIjoxNzgyMjE4OTc3LCJpYXQiOjE3NTA2ODI5NzcsImlzcyI6Imh0dHBzOi8va3ViZXJuZXRlcy5kZWZhdWx0LnN2Yy5jbHVzdGVyLmxvY2FsIiwianRpIjoiNzc3NDAyZTctYWRkYy00MTEzLWE3MGEtNzMwYjhhMGI2ZDFmIiwia3ViZXJuZXRlcy5pbyI6eyJuYW1lc3BhY2UiOiJ0ZXN0LW5zIiwibm9kZSI6eyJuYW1lIjoibG9jYWwtY2x1c3Rlci1jb250cm9sLXBsYW5lIiwidWlkIjoiMTE4YjUwYmYtMzg0Ni00Mzk0LWJmZTMtMTg2YzcwZWEwNWFlIn0sInBvZCI6eyJuYW1lIjoiY3VybC10ZXN0LXBvZCIsInVpZCI6IjY1N2RjZmMwLTE1OTYtNDdlMC05NDAzLTQ3OWM4NmIxYThjNCJ9LCJzZXJ2aWNlYWNjb3VudCI6eyJuYW1lIjoibXktc2EiLCJ1aWQiOiJhOTQ5ZmU2NC0xYmM2LTRlYzAtOTM5Mi05YTQzNWE0Y2FjMjEifSwid2FybmFmdGVyIjoxNzUwNjg2NTg0fSwibmJmIjoxNzUwNjgyOTc3LCJzdWIiOiJzeXN0ZW06c2VydmljZWFjY291bnQ6dGVzdC1uczpteS1zYSJ9.A5k-RYz3OMxMFjwtKCKHBOFKz_2PrhXQGdjIQwzUg0op4b4e7AmIjddCrubKl_8WjorfVhAkaMd2XkCKv_S6CsdQpfgIwxdsn2GcZ6vyHEWiiKcBFNnoZIumt3wBZv6Eg0HKuR0gSOY3qdzwDcIuvAfOF2jIMPKR7RpC8-_MN7fCYdp1zxMMyxQWumc8974yrWqUEFjq2uQKKjnRgEqP1Nz0NYUzcXLNUjMbniemh_nbGeR97Etu7ktq2U04pRMnQJEg3qhVkoBwAxGk3AGRyeWkKUH-1Xzh3-vwVao4-PykEiacylgBfP6UvVHu5WSK4Q8jRf_r43TtnhHfBnC55A~ $ echo $KUBERNETES_SERVICE_HOST
10.96.0.1
~ $ curl -sSk -H "Authorization: Bearer $TOKEN" \
>   https://$KUBERNETES_SERVICE_HOST/api/v1/pods
{
  "kind": "Status",
  "apiVersion": "v1",
  "metadata": {},
  "status": "Failure",
  "message": "pods is forbidden: User \"system:serviceaccount:test-ns:my-sa\" cannot list resource \"pods\" in API group \"\" at the cluster scope",
  "reason": "Forbidden",
  "details": {
    "kind": "pods"
  },
  "code": 403

 

message를 잘 보면 나오지만 serviceaccount:my-sa는 pods를 나열할 수 없다 403 에러 뜬다. 이렇게 나온다. 

serviceaccount를 부여했지만 그 serviceaccount에 아무것도 권한을 주지 않았기 때문에 에러가 뜬다.

이제는 권한을 주자. 

 

4) Role 생성

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  namespace: test-ns
  name: pod-reader
rules:
- apiGroups: [""] # "" indicates the core API group
  resources: ["pods"]
  verbs: ["get", "watch", "list"]

 

Role을 만들었으니 이제 이 Role을 serviceaccount에 부여하자 

 

5) RoleBinding 생성

apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: read-pods-binding
  namespace: test-ns
subjects:
- kind: ServiceAccount
  name: my-sa
  namespace: test-ns
roleRef:
  kind: Role #this must be Role or ClusterRole
  name: pod-reader
  apiGroup: rbac.authorization.k8s.io

 

 

Rolebinding을 통해서 ServiceAccount와 Role을 매핑시켜주었다. 

 

6) 다시 되는지 확인 

https://$KUBERNETES_SERVICE_HOST/api/v1/namespaces/test-ns/pods
{
  "kind": "PodList",
  "apiVersion": "v1",
  "metadata": {
    "resourceVersion": "1129322"
  },
  "items": [
    {
      "metadata": {
        "name": "curl-test-pod",
        "namespace": "test-ns",
        "uid": "657dcfc0-1596-47e0-9403-479c86b1a8c4",
        "resourceVersion": "1128647",
        "creationTimestamp": "2025-06-23T12:49:37Z",
        "annotations": {
          "kubectl.kubernetes.io/last-applied-configuration": "{\"apiVersion\":\"v1\",\"kind\":\"Pod\",\"metadata\":{\"annotations\":{},\"name\":\"curl-test-pod\",\"namespace\":\"test-ns\"},\"spec\":{\"containers\":[{\"command\":[\"sleep\",\"3600\"],\"image\":\"curlimages/curl\",\"name\":\"curl\"}],\"serviceAccountName\":\"my-sa\"}}\n"
        },
        ~~~~~~
        "startTime": "2025-06-23T12:49:37Z",
        "containerStatuses": [
          {
            "name": "curl",
            "state": {
              "running": {
                "startedAt": "2025-06-23T12:49:44Z"
              }
            },
            "lastState": {},
            "ready": true,
            "restartCount": 0,
            "image": "docker.io/curlimages/curl:latest",
            "imageID": "docker.io/curlimages/curl@sha256:9a1ed35addb45476afa911696297f8e115993df459278ed036182dd2cd22b67b",
            "containerID": "containerd://b28cf33345df3545fb88c11f0d9ce1c682eda86ec3c775cb320385dfddd906f2",
            "started": true
          }
        ],
        "qosClass": "BestEffort"
      }
    }
  ]

 

 

이렇게 잘 되었다. 

 

여기서 알 수 있는 점은 Pod안의 Token을 이용한다는 점이고 kube API 서버의 위치가 자동으로 환경변수로 들어가있다는 점이다. 

 

ServiceAccount를 사용하게 되면 Pod안에 Token이 /var/run/secrets/kubernetes.io/serviceaccount/token 안에 생기고 

KUBERNETES_HOST / KUBERNETES_PORT 값들이 자동으로 박히게 된다. 

 

경로 내용

/var/run/secrets/kubernetes.io/serviceaccount/token 📌 ServiceAccount 토큰 (JWT 형식)
/var/run/secrets/kubernetes.io/serviceaccount/ca.crt 📌 API 서버의 CA 인증서
/var/run/secrets/kubernetes.io/serviceaccount/namespace 📌 이 Pod가 속한 네임스페이스

 

그럼 이 ServiceAccount로 API 서버에 "무엇을 하려고" 통신을 하는걸까? 

 

대답은 GPT가 너무 잘 대답해주어서 가져왔다.

1. 🔁 Controller나 Operator가 리소스를 감시할 때

예: ReplicaSet, Deployment, Custom Operator 등

  • 컨트롤러는 "지금 있는 Pod 개수" 를 주기적으로 조회해서,
    부족하면 새로 만들고, 많으면 삭제함
  • 이때 API 서버에서 pods를 list/get/watch 해야 하니까
    → ServiceAccount가 필요함
yaml
복사편집
rules: - apiGroups: [""] resources: ["pods"] verbs: ["list", "watch"]

2. 🧠 앱이 자기 주변 상태를 파악하려 할 때

예: 클러스터 내 다른 서비스가 몇 개 있는지 알고 싶을 때

  • 서비스 디스커버리
  • 메트릭 수집기 (Prometheus exporter, FluentBit)
  • 모니터링 대상 찾기

→ Pod 목록을 읽어서 어떤 앱이 실행 중인지 확인함
→ pods를 list하거나 endpoints를 조회하는 권한이 필요함


3. 📦 CI/CD Agent가 리소스를 배포·관리할 때

예: ArgoCD, Tekton, Jenkins

  • Pod 내부에서 Deployment를 생성하거나 삭제해야 함
  • kubectl apply 하는 것처럼 실제 리소스를 API로 보냄

→ 이때도 ServiceAccount가 API를 호출함
→ 그래서 그 계정에 Role/ClusterRole을 연결해줘야 함


4. 🧪 애플리케이션 테스트/자기관리 목적

예:

  • 스스로 “지금 내 Pod의 상태가 뭔지”, “내가 속한 네임스페이스의 자원은 어떤지” 확인
  • 예: downwardAPI나 fieldRef로는 부족할 때

보면볼수록 k8s 진짜 잘 만든것 같다. 

'K8S' 카테고리의 다른 글

Cert-Manager Mini Project (1)  (2) 2025.06.12
X-Forwarded-For + Ingress(K8S)  (0) 2025.06.08
Kubernetes의 Log System 를 알아보자  (0) 2025.06.01
인턴 트러블슈팅 후기 2  (2) 2025.01.12
인턴 트러블슈팅 후기 1  (1) 2025.01.08