..

pkos) aws에서 권한 훔치기

instance metadata

실습환경은 어느 때와 같이 kops로 만든 cluster이지만, 취약하게 만들기 위하여 instanceMetadata이 필요했다. 수정에 앞서 우선.. 인스턴스 메타데이터가 뭔지부터 찾아봤다.

독스를 보면 ‘인스턴스 메타데이터는 실행 중인 인스턴스를 구성하거나 관리하는 데 사용할 수 있는 인스턴스에 대한 데이터입니다’이라고 나와있다. 인스턴스와 관련된 모든 정보를 가지고 있는 것이라 볼 수 있다. 🔗

아무런 설정 없이 생성을 하면 기본적으로 IMDSv2(instance metadata service version2)로 설정되어있다.

이 때 인스턴스에 접근하여 독스의 내용에따라 ‘인스턴스 메타데이터 서비스(IMDS)의 IPv4 주소를 사용합니다 169.254.169.254’ 로 호출을 시도해봤다. IMDSv2의 경우 메타데이터에 접근하려면 세션 토큰이 필요하기 때문에 권한이 부족하여 401 오류가 발생했다. 이와 다르게 IMDSv1의 경우는 세션토큰 없이 응답을 받을 수 있다고 하여 변경을 해보았다. 🔗

1-0

콘솔에서의 인스턴스 요약에 보면 IMDSv2: Required라고 있는 것을 ‘작업>인스턴스 설정>Modify instance metadata options’에 가서 Optional로 변경해주었다. 이 경우, v1과 v2가 요청에 따라 구분되어 동작한다. 토큰없이도 응답을 받은 것을 아래이미지로 확인 할 수 있다.

1-1

안에 뭐가 들어있는지 확인해보면 해당 인스턴스의 모든 정보들이 담겨있다. 이미지나 호스트네임은 물론.. 인스턴스가 물고있는 iam의 access key, secret key, token까지 모두 확인이 가능하다. 🔗

1-2

위에서 알게된 것을 토대로, pod의 인스턴스메타데이터를 통해 키를 얻어 aws 리소스에 명령을 내리는 테스트를 진행해보았다. 우선, 보안을 취약하게 만들기 위해 위에서 본 인스턴스 메타데이터를 하나의 노드에만 변경하였다. 🔗

(nyoung:N/A) [root@kops-ec2 ~]# kops edit ig nodes-ap-northeast-2a

...
spec:
  instanceMetadata:
# IMDSv1으로 변경
    httpTokens: optional 
...

(nyoung:N/A) [root@kops-ec2 ~]# kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster --yes

인스턴스메타데이터를 얻을 파드를 nicolaka/netshoot 이미지를 사용하여 각 노드에 하나씩 배포한 뒤, 각 파드에서 169.254.169.254를 호출해보았다. 어떤 파드가 수정된 노드에 올라가있는지.. 아주 잘 보인다.

# 명령어를 쉽게 내리기 위해 각 파드 이름을 변수처리해줬다.
# 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})


# 반환되지 않는 것으로 보아, v2를 사용 중인 노드에 배포된 것으로 확인
(nyoung:N/A) [root@kops-ec2 ~]# kubectl exec -it $PODNAME2 -- curl 169.254.169.254 ;echo

(nyoung:N/A) [root@kops-ec2 ~]# kubectl exec -it $PODNAME1 -- curl 169.254.169.254 ;echo
1.0
2007-01-19
2007-03-01
2007-08-29
2007-10-10
2007-12-15
2008-02-01

# 아래와 같이 자격증명에 필요한 정보를 얻는다.
(nyoung:N/A) [root@kops-ec2 ~]# k exec -it $PODNAME1 -- curl 169.254.169.254/latest/meta-data/iam/security-credentials/nodes.$KOPS_CLUSTER_NAME | jq
{
  "Code": "Success",
  "LastUpdated": "2023-04-03T14:26:51Z",
  "Type": "AWS-HMAC",
  "AccessKeyId": ACCESSKEYID,
  "SecretAccessKey": SECRETACCESSKEY,
  "Token": TOKEN,
  "Expiration": "2023-04-03T21:02:27Z"
}

방금 전 얻은 키를 사용할 파드를 amazon/aws-cli 이미지를 사용하여 올렸다. 올린 파드에 접속후, 획득한 정보를 환경변수에 넣어서 자격증명에 사용했다. 🔗 자격 증명 후, 위의 권한을 사용하여 aws 내의 리소에 접근해보았다.

현재 상태가 runing 중인 인스턴스를 종료하는 명령어를 실행하였다. 이 때 사용되는 파드로부터 훔친권한은 노드에 붙어있는 iam이기 때문에, 해당 iam이 권한이 있어야 동작한다.

