PKOS Study #5 – Security

PKOS Study #5는 Security 세션으로 Node의 Metadata 탈취와 그에 대한 보안을 IRSA을 통해 진행하고 kubescape, Polaris을 통해 Deployment, Pod에 대해 점검하는 부분을 진행할 예정이다.

0. 환경 구성

이전 실습 때의 환경을 그대로 사용할 예정이다. 다만, 이전 실습 때 2a AZ에 대해 Worker Node를 2개로 확장했던 부분(Node Down Alert Test 목적)이 있어 이건 다시 1개로 축소하고 진행할 예정이다.

ap-northeat-2a에 대해 Size을 1로 축소하는 명령과 Update을 진행했다.

Update가 완료됐고 기존 2대였던 2a의 Worker Node가 1개로 변경되었다.

Pod에서 EC2의 Metadata을 탈취하기 위해 2대의 Worker Node 중 2a AZ에 있는 Worker Node의 IMDSv2 보안을 제거해야 한다.

변경 전 화면이다

위처럼 변경을 진행하고 Rolling Update을 진행한다.

생각해보면 앞서 Worker Node을 2대에서 1대로 변경할 때 해당 내용도 같이 적용해서 변경했으면 어땠을까 하는 생각이 들었다.

사전에 원복하고 진행해야 겠다는 생각이 먼저 들고 이후 환경 설정을 하다보니 Rolling Update 하는데 시간이 2배로 발생하게 됐다.

위와 같이 Rolling Update가 완료됐고 본격적으로 스터디 실습을 시작하도록 하겠다.

1. EC2 IAM&Metadata

실습 환경 구성 때 2대의 Worker Node 중에서 1대의 Worker Node(2a AZ)에 대해 IMDSv2 보안을 제거하였었다.

제대로 설정이 진행되었다면 2a AZ Node에 배포 된 Pod에서는 Node EC2에 대한 Metadata를 확인할 수 있을 것이다.

아래와 같이 확인을 진행해보았다.

<em># 파드 이름 변수 지정</em>
PODNAME1=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[0].metadata.name})
PODNAME2=$(kubectl get pod -l app=netshoot-pod -o jsonpath={.items[1].metadata.name})

<em># EC2 메타데이터 정보 확인</em>
kubectl exec -it $PODNAME1 -- curl 169.254.169.254 ;echo
kubectl exec -it $PODNAME2 -- curl 169.254.169.254 ;echo

Pod 1, 2에 대해 각각 변수 지정을 해주고 exec을 통해 각 Pod에서 AWS Metadata 호출인 curl 169.254.169.254을 실행해주었다.

Pod1은 Metadata가 확인되지만 Pod2에서는 Metadata가 확인되지 않는다. 이렇게만 보면 Pod1이 2a AZ Node에 배포 된 Pod로 보이지만 혹시 모르니 추가적으로 확인을 진행해보았다.

kubectl get pod -o wide로 2개의 Pod에 대해 Node 정보를 확인했고 AWS Console에서 EC2 정보를 통해 Instance ID값을 불러왔다.

Pod1의 이름인 netshoot-pod-7757d5dd99-5g9bw의 Node는 i-0ec2490cd0951b339이다. i-0ec2490cd0951b339의 Instance Name은 2a AZ의 EC2임을 확인할 수 있었다. 이와 같이 IMDSv2 보안을 제거한 EC2에 배포 된 Pod는 해당 EC2의 Metadata을 볼 수 있음을 확인할 수 있다. 이 점은 보안 취약점이라고 볼 수 있다. Pod의 특정 권한이 탈취되었을 때 Pod을 통해 Node 역할을 하는 EC2의 정보 조회는 물론 제어권까지 가져올 수 있기 때문이다.

특히 위처럼 security credentials 정보를 조회하면 Accesskey, SecretKey 그리고 Token까지 확인할 수 있기 때문에 AWS Account 측면의 보안에도 매우 취약하게 된다.

그럼 위에서 확인한 Accesskey, Secretkey, Token으로 어떤 작업까지 가능한지 한 번 확인을 진행해본다.

awscli가 설치 된 Image을 이용해서 Pod을 배포한다음 테스트를 진행한다.

Pod에 bash 연결해 Accesskey, Secretkey, Token 입력 후 aws cli을 통해 instance 정보를 출력해보았는데 역시나 정보를 받아올 수 있었다. 이는 사실 IMDSv2 보안이 제거된 것과는 무관하다. 무슨 뜻이냐면 IMDSv2 보안 제거가 되었기 때문에 정보를 확인할 수 있는 것이 아니라 보안이 제거 된 상태에서 긁어온 Security Credentials 정보를 통해 AWSCLi에 인증 받아 정보를 받아온 것이기 때문이다.

여기서 탈취한 Credentials 정보의 권한이 FullAccess라면 해당 정보를 통해 어떤 작업이든 가능하다고 볼 수 있다. 물론 Expired Time에 따라 시간이 지나면 사용할 수 없을 수 있지만 이미 IMDSv2 보안 제거로 인해 얼마든지 Metadata을 불러올 수 있기 때문에 이 부분은 전혀 문제가 되지 않는다.

