AWS S3와 IAM — 권한이 연결되는 구조 이해하기
이번엔 AWS 클라우드 보안 쪽으로 S3랑 IAM 개념을 배우고, 직접 버킷 만들어서 권한 설정 바꿔가며 실습까지 해봤다. 개념 정리 + 실습 과정 순서대로 정리해본다.
AWS S3 개요
S3(Simple Storage Service)는 웹 서비스를 통해 어디서나 접근 가능한 오브젝트 스토리지다. 그냥 파일 저장소라고 생각하면 편하다. 다만 EC2처럼 뭘 설치하거나 프로세스를 돌리는 용도가 아니라, 순수하게 데이터를 저장하고 꺼내쓰는 역할만 한다.
버킷 & 구조
- 버킷 이름은 전 세계에서 유일해야 함 (계정 안에서만 유니크한 게 아니라 AWS 전체에서)
- 버킷 생성 시 리전(위치) 지정 필요
- 버킷 안 객체는 키-값 형태로 매핑됨 (파일 경로 = 키, 실제 파일 = 값)
- 객체를 올리면
S3 URI,Object URL두 가지 주소 정보가 부여됨
S3 보안 / 권한 (접근 제어)
S3 권한 설정 방식은 크게 4가지가 있다.
| 방식 | 특징 |
|---|---|
| Bucket Policy | 버킷/객체 단위로 세밀한 조건 기반 정책 설정 (읽기만 허용, 특정 IP만 허용 등) |
| ACL (Access Control List) | 객체 단위 읽기/쓰기 나열 방식, 간단한 권한을 빠르게 줄 때 |
| IAM | 보안 주체(User/Role)에게 직접 권한 할당, 세밀한 리소스 접근 관리 필요할 때 |
| Pre-signed URL | 제한 시간 동안만 접근 가능한 링크 (예: PPT 다운로드 링크를 며칠만 열어주는 식) |
사고는 “공개 버킷 하나”보다 권한 조합에서 시작된다. ACL을 켜면 누구나 그 객체에 접근할 수 있게 된다는 걸 실습으로 직접 확인했다.
IAM 개요
IAM은 회사 사원증 같은 개념이다. 근데 이 사원증에도 두 종류가 있다.
- IAM User: 한번 발급받으면 장기적으로 그 권한을 씀. ID/PW 또는 액세스 키로 접근
- IAM Role: 임시 자격증명을 그때그때 발급받아서 씀. AWS API(STS)를 통해서만 접근 가능
장기 자격증명(User의 액세스 키)은 만료가 없어서 유출되면 위험이 계속 남아있는 반면, Role은 시간 지나면 자동으로 만료되기 때문에 요즘 실무에서는 가능하면 Role 기반으로 가는 추세다.
IAM 정책 JSON 구조
IAM 정책 문서는 다음 4가지 요소로 구성된다.
- 누가 — Role/User 자체가 주체 (RoleName으로 지정, 예:
redteam-lab-role) - 어떻게(Action) —
s3:*,ssm:GetParameter,iam:List*등.*는 “전체 허용” - 무엇을(Resource) —
stix-ticket-assets-*버킷처럼 특정 리소스,*는 “전체 리소스” - 할 수 있다/없다(Effect) —
Allow/Deny
{
"RoleName": "redteam-lab-role",
"PolicyName": "overpermissioned-access",
"PolicyDocument": {
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:*"],
"Resource": [
"arn:aws:s3:::stix-ticket-assets-*",
"arn:aws:s3:::stix-ticket-assets-*/*"
]
},
{
"Effect": "Allow",
"Action": [
"ssm:GetParameter", "ssm:GetParameters",
"ssm:GetParametersByPath", "ssm:DescribeParameters",
"iam:List*", "iam:Get*"
],
"Resource": "*"
}
]
}
}
위 출력은 iam:Get* 권한으로 aws iam get-role-policy 명령어를 실행한 결과다. 정책 이름부터 overpermissioned-access라고 대놓고 적혀있는 게 포인트인데, ssm:GetParameter*(시크릿 조회) + iam:List*/Get*(계정 구조 정찰) 조합이 딱 공격자 입장에서 정찰 단계에 필요한 권한 세트라는 걸 알 수 있었다.
IAM Role 접근 방식 (AssumeRole 흐름)
Role은 다음 4단계를 거쳐서 실제로 쓰이게 된다.
- Assume Role — User/AWS Service/외부 IdP가 “나 이 Role 좀 빌려쓸게”라고 STS에 요청
- check — STS가 두 가지를 검증
- 요청자 쪽의 Permissions (내가 AssumeRole 할 권한이 있는가)
- Role 쪽의 Trust Policy (이 Role이 나 같은 요청자를 받아주는가)
- 둘 다 통과해야 다음 단계로 넘어감
- Return Temporary Credentials — 검증 통과 시 STS가 임시 자격증명 발급 (
AccessKeyId,SecretAccessKey,SessionToken,Expiration세트) - API Call — 이 임시 자격증명으로 실제 S3 같은 서비스에 API 호출
실습: 버킷 생성 → 권한 설정 → EC2 연결까지
1. 콘솔에서 이미지 객체 업로드 + ACL 테스트
먼저 콘솔에서 내방.jpeg라는 이미지 파일을 버킷에 올려봤다.

이 상태에서 권한 설정을 바꿔가며 어떻게 접근 제어가 이루어지는지 확인했다.

이렇게 설정을 바꿔주면 객체 URL만으로 누구나 이미지에 접근할 수 있게 된다. ACL을 활성화하면 모든 사람이 객체를 읽을 수 있다는 걸 직접 눈으로 확인한 부분.
2. EC2 인스턴스 생성 + SSH 접속