위: aws cli image를 사용한 pod에서 자격증명을 한 상태 아래: aws ec2의 상태를 확인

명령어를 내리기 전 아래창을 통해 인스턴스가 실행 중임을 확인 1-3

윗창에서 명령어를 내리면 상태 값에 나오지 않는 것을 확인 1-4

콘솔에서 보면 종료 중인 것으로 확인된다. 1-5

이에대한 대응방안으로는 인스턴스메타데이터에 쉽게 접근하지 못하도록 IMDSv2으로 버전으로 제한하거나 IRSA을 이용하여 파드가 노드의 iam을 가져가는 것이 아니라 sa에 별개의 iam을 연결하여 사용하는 방안이 있다.

다음 테스트를 진행하기 위해 위에서 진행했던 파드를 모두 삭제했다.

irsa

kubernetes의 sa에 aws의 iam을 연결하는 방법이 aws에서는 IRSA(IAM Role for Service Account)을 통해 구성이 가능하다. 이 작업을 위해서는 외부에서 aws의 iam을 자격증명 할 수 있도록 openid connect 설정이 필요하다. 🔗 eks의 경우에는 해당 작업이 필요없지만, 테스트환경인 kops cluster에서는 cluster spec을 변경해야한다. 🔗 🔗

(nyoung:N/A) [root@kops-ec2 ~]# kops edit cluster

...
spec:
# openid connect provider의 엔드포인트로 public S3 bucket지정 
# allUsers에 대해 read권한이 열려있어야함
  serviceAccountIssuerDiscovery:
    discoveryStore: s3://nyoung-publicly-readable-store
# openid connect provider 활성화
	  enableAWSOIDCProvider: true
  certManager:
    enabled: true
# aws권한으로 sa에서 역할을 할 수 있도록 활성화
  podIdentityWebhook:
    enabled: true

  iam:
# iam의 policy arn과 sa 작성
    ServiceAccountExternalPermissions:
    - name: testsa
      aws:
        policyARNs:
        - arn:aws:iam::643127389001:policy/nyoung-test 
# 만약 sa마다 정책을 다르게 가져간다면 아래에 추가해주면 된다.
#   - name: another-sa
#     ...
...


 (nyoung:N/A) [root@kops-ec2 ~]# kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster --yes

업데이트 후에 콘솔에서 iam > 자격증명 공급자를 보면, openid connect provider가 생성된 것을 볼 수 있고, 버킷에는 인증에 필요한 정보(openid connect 의 구성정보)가 생성된 것을 볼 수 있다. 인증 시에는 버킷내에 생성된 openid connect configure file의 issuer url을 따라 provider 정보를 확인하고 jwks uri로 토큰을 가져와 검증하게된다.

파일 내에 이런식으로 작성되어있다. … “issuer”: “https://nyoung-publicly-readable-store.s3.ap-northeast-2.amazonaws.com”, “jwks_uri”: “https://nyoung-publicly-readable-store.s3.ap-northeast-2.amazonaws.com/openid/v1/jwks”, …

1-6

그럼 이제 iam과 연결된 sa를 사용하여 권한을 제대로 부여받았는지 확인해보았다. sa 생성한 뒤, amazon/aws-cli이미지로 두개의 파드를 생성했다.

1-7

sa를 적용하지 않고 그냥 node의 iam을 가져가는 testpo는 s3 리스트를 가져올 수 없지만, s3 리스트를 볼 수 있는 iam이 연결된 sa를 붙인 testpo-sa는 확인이 가능하다.

sa가 붙은 파드 내부를 보면 환경변수와 볼륨이 마운트 되어있는 것을 볼 수가 있다.

...
Environment:
      AWS_ROLE_ARN:                 arn:aws:iam::643127389001:role/testsa.default.sa.nyoung.xyz
      AWS_WEB_IDENTITY_TOKEN_FILE:  /var/run/secrets/eks.amazonaws.com/serviceaccount/token
    Mounts:
      /var/run/secrets/eks.amazonaws.com/serviceaccount from aws-iam-token (ro)

Volumes:
  aws-iam-token:
    Type:                    Projected (a volume that contains injected data from multiple sources)
    TokenExpirationSeconds:  86400
...

만든적없는 role arn이 붙어있어서 콘솔을 확인해봤는데.. 위에서 podIdentityWebhook을 활성화 해줌으로 sa가 iam의 정책과 연결되면서 iam의 역할을 만든 것을 확인할 수 있다.

1-8

이런식으로 각 파드에 필요한 권한만큼만 할당하는 것이, 만약 메타데이터가 유출되더라도.. 최소한의 피해를 막을 수 있는 방안으로 보인다.