앞서는 IMDSv2 보안이 제거 된 Node Instance의 정보를 통해 AWS Account의 접근까지 위험한 상황이 발생하는 것에 알아보았다면 이제는 이 부분에 대한 방어책을 알아봐야 한다.

IRSA(IAM Roles for Service Accounts)을 사용해서 진행할 계획이다. 위에 내용을 보면 생성 된 Pods는 Node의 IAM을 가져오게 되어있다. 이 부분을 IRSA을 통해 Pod들 마다의 권한을 부여할 계획이다. 그렇게 되면 Metadata 유출로 인한 보안 사고를 방지할 수 있다. 

스터디 실습 환경은 EKS가 아닌 kOps을 활용해서 진행하기 때문에 kOps에서 IRSA을 사용하는 방안으로 진행할 예정이다. IRSA을 kOps에서 진행하기 위한 설정은 아래 링크를 참고하였다.Cluster Resource – kOps – Kubernetes OperationsThe Cluster resource The Cluster resource contains the specification of the cluster itself. The complete list of keys can be found at the Cluster reference page. On this page, we will expand on the more important configuration keys. The documentation for tkops.sigs.k8s.io

사전에 준비해야 하는 내용은 다음과 같다.

IAM Policy : 나는 S3에 대해 ListAllMyBucket을 적용한 Policy을 미리 생성하였다.

S3 Bucket : Public Read가 가능한 Bucket을 생성하였다.

<em>#kops edit cluster</em>

spec:
  podIdentityWebhook:
    enabled: true
  serviceAccountIssuerDiscovery:
    discoveryStore: s3://publicbucket
    enableAWSOIDCProvider: true
  iam:
    allowContainerRegistry: true
    legacy: false
    serviceAccountExternalPermissions:
    - aws:
        policyARNs:
        - arn:aws:iam::accountNo:policy/irsa-pol
      name: irsa-iam
      namespace: default

위와 같이 cluster edit을 해주고 rolling update를 진행했다.

참고로 policyARN에 표시 된 Policy는 위 화면과 깉이 ListAllMyBuckets 작업이 허용되어있다. 후에 해당 권한으로 S3 ls을 실행해서 테스트할 예정이다.

Rolling Update가 진행되는 동안 Public Bucket으로 설정한 S3 Bucket에 들어가보았고 위 화면과 같이 s3bucket/openid/v1/jwks 경로에 토큰 정보가 생성된 것을 확인할 수 있었다.

마찬가지로 IAM 자격 증명 공급자에도 위와 같이 공급자가 생성된 것을 확인할 수 있다. 공급자 주소는 위에서 사용 된 s3 bucket의 경로가 표시된다.

긴 기다림 끝에 Rolling Update도 완료되었다.

이제 ServiceAccount을 통해 Pod에 권한을 부여하고 해당 Pod에서 권한을 잘 받았는지 확인해보려고 한다.

우선 Service Account 없이 생성한 awscli pod에서는 역시나 권한이 없다고 나온다.

그럼 이제 Service Account을 포함해서 배포한 Pod에서 테스트를 진행해본다.

S3 Bucket List을 불러오는데 성공했다. 이렇게 Pod에 각각 IAM 권한을 부여하는 방식으로 보안을 유지할 수 있게 된다.

물론, IMDSv2 보안을 유지함으로 인해 Metadata 자체를 노출시키지 않는 것도 중요하니 이 부분을 까먹지 말아야 한다.

2. kubescape을 통한 Cluster 취약점 점검

kubescape는 보안 권고 사항 기반으로 k8s Cluster(yaml, helm 등)의 취약점을 점검해주는 서비스이다.

curl -s https://raw.githubusercontent.com/kubescape/kubescape/master/install.sh | /bin/bash

kubescape download artifacts
tree ~/.kubescape/
cat ~/.kubescape/attack-tracks.json | jq

설치는 간단하게 install.sh 파일을 통해 진행 된다. 설치를 하고 articats를 다운 받는다.

설치도 하고 Artifacts도 받았으니 이제 Scan을 진행해보도록 한다.

Scan을 실행하면 위와 화면이 나오면서 Scan이 진행된다.

Scan이 완료되면 위와 같이 결과값이 나온다. 생각보다 Critical이 꽤 많은 상황이다. 아마도 이것저것 테스트를 하며 설정을 변경했기 때문인 것 같다.

kubescape은 간략하게 확인하는 내용만 진행하고 polaris로 넘어간다. Polaris는 Webpage도 제공하기 때문에 Cirtical, Warning 등 내용을 확인하며 수정하는 테스트를 병행하기 수월하기 때문이다.

3. polaris을 통한 보안 점검 진행

위에서 진행한 kubescape와 비슷한 서비스인 Polaris를 실습해보려고 한다.