EC2 인스턴스를 만들고, 발급받은 .pem 키로 SSH 접속했다.

3. EC2 ↔ S3, 아직 연결 안 된 상태
근데 EC2랑 S3는 기본적으로 아무 관계도 없는 별개의 서비스다. EC2 안에서 aws sts get-caller-identity 같은 명령어를 쳐보면 자격증명을 찾을 수 없다는 에러가 뜬다. 즉 이 EC2가 S3 API를 호출하려면, IAM Role을 만들어서 붙여줘야 한다.
4. IAM Role 생성 및 연결

Role을 만들어서 EC2 인스턴스에 attach했다. 다시 aws sts get-caller-identity를 쳐보면:

이번엔 assumed-role/... 형태로 정상적으로 identity 정보가 나온다. 이어서 aws s3 ls를 쳐보면:

버킷 목록에 정상적으로 접근이 가능한 걸 확인했다.
추가로 다음 두 명령어도 확인해봤다.
# 계정에 있는 모든 S3 버킷 목록 조회
aws s3api list-buckets
# 지정한 버킷의 Public Access Block 설정 상태 조회
aws s3api get-public-access-block --bucket bucket-sec-wave-user04

둘 다 정상 출력됨.
CLI에서 버킷 생성부터 권한 설정까지
이번엔 콘솔이 아니라 AWS CLI로 버킷을 만들고, 정책까지 직접 설정해봤다.
1. 환경변수 설정
버킷 이름을 계정 ID + 타임스탬프로 유니크하게 만들기 위해 환경변수부터 잡아준다.
AWS_REGION=ap-northeast-2
ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text)
BUCKET="s3-security-lab-$ACCOUNT_ID-$(date +%s)"
echo $BUCKET
# s3-security-lab-<ACCOUNT_ID>-1784593381
2. 버킷 생성
aws s3api create-bucket --bucket $BUCKET --region $AWS_REGION \
--create-bucket-configuration LocationConstraint=$AWS_REGION

버킷 생성 완료.
3. 텍스트 파일 생성 및 업로드
# 텍스트 파일 생성
echo "test file for S3 security lab" > name.txt
# 업로드
aws s3 cp name.txt s3://$BUCKET/name.txt
# 업로드 확인
aws s3 ls s3://$BUCKET

객체가 잘 올라간 걸 확인.
4. Public Access Block(PAB) 확인
PAB가 true로 설정돼있으면, 다른 설정이 어떻게 돼있든 무조건 외부 접근이 차단된다. 이 버킷이 그런지 먼저 확인해봤다.
aws s3api get-public-access-block --bucket $BUCKET

전부 true면 외부 접근이 절대 불가능한 상태라는 뜻. 이번 실습에서는 의도적으로 퍼블릭 노출을 재현해볼 거라 PAB를 꺼줬다.

5. Bucket Policy 작성 및 적용
PAB를 끈 다음, 퍼블릭 읽기를 허용하는 버킷 정책을 JSON으로 작성해서 적용했다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::s3-security-lab-<ACCOUNT_ID>-1784593381/*"
}
]
}

정책이 잘 적용됐는지 확인:
aws s3api get-bucket-policy --bucket s3-security-lab-<ACCOUNT_ID>-1784593381

6. 실제 퍼블릭 노출 검증
curl -I https://s3-security-lab-<ACCOUNT_ID>-1784593381.s3.ap-northeast-2.amazonaws.com/name.txt
HTTP/1.1 200 OK 응답이 뜨면서 인증 없이도 접근이 가능해진 걸 확인했다. 아예 내용까지 받아보면:
curl https://s3-security-lab-<ACCOUNT_ID>-1784593381.s3.ap-northeast-2.amazonaws.com/name.txt

txt 파일 내용이 그대로 노출되는 것까지 확인했다. PAB → Bucket Policy → 실제 노출 검증까지 이어지는 이 플로우는 실무에서 S3 misconfiguration을 점검할 때 쓰는 순서와 동일하다. 익숙해지도록 여러 번 반복해서 손에 익혀둘 필요가 있다.
결론
이번 실습에서 가장 크게 와닿았던 개념은 이거다.
EC2 서버에
.pem비밀키로 SSH 접속하는 것 자체가, 이미 그 EC2에 붙어있는 IAM Role의 권한을 얻는다는 것과 같다.
EC2 인스턴스는 생성 시점에 이미 어떤 IAM Role을 쓸지 정해져 있고 (Instance Profile), 그 서버 “안”에 들어온 존재는 별도 인증 절차 없이 EC2 메타데이터 서비스(169.254.169.254)를 통해 그 Role의 임시 자격증명을 자동으로 받아올 수 있다. 그러니까 진짜 방어선은 두 겹으로 나뉜다.
- 1차 방어: 애초에 서버 안에 못 들어오게 막는 것 (SSH 키 관리, 보안그룹, IMDSv2 강제)
- 2차 방어: 들어와도 할 수 있는 걸 최소화하는 것 (IAM Role 최소 권한 원칙)
1차만 믿고 있으면 위험하다. SSH 키 유출이든, 웹앱의 SSRF 취약점이든 어떤 경로로든 서버 내부에 발을 들이는 순간, 그 서버에 붙은 Role 권한이 통째로 넘어갈 수 있기 때문이다. 이번 랩에서 확인한 Role 중 하나가 AdministratorAccess(계정 전체 권한)를 달고 있었는데, 만약 이런 서버가 실제 운영 환경에 있었다면 SSH 하나 뚫리는 순간 계정 전체가 위험해지는 셈이다.
이 개념이 앞으로 진행할 모의해킹 실습의 첫 번째 단계에서 계속 쓰일 핵심 뼈대라, 확실히 정리를 해보았다.
원본 · velog.io