SDK 혹은 CLI 를 이용하면 외부에서 자신이 생성한 AWS 리소스에 접근할 수 있습니다. 콘솔에 로그인하여 리소스에 접근하는 것처럼 이러한 도구를 사용할 때에도 자격 증명을 제공하여 권한이 있는 리소스에 접근할 수 있게 됩니다.
로컬 개발환경에서는 .env, shell 혹은 ~/.aws/credentials 에 Access Key 와 같은 자격 증명을 저장하여 사용합니다. 다만, 이 방식은 개발자의 노트북에 접근만 할 수 있다면 누구나 자격 증명을 확인할 수 있게 되고 도난당할 수 있는 지점이 됩니다.
AWS Vault 는 이러한 취약점을 개선하기 위해 Mac 의 keychain 과 같은 안전한 저장소에 자격 증명을 저장하여 접근하는 도구입니다. 자격 증명이 텍스트로 보관되지 않고 실제 사용 시에는 임시 자격 증명을 발급하여 사용하므로 보다 안전하게 자격증명을 관리할 수 있습니다.
AWS Vault 의 사용 방법은 이 블로그에서 정말 잘 정리해 주셨습니다. 확인해 보시면 좋을 것 같습니다.
또한, AWS Vault 는 백엔드 어플리케이션 개발과 같이 장시간으로 자격 증명이 필요한 경우를 위해 --server 옵션을 제공합니다. 이 옵션은 어플리케이션에서 명시적으로 자격 증명을 제공하지 않아도 리소스에 접근할 수 있도록 자동으로 임시 자격 증명을 발급해 줍니다.
이 기능을 제공하기 위해 EC2 혹은 ECS 인스턴스의 metadata endpoint 를 모방한 서버를 백그라운드에서 실행합니다. 어플리케이션의 SDK 가 자격 증명을 찾을 때마다 이 서버를 통해 새로운 임시 자격 증명을 발급받고, SDK 는 여기서 발급된 자격 증명을 통해 리소스에 접근할 수 있게 됩니다.
--server 옵션은 기본적으로 ECS 를 모방하며, --ec2-server 혹은 --ecs-server 옵션으로 명시적으로 설정할 수도 있습니다. 이번 글에서는 EC2 를 기반으로 AWS Vault 가 어떻게 이러한 기능을 제공하는지 간단히 알아보겠습니다.
먼저 자격 증명과 SDK 에 이를 제공하는 방식에 대해 간단히 알아보고 넘어가겠습니다.
장기 자격 증명
장기 자격 증명은 IAM User 의 Access Key 를 이용하는 방식입니다. Access key 는 Access key id 와 Secret access key 로 이루어지고, 이를 SDK/CLI 에 제공해 주면 User 가 가진 권한으로 리소스에 접근할 수 있게 됩니다.
// nodejs sdk
this.sts = new STS({
region: 'ap-northeast-2',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
} as AwsCredentialIdentity,
});
임시 자격 증명
임시 자격 증명의 발급을 이해하기 위해서는 IAM Role 에 대해 알아야합니다. Role 은 IAM User 와 마찬가지로 정책을 통해 특정한 리소스에 대한 권한을 리소스로, User 와 달리 누군가가 맡을(assume) 수 있습니다.
Role 을 맡기 위해서는 Role 이 신뢰할 수 있는 리소스여야 합니다. 이는 Role 의 Trust relationship 이라는 항목에 명시할 수 있는데, 여기에 User, AWS 서비스 등 특정 리소스를 설정하면 해당 리소스는 Role 의 입장에서 신뢰할 수 있는 상태가 되고 리소스는 Role 을 맡을 수 있는 권한을 가지게 됩니다.
resource "aws_iam_role" "role" {
name = "assume_role_test"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AssumeRoleTest"
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
AWS = "arn:aws:iam:1234:myuser"
},
},
]
})
}
위와 같이 생성하면 sts:AssumeRole 을 Principal 에 명시된 리소스가 사용할 수 있도록 허용을 해줍니다. sts:AssumeRole 은 임시 자격 증명을 얻을 수 있는 Action 입니다. 즉, 위 설정으로 "myuser" 는 "assume_role_test" Role 의 임시 자격 증명을 획득할 수 있게 됩니다.
임시 자격 증명은 IAM User 나 특정 리소스가 IAM Role 을 맡을 때(Assume) AWS STS (Security Token Service) 에서 생성해 주며, 한 번 발급되면 폐기시키기 전까지 유효한 Access Key 와 달리 만료기한이 있습니다.
아래는 Javascript STS SDK를 이용하여 assumeRole 을 수행하는 코드입니다.
this.sts.assumeRole({
RoleArn: "arn:aws:iam:1234:role/assume_role_test",
RoleSessionName: 'local-credentials',
DurationSeconds: 900,
});
이 결과로 Access Key 와 SessionToken 이 포함된 Credentials 을 반환하는데, 이를 이용하면 "assume_role_test" 의 모든 권한을 수행할 수 있게 됩니다.
Credential Providers Chain
AWS 는 수많은 언어로 만들어지는 SDK 에서 동일한 방식으로 자격을 증명할 수 있도록 Standardized credential providers 라는 표준을 만들었습니다. Access Key(장기 자격 증명), Assume role(단기 자격 증명), IMDS 등 자격 증명을 할 수 있는 여러 방식이 있으며 기본적으로 Default credential providers chain 에 정의된 우선순위에 따라 자격 증명을 찾게 됩니다.
예를 들어, EC2 에서 실행되는 백엔드 어플리케이션에 아래와 같은 코드가 있다고 가정하겠습니다.
new S3Client({ region: "REGION" });
SDK 는 Default credential providers chain 에 따라 EC2 인스턴스 내에 유효한 자격 증명이 있는지 확인합니다. 만약 EC2 에 Access Key 에 대한 설정이 없고 IAM Role 이 연결되어 있다면 IMDS credential provider 를 이용하여 임시 자격 증명을 발급하여 사용하는 거죠.
AWS Vault 는 이 동작을 모방하여 로컬 개발환경에서 실행되는 어플리케이션이 마치 EC2 인스턴스에서 실행하는 것처럼 만들어주어 별도의 자격증명을 제공하지 않아도 IMDS credential provider 를 이용할 수 있도록 해줍니다.
Instance Metadata Service (IMDS)
그러면 IMDS 는 무엇일까요?
Instance Metadata 는 hostname, profile, security group 등 실행 중인 인스턴스를 관리하는 데 사용할 수 있는 인스턴스 관련 데이터로, IMDS(Instance Metadata Service) 에 의해 제공됩니다.
IMDS 는 EC2 가 생성되면 자동으로 연결되는데, 일반적으로 AWS, GCP 등의 클라우드 서비스는 IMDS 의 IP 를 Link-local address 중 하나인 169.254.169.254 를 이용합니다.
Link-local address 는 사설 IP 대역과 비슷하게 특수한 목적으로 설계된 IP 대역으로, RFC 6890에 정의되어 있으며 169.254.0.0/16 을 이용합니다. 이는 직접 연결된 하위 네트워크 내에서만 유효한 주소로, 외부에서 접근할 수 없습니다.
실제 EC2 내에서 아래와 같이 요청하면 응답을 받을 수 있습니다.
curl http://169.254.169.254/latest/meta-data
IMDS 가 연 endpoint 를 이용하면 EC2 에 연결된 Role 을 통해 임시 자격 증명을 가져올 수 있습니다. 이를 확인하기 위해 Instance profile 이 있는 Role 을 생성하고 EC2 에 연결해야 합니다. 연결할 때, Role 의 Trust relationship 에는 반드시 EC2 관련 설정이 포함되어야 합니다.
resource "aws_iam_role" "role" {
name = var.role_name
// EC2 에서 AssumeRole 을 할 수 있도록 허용
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "Ec2AssumeRoleTest"
Action = "sts:AssumeRole"
Effect = "Allow"
Principal = {
Service = "ec2.amazonaws.com"
}
},
]
})
}
// Role 에 instance profile 설정
resource "aws_iam_instance_profile" "role_instance_profile" {
name = var.role_name
role = aws_iam_role.role.name
}
연결한 후 아래와 같이 /latest/meta-data/iam/security-credentials 로 요청하면 연결되어 있는 Instance profile 이름을 줍니다.
# 요청
curl http://169.254.169.254/latest/meta-data/iam/security-credentials
# 응답
role-with-instance-profile
이렇게 얻어온 Instance profile 이름을 이용하여 아래와 같이 /latest/meta-data/iam/security-credentials/{instance-profile} 로 요청하면 최종적으로 임시 자격 증명을 가져올 수 있습니다.
# 요청
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-with-instance-profile
# 결과
{
"Code" : "Success",
"LastUpdated" : "2024-04-13T16:59:56Z",
"Type" : "AWS-HMAC",
"AccessKeyId" : "ASIAYLLYEEZYD4*******",
"SecretAccessKey" : "irfl1GZtHXs2daAB****************************",
"Token" : "IQoJb3JpZ2luX2VjEEEaDmFwLW5vcnRoZWFzdC0yIkcwRQIgQGuDCGDjBzXM/FKn**********************************************************************",
"Expiration" : "2024-04-13T23:35:14Z"
}
실제로 ec2 내에서 awscli 를 사용한 모습을 디버그 로그를 찍어보면 위와 같이 IMDS 를 사용하는 것을 확인할 수 있습니다.
aws s3 ls --debug
# debug logs
...
2024-04-14 03:25:26,571 - MainThread - urllib3.connectionpool - DEBUG - Starting new HTTP connection (1): 169.254.169.254:80
2024-04-14 03:25:26,573 - MainThread - urllib3.connectionpool - DEBUG - http://169.254.169.254:80 "PUT /latest/api/token HTTP/1.1" 200 56
2024-04-14 03:25:26,574 - MainThread - urllib3.connectionpool - DEBUG - Resetting dropped connection: 169.254.169.254
2024-04-14 03:25:26,576 - MainThread - urllib3.connectionpool - DEBUG - http://169.254.169.254:80 "GET /latest/meta-data/iam/security-credentials/ HTTP/1.1" 200 26
2024-04-14 03:25:26,576 - MainThread - urllib3.connectionpool - DEBUG - Resetting dropped connection: 169.254.169.254
2024-04-14 03:25:26,578 - MainThread - urllib3.connectionpool - DEBUG - http://169.254.169.254:80 "GET /latest/meta-data/iam/security-credentials/role-with-instance-profile HTTP/1.1" 200 1610
2024-04-14 03:25:26,578 - MainThread - botocore.credentials - DEBUG - Found credentials from IAM Role: role-with-instance-profile
...
만약 role 의 Trust relationship 에 EC2 를 허용하는 설정이 없는 경우 AssumeRoleUnauthorizedAccess 예외가 발생합니다.
# role 은 Trust relationship 설정이 안되어있는 Role 입니다.
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role
# 결과
{
"Code": "AssumeRoleUnauthorizedAccess",
"Message": "~~"
}
또한, Instance profile 을 연결하지 않은 EC2 에서 /latest/meta-data/iam/security-credentials 로 요청하면 아래와 같이 Not Found 예외가 발생합니다.
# 요청
curl http://169.254.169.254/latest/meta-data/iam/security-credentials
# 혹은
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/role-with-instance-profile
# 응답
<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>404 - Not Found</title>
</head>
<body>
<h1>404 - Not Found</h1>
</body>
</html>
--ec2-server 옵션을 사용하면 AWS Vault 는 위 IMDS 엔드포인트를 구현한 서버 하나를 백그라운드에서 실행합니다. 즉, 이 서버는 EC2 의 IMDS 와 마찬가지로 임시 자격 증명을 발급하는 역할을 하는 엔드포인트를 가지고 있습니다. 아래는 Express 로 간단히 구현한 코드이고, 실제 Go 로 구현된 AWS Vault 코드를 보면 비슷한 코드를 확인할 수 있습니다.
router.get('/iam/security-credentials', (req, res) => {
console.log('/iam/security-credentials');
res.send(AWS_ROLE);
});
router.get(`/iam/security-credentials/${AWS_ROLE}`, async (req, res) => {
console.log(`/iam/security-credentials/${AWS_ROLE}`);
try {
const credentials = await credentialProvider.credentials();
res.send({
Code: 'Success',
LastUpdated: new Date(),
Type: 'AWS-HMAC',
AccessKeyId: credentials.Credentials?.AccessKeyId,
SecretAccessKey: credentials.Credentials?.SecretAccessKey,
Token: credentials.Credentials?.SessionToken,
Expiration: credentials.Credentials?.Expiration,
});
} catch (error) {
console.error(error);
res.status(500).send({
Code: 'Failed',
});
}
});
다만 IMDS 는 169.254.169.254 를 이용하므로 localhost (Loopback ip) 에 alias 를 걸어두어 SDK 가 이 서버로 접근할 수 있게 해주어야 합니다.
// ifconfig lo0 alias 169.254.169.254
cprocess.exec('ifconfig lo0 alias 169.254.169.254', (error, stdout, stderr) => {
if (error) {
console.error(error);
}
console.log('ifconfig done...');
app.listen(80, '169.254.169.254', () => {
console.log('169.254.169.254:80 listening...');
});
});
위와 같이 IMDS 스펙을 구현한 서버를 로컬에서 실행하고 SDK 로 요청을 해보면 SDK 가 이 서버의 엔드포인트를 통해 임시 자격 증명을 가져오는 것을 확인할 수 있습니다.
마지막으로 정리하면, AWS Vault 는 위에서 설명한 IMDS 엔드포인트를 구현한 서버 하나를 백그라운드에서 실행하고, SDK 는 IMDS credential provider 를 이용하여 리소스에 접근할 수 있게 됩니다.
이외에도 AWS Vault 는 다른 유요한 기능도 제공해주니 Github 에서 확인해 보시면 좋을 것 같습니다.