Polaris는 리소스 구성을 검증하고 수정하는 k8s용 오픈소스 Policy Engine이다. 30개 이상의 기본 정책을 포함하고 있고 JSON 기반의 사용자 정책도 적용할 수 있다.

설치는 위와 같이 진행 된다. GitOps 이후 오랜만에 helm install을 이용해서 설치하는 실습인 것 같다.

배포 된 후 연결 된 도메인에 접근하면 위와 같은 화면을 볼 수 있다.

Score는 Polaris가 갖고있는 모범 사례 정책 대비 내 구성 내역의 점수를 나타낸다. 68%라는 건 32% 정도 부족한 부분을 갖고 있다고 보면 좋을 것 같다. 당연히 점수가 높을수록 권고 사항을 많이 갖추고 있다고 볼 수 있다.

Namespace Filter을 적용할 수도 있다. 나는 GitOps 때 생성한 argocd를 Filter해보았다.

argocd Namespace만의 점수를 볼 수 있다. 아까 전체적인 점수는 68%에 Grade는 D+였는데 argocd는 78%에 C+로 전체보다는 비교적 높은 점수임을 알 수 있다. 그리고 dangerous checks도 0개이니 비교적 안전하게 유지 중이라고 볼 수 있다.

이번엔 Deployment을 분석하기 위해서 Namespace Filter을 다시 Default로 변경하였고 그 중에 아까 실습 때 배포한 netshoot-pod Deployment을 분석해보았다. 기준 정책 도달한 내역보다 도달하지 못한 내역들이 더 많은 것으로 보인다. 그 중에 수정할만한 내용을 찾아보겠다. Should not be allowed to run as root 메시지를 보면 Root 권한으로 실행시킨다는 것을 알 수 있다. 이 문제를 해결하기 위해 yaml 파일을 수정한다.

    spec:
      containers:
      - name: netshoot-pod
        image: nicolaka/netshoot
        command: ["tail"]
        args: ["-f", "/dev/null"]
        <em>#secuirtyContext을 runAsNonRoot 설정해준다.</em>
        securityContext:
          runAsNonRoot: true

위와 같이 securityContext에 runAsNonRoot을 입력하고 다시 배포를 진행한다.

Container netshoot-pod 섹션에 root 관련 문구가 Passing Check로 바뀐 것을 알 수 있다. 나머지 2개의 Critical도 수정해보았다.

image Tag을 입력해주었고 앞서 설정한 runAsNonRoot 밑에 privileged을 false로 작성하고 Capabilities을 설정해 권한을 제한하였다.

Critical을 모두 제거된 것을 확인할 수 있다. Warning도 비슷한 방식으로 제거할 수 있지만 우선은 Critical만 제거하는 것으로 진행한다.

지금까지는 Polaris을 통해 이미 배포한 Pod에 대한 점검만을 진행했었다. 그럼 점검 사항에 위배되는 배포를 진행할 때 방지하는 방법은 없을까? polaris webhook을 사용하면 된다.

<em># webhook 활성화 적용</em>
helm upgrade polaris fairwinds-stable/polaris --namespace polaris --version 5.7.2 --reuse-values --set webhook.enable=true 
kubectl get pod,svc -n polaris

위처럼 Webhook 기능을 활성화 하고 Pod 배포를 진행한다.

배포에 사용한 yaml은 위에 Critical 해소 때 사용한 구문을 지우고 수정한 버전의 yaml을 사용하였다.

배포가 진행되지 않은 것을 알 수 있다. Privilege, run as root, Image Tag 등에 대해 문제가 있다고 나오고 get pod를 실행했을 때도 해당 pod가 배포되지 않은 것을 확인할 수 있었다.

4. 마치며

이번 세션을 마지막으로 kOps 스터디가 마무리되었다. 5주라는 짧은 시간 동안 kOps을 통해 k8s을 배울 수 있었다. 단순하게 k8s을 구성하고 pod들을 배포하는 실습이 아닌 GitOps 환경을 구성하고 Prometheus와 Grafana을 통해 Monitoring&Alert 설정도 진행했었다.

그리고 마지막으로는 Security 부분까지 진행해볼 수 있었는데 이 부분이 제일 도움이 되었던 것 같다. 프로젝트를 진행하다보면 요건 사항 중에 보안에 대한 부분이 사실 가장 많이 나올 수밖에 없는데 이때 Public Cloud Native로 조절할 수 있는 부분과 다른 서비스를 사용하는 방안이 있는데 이런 것들을 병합하여 사용할 수 있다는 걸 알게 된 게 가장 큰 수확이라고 생각한다.

스터디는 종료됐지만 미흡했거나 제대로 이해하지 못했던 부분들은 다시 한 번 처음부터 진행해보면서 나름대로 정리하는 기회로 삼으면 좋을 것 같다.

유익한 스터디를 기획하고 진행해주신 가시다님(Blog링크)께 감사 인사를 드린다.