최근 들어 사내 업무가 꽤 많고, 잠시 현생이 너무 바빠서 그동안 블로그 글을 잘 쓰지 않았는데, 오늘부터 다시 초심을 되찾고 쓰려 한다. 이번 글은 IRSA에 대한 글이며, IRSA에 대한 발표를 클라우드 동아리에서 연설하기도 했고, 파헤칠 수록 흥미로운 부분이 많아, 이에 관해서 블로그 글을 써보려 한다.
IRSA에 대해 아마 처음 들어본 사람들이 많을 것이다. 결론부터 말하면, Elastic Kubernetes Service
로 구축한 Kubernetes의 Pod에,
ServiceAccount를 IAM Role이 매핑한 상태로 주입함으로써, 해당 애플리케이션이 AWS 서비스에 접근할 수 있도록 하는 방법이다.
여러분이 백엔드 서버를 개발할 때 "사진 업로드 기능" 같은 로직을 쓰려면 S3
를 많이 사용할 것이다. 그렇게 하기 위해 웹 콘솔로 IAM User
을 만들고, AmazonS3FullAccess
기능을 추가하거나 귀찮으면 AdministratorAccess
권한을 부착하고, Access Key
를 발급받을 것이다.
학부생 때 한 프로젝트에서도 대충 IAM User 생성해서 저런식으로 권한 붙였다...
Access Key
를 발급 받았으면 이제 복사 + 붙여넣기해서 이런 코드로 구현했을 것이다.
const AWS = require('aws-sdk');
const fs = require('fs');
AWS.config.update({
accessKeyId: process.env.ACCESS_KEY_ID,
secretAccessKey: process.env.SECRET_ACCESS_KEY,
region: process.env.AWS_REGION
});
const s3 = new AWS.S3({
region: 'ap-northeast-2'
});
s3.upload(params, (err, data) => {
if (err) {
console.error('File upload failed:', err);
} else {
console.log('File uploaded successfully:', data.Location);
}
});
Node.js면 위와 같이, Springboot면 Java 코드로 대충 비슷하게 구현했을 것이다. 당연히 민감한 정보이기 때문에 process.env.ACCESS_KEY
로 치환을 하거나, ${ACCESS_KEY}
이런 식으로 가리고, .env
를 통해 주입하거나, 다른 방법으로 주입했을 것이다. 허나, 그렇다고 해서 공격에 완전히 자유롭지는 않다.
가장 큰 문제는 IAM User의 액세스 키가 영구적이라는 점이다. 탈취당할 확률이 극히 낮지만, 한번 탈취당하면 인지하기 전까지 계속해서 AWS 리소스를 마음대로 쓸 수 있다. 반드시 인위적으로 유출된 액세스 키를 지워야 한다.
AWS는 이러한 이유로 사람이 직접 웹 콘솔 또는 aws cli
로 AWS 서비스를 사용할 때만 IAM User를 사용하도록 권고한다. 그리고 애플리케이션이 AWS 서비스에 접근해야 할 때는 웬만하면 IAM Role을 사용하라고 한다. 즉, IAM User 자체를 사용하는 건 문제될 건 없지만, "Best Practice" 는 서비스에 임시 자격 증명을 부여해서 AWS 리소스에 접근하도록 하는 것이다. 밑에 보이는 사진 처럼, EC2 인스턴스에서 동작하는 애플리케이션은 IAM User대신 IAM Role를 사용하라고 권고한다.
그럼 왜 IAM Role을 사용하라고 권유를 하는 것일까?
첫 번째로, IAM Role에는 "영구적" 이라는 개념이 없다. 즉, IAM User와 달리, 영구적으로 접근할 수 있는 장치가 없고 반드시 "임시로" 사용을 하여 권한을 취득해야 한다.
두 번째로, IAM Role을 사용하기 위해서는 반드시 Trust Relationship
에 사용 주체가 명시되어야 한다. 즉, 내가 아무리 관리자 권한을 가지고 있다고 하더라도 IAM Role의 Trust Relationship
에 명시가 안되어있으면 사용을 할 수가 없다.
IAM Role을 사용한다는 것을, Assume Role
이라고 한다. 만일 aws cli
로 IAM Role을 Assume
하는 명령어를 입력하면 어떻게 될까?
$ aws sts assume-role --role-arn arn:aws:iam::12345678:role/example-role
{
"Credentials": {
"AccessKeyId": "ABCDQWERTYUU",
"SecretAccessKey": "abcdqwertyuiop1234567890",
"SessionToken": "ABCDQWERTYUIOP123456789ASDFGHJKL",
"Expiration": "2099-01-11T11:11:00+00:00"
},
"AssumedRoleUser": {
"AssumedRoleId": "QWERTYUIOP123456789:tempsession",
"Arn": "arn:aws:sts::12345678:assumed-role/example-role/tempsession"
}
}
다음과 같이 임시 자격 증명 (Credentials
)이 발급이 된다. 해당 자격 증명에도 똑같이 액세스 키, 시크릿 키가 발급되지만, 차이점은 이 키들은 "임시적" 이기 때문에 시간 제한이 있다. 또한, SessionToken
이 추가로 발급되기 때문에 검증 수단이 하나 더 생긴다.
SessionToken은 해당 임시 자격 증명을 검증하기 위해 사용된다.
즉, 임시적이기 때문에 탈취당하더라도 시간 제한이 있으며, SessionToken
이 없으면 IAM Role을 사용할 수 없다.
그럼 IAM Role이 얼마나 보안적인지는 알았는데, 여기서 ServiceAccount는 왜 나온 것일까? ServiceAccount는 요약하자면 Kubernetes Pod의 식별자다. 일종의 "여권"이다.
우리가 공항에서 신분을 증명할 때는 여권으로 증명을 한다. Pod도 마찬가지로 Kubernetes의 핵심 서버인 kube-apiserver
에 자신을 증명하기 위해 ServiceAccount를 사용한다.
Pod는 자신을 식별할 때 ServiceAccount를 토큰의 형태로 제출하는데, 해당 토큰을 Decode하면 다음과 같이 출력된다.
{
"header": {
"alg": "RS256",
},
"payload": {
"aud": [
"https://kubernetes.default.svc"
],
"exp": 1700000000,
"iat": 1700100000,
"iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/ABCDQWER1234",
"kubernetes.io": {
"namespace": "example",
"pod": {
"name": "example-pod-1234",
"uid": "abcd1234qwerasdf"
},
"serviceaccount": {
"name": "example-pod",
"uid": "abcd1234qwerasdf"
},
"warnafter": 1700050000
},
"nbf": 1710050000,
"sub": "system:serviceaccount:example:example-sa"
}
}
JWT Token의 형식으로 출력이 되는데, 누가 발행했는지, 그리고 파드의 정보와, 이 토큰의 주체는 누구인지 등의 여러가지 정보가 나온다.
특이한건 발행 주체를 나타내는 iss
필드에 OIDC 관련 URL이 나오는데, kube-apiserver
는 바로 해당 URL에 접속하여 OIDC 공개키를 통해 JWT Token의 Signature 부분을 검증한다.
만일 검증이 된다면, 해당 Token을 제출한 파드를 신뢰를 한다는 것이다. 즉, Pod에 ServiceAccount가 주입이 될 때 얻는 Token을 kube-apiserver
에 제출함으로써 자신을 식별하는 셈이다.
자 그럼 IRSA를 이루는 IAM Role, 그리고 ServiceAccount에 대해 설명을 마쳤다. 퀴즈를 하나 내보겠다.
AWS는 왜 이 두 가지를 매핑해서 Pod에 주입하면 AWS 리소스에 접근할 수 있게끔 허용한 것일까?
답은 이렇다. Pod는 자신을 "식별" 하기 위해 ServiceAccount의 Token을 사용한다. 또한, IAM Role은 자신을 사용하기 위해 접근하는 주체를 "식별" 하고Trust Relationship
에 있으면 허락한다.
즉, IAM Role에 해당 Pod가 사용하는 ServiceAccount의 Token의 정보를 Trust Relationship
에 등록한다면, Pod가 제출한 ServiceAccount의 Token이 AWS에 전달되어 IAM Role를 Assume
할 수 있게 한 것이다.
IRSA를 주입하기 위해서는 ServiceAccount의 Manifest를 다음과 같이 생성하여 Pod에 주입한다.
apiVersion: v1
kind: ServiceAccount
metadata:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::12345678:role/example-role # IAM Role의 arn
name: example-sa
namespace: example
만일 위의 예시처럼 annotations
필드에 Assume
하고자 하는 IAM Role의 arn을 명시한다면, EKS 내부에서 이를 인식하고 다음 내용의 Token을 주입해준다.
{
"header": {
"alg": "RS256",
},
"payload": {
"aud": [
"sts.amazonaws.com"
],
…
"iss": "https://oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234",
…
"sub": "system:serviceaccount:example:example-sa"
}
}
그리고 위의 Token의 내용이 정확히 아래처럼 Trust Relationship에 등록된다면, IAM Role은 해당 Token을 신뢰하게 된다.
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::12345678:oidc-provider/oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:sub": "system:serviceaccount:example:example-sa",
"oidc.eks.ap-northeast-2.amazonaws.com/id/ABCD1234:aud": "sts.amazonaws.com"
}
}
}
]
}
지금까지 IRSA에 대해서 쉽고 간단하게 알아보았다. 어떤가? IAM User보다 복잡하지만, 보안 하나만큼은 확실하게 챙길 수 있을 것 같지 않은가?
더 자세한 내용은 유튜브에 Cloud Club Conference에서 발표한 영상이 곧 업로드 될 예정이니, 확인하면 좋을 것 같다.