본문 바로가기
Kubernetes/PKOS

[5주차-2] EC2 Instance Metadata와 IRSA

by isn`t 2023. 4. 9.

*본 게시글의 내용은 개인 학습내용과 더불어 가시다님의 노션 페이지와 스터디 자료인 '24단계 실습으로 정복하는 쿠버네티스 도서' 를 기반으로 작성하였습니다.

EC2 meta data

IMDSv2(Instance Meta Data Service)

EC2 인스턴스에서 인스턴스의 메타데이터를 검색할 수 있도록 하는 서비스이다. 일반적으로는 인스턴스에서 실행중인 애플리케이션이 AWS에서 제공하는 인스턴스의 메타 데이터에 접근해야 할 때 사용된다.

EC2 인스턴스 메타데이터는 EC2 인스턴스의 IP, 보안그룹, IAM 롤 등 인스턴스 전반에 대한 정보를 담고 있는데, IMDSv2에 의해 암호화 및 권한에 대한 검증을 거치기 때문에 인증된 EC2 인스턴스만 IMDSv2를 통해 인스턴스 메타데이터에 접근할 수 있다.

스터디 자료와는 순서가 조금 다르지만, 비교를 위해 먼저 kOps로 구성한 쿠버네티스 클러스터에 파드를 생성하고, 파드 내에서 인스턴스 메타데이터에 접근해보았다.

# 파드 생성

$ cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:etshoot-pod
  name: netshoot-pod
spec:licas: 2
  replicas: 2
  selector:bels:
    matchLabels:oot-pod
      app: netshoot-pod
  template:a:
    metadata:
      labels:netshoot-pod
        app: netshoot-pod
    spec:tainers:
      containers:shoot-pod
      - name: netshoot-podtshoot
        image: nicolaka/netshoot
        command: ["tail"]v/null"]
        args: ["-f", "/dev/null"]ds: 0
      terminationGracePeriodSeconds: 0
EOF

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

$ POD1에서 인스턴스 메타데이터 접근 시도
$ kubectl exec -it $PODNAME1 -- curl 169.254.169.254 ;echo

위와 같이 수행하면 아무것도 출력되지 않는다. IMDSv2에 의해 인증된 EC2에서만 메타데이터에 접근할 수 있기 때문이다. 만약 파드가 노출되어 누군가 쉘을 획득한다고 해도, 실제 호스트인 EC2에 대한 정보를 획득할 수 없는 상태이므로 영향 범위를 제한할 수 있다.

다음으로 클러스터의 워커노드 1대에서 IMDSv2에 의한 메타데이터 보안 접근을 해제한 뒤 다시 메타데이터에 접근해보았다.

$ kops edit ig nodes-ap-northeast-2a

# 아래 3줄 제거
  instanceMetadata:
    httpPutResponseHopLimit: 1
    httpTokens: required

$ kops update cluster --yes && echo && sleep 3 && kops rolling-update cluster --yes
$ kubectl exec -it $PODNAME1 -- curl 169.254.169.254 ;echo
1.0
2007-01-19
...
2022-09-24
latest

$ kubectl exec -it $PODNAME1 -- curl 169.254.169.254/latest/meta-data/iam/security-credentials/nodes.$KOPS_CLUSTER_NAME | jq
{
  "Code": "Success",
  "LastUpdated": "2023-04-08T15:01:46Z",
  "Type": "AWS-HMAC",
  "AccessKeyId": "ABC...R",
  "SecretAccessKey": "AS....FASDF",
  "Token": "...6y2W4=",
  "Expiration": "2023-04-08T21:37:35Z"
}

위와 같은 상태에서 컨테이너를 탈취하고 EC2 메타데이터에 접근한다면, EC2에 부여된 IAM Role(Instance Profile)의 임시 토큰을 획득할 수 있으므로 AWS 영역까지 영향범위가 확대된다.

다양한 명령을 테스트 해보기 위해 EC2에 연결된 IAM Role에 기존 권한에 더해 AdministratorAccess 권한을 추가해보았다.
기존에는 EC2, IAM, ECR, S3에 대한 일부 권한만이 부여되어 있었다.

# python3 boto3 사용을 위한 파드 생성
cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: boto3-pod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: boto3
  template:
    metadata:
      labels:
        app: boto3
    spec:
      containers:
      - name: boto3
        image: jpbarto/boto3
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

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

# 파드 쉘 접근
kubectl exec -it $PODNAME1 -- sh
kubectl exec -it $PODNAME2 -- sh

쉘로 접근한 파드 내에서 아래 파이썬 코드를 실행해보면 두 파드가 서로 다른 결과를 보여준다.

import boto3

ec2 = boto3.client('ec2', region_name = 'ap-northeast-2')
response = ec2.describe_instances()
print(response)

POD1

~/dev # python test.py
Traceback (most recent call last):
  File "test.py", line 4, in <module>
    response = ec2.describe_instances()

...

  File "/usr/local/lib/python2.7/site-packages/botocore/auth.py", line 315, in add_auth
    raise NoCredentialsError
botocore.exceptions.NoCredentialsError: Unable to locate credentials

POD2

~/dev # python test.py 
{u'Reservations': [{u'OwnerId': '201554454372',

...

'date': 'Sat, 08 Apr 2023 15:25:12 GMT', 'content-type': 'text/xml;charset=UTF-8'}}}

워커노드 두 대중 한 대만 IMDSv2에 대한 보안 설정을 해제하였으므로, 한 쪽에서는 권한을 획득하지 못해 NoCredentialsError에러를, 한 쪽에서는 EC2 메타데이터를 모두 읽어와 결과를 출력하였다.

일반 리눅스 컨테이너에서 AWS CLI를 설치하고 AWS CLI로 명령을 실행하더라도 같은 결과를 얻을 수 있었다.

cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: linux-pod
spec:
  replicas: 2
  selector:
    matchLabels:
      app: linux
  template:
    metadata:
      labels:
        app: linux
    spec:
      containers:
      - name: linux
        image: ubuntu
        command: ["tail"]
        args: ["-f", "/dev/null"]
      terminationGracePeriodSeconds: 0
EOF

POD1

# aws s3 ls

Unable to locate credentials. You can configure credentials by running "aws configure".

POD2

# aws s3 ls
2023-03-11 15:59:12 ilikebigmac

# # aws ec2 describe-vpcs
{
    "Vpcs": [
        {
            "CidrBlock": "172.31.0.0/16",
            "DhcpOptionsId": "dopt-068252d0ed40aeebd",
            "State": "available",
            "VpcId": "vpc-05c6d7be1dad725a5",
            "OwnerId": "201554454372",

...

앞서 획득한 액세스키와 토큰을 완전히 분리된 로컬 환경에서 환경변수로 주입하고 사용해보았다.

export AWS_ACCESS_KEY_ID="~~~~"
export AWS_SECRET_ACCESS_KEY="~~~~"
export AWS_SESSION_TOKEN="~~~~"


isnt@DESKTOP-41KS4N1:~$ aws s3 ls
2023-03-12 00:59:12 ilikebigmac

isnt@DESKTOP-41KS4N1:~$ aws sts get-caller-identity
{
    "UserId": "AROAS53MU45SAF7H2SAYG:i-0e25c5f531ae7802d",
    "Account": "201554454372",
    "Arn": "arn:aws:sts::201554454372:assumed-role/nodes.ilikebigmac.link/i-0e25c5f531ae7802d"
}

마치 본래의 워커노드로 사용했던 EC2인 것 처럼 권한 획득이 가능했다. 실제로 instance-id로 응답받은 i-0e25c5f531ae7802d 종료시키고 콘솔에서 확인해보았다.

isnt@DESKTOP-41KS4N1:~$ aws ec2 terminate-instances --instance-ids i-0e25c5f531ae7802d
{
    "TerminatingInstances": [
        {
            "CurrentState": {
                "Code": 32,
                "Name": "shutting-down"
            },
            "InstanceId": "i-0e25c5f531ae7802d",
            "PreviousState": {
                "Code": 16,
                "Name": "running"
            }
        }
    ]
}

실제로 해당 인스턴스가 종료됨을 확인하였다. 앞서 Administrator 권한을 롤에 부여했었는데, 만약 실제로 이러한 권한을 가진 인스턴스에서 탈취가 이루어지는 경우 AWS 위에 구현된 모든 환경이 영향 범위에 포함되므로 워커노드로 사용할 경우 외에도 각별히 주의해야 함을 느꼈다.

IRSA(Instance Role-Based Access Control for Service Accounts)

이에 대한 대응 방안으로 IRSA가 있다. EC2의 롤을 공유하거나, 파드 내에 사용자의 액세스키와 시크릿 키를 저장하는 것은 보안상 취약점으로 작용할 수 있다. IRSA는 IAM 롤을 쿠버네티스의 Service Account에 매핑하여 권한을 부여하고 해당 Service Account는 파드에 연결되어 각 파드별로 롤을 부여하고 권한을 제어할 수 있도록 하는 개념이다.

IRSA 사용을 위해서는 OIDC 가 전제된다. EKS는 클러스터 생성시에 함께 적용되지만, 실습환경인 kOps에서는 별도로 설정해 주어야 한다. 설정 방법은 kOps의 공식 문서를 참고하여 진행하였으며, kOps addon으로 클러스터 명세의 spec 필드 하위에 추가해주면 OIDC는 자동 생성 및 적용된다.

#spec: #spec 필드 하위에 추가
  serviceAccountIssuerDiscovery:
    discoveryStore: s3://ilikebigmac
    enableAWSOIDCProvider: true
  podIdentityWebhook
    enabled: true

...
 # iam: #spec.iam 필드 하위에 추가
    useServiceAccountExternalPermissions: true
    serviceAccountExternalPermissions:
      - name: test-sa
        namespace: default
        aws:
          inlinePolicy: |-
            [
              {
                "Effect": "Allow",
                "Action": "s3:ListAllMyBuckets",
                "Resource": "*"
              }
            ]
  certManager:
    enabled: true #기본 설정되어있음

공식 문서에서는 certManager 필드도 함께 예시에 포함되어 있는데, 실습 환경의 클러스터에는 이미 적용되어 있어 오류가 발생하므로 제외하고, iam 필드 이하의 내용들도 iam 필드가 이미 존재하므로 하위의 내용만 추가하였다.

변경한 옵션들은 다음과 같다.

  • serviceAccountIssuerDiscovery : kOps가 OIDC 정보를 S3 버킷에 저장하도록 지정하고 OIDC Provider를 활성화
  • certManager, podIdentityWebhook : Service Account가 지정된 IAM Role을 수임하도록 설정
  • iam : 적용할 IAM Policy와 SA 설정

이후에 kops update cluster --yes 명령으로 클러스터를 업데이트하면, 추가로 생성되는 리소스들을 확인할 수 있다.(IAMOIDCProvider, IAMRole, IAMRolePolicy..)

  • s3 버킷 내 OIDC 데이터 추가 확인

  • 자격 증명 공급자 생성 확인

이제 SA를 생성하여 Role을 연결하고, 해당 SA를 Pod와 매핑해준다.

SA

cat <<EOF | kubectl create -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: irsa-test
  namespace: default
EOF

Deployment

cat <<EOF | kubectl create -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: irsa-test
spec:
  replicas: 2
  selector:
    matchLabels:
      app: irsa
  template:
    metadata:
      labels:
        app: irsa
    spec:
      containers:
      - name: irsa-test
        image: amazon/aws-cli
        command: ["tail"]
        args: ["-f", "/dev/null"]
      serviceAccountName: "test-sa"
EOF
$ kubectl exec -it irsa-test-859dbbfc6c-4p5cf -- bash


bash-4.2# aws s3 ls
2023-03-12 00:59:12 ilikebigmac

bash-4.2# aws ec2 describe-instances
An error occurred (AccessDenied) when calling the ListBuckets operation: Access Denied

최초 부여한 권한대로 s3 버킷에 대한 조회가 가능하며, 그 외의 동작은 불가능함을 확인할 수 있다.

해당 파드에 연결된 SA를 통해 부여받은 권한으로만 동작하기 때문에 행여 누군가 쉘을 획득하더라도 그 영향범위가 한정적이므로 피해를 최소화 힐 수 있으며, 관리자 입장에서도 인스턴스의 권한을 끌어서 쓸 때보다 세밀하게 특정지어 조치할 수 있게 된다.