← 목록으로AWS

S3 버킷 접근 제어 및 Presigned URL 운영 전략

퍼블릭 액세스 차단 + IAM Role 기반 접근 제어, ECS/EC2/온프레미스 환경별 S3 권한 설계, STS 세션 만료 문제 해결을 위한 장기키 Presigned URL 전략

AWSSecurityDevOpsS3
2025-02-17

개요

운영 환경에서 S3 버킷을 여러 서비스(ECS, EC2, 온프레미스 서버, 사내 PC)에서 접근해야 하는 상황에서, 보안과 운영 편의성을 모두 만족하는 접근 제어 체계를 설계하고 구축했습니다.

핵심 원칙:

  • 퍼블릭 액세스 완전 차단
  • IAM Role/User 기반 최소 권한 부여
  • TLS(HTTPS) 강제
  • Presigned URL을 통한 외부 공유

퍼블릭 액세스 차단 + IAM Role 접근

S3 퍼블릭 액세스 차단을 활성화하면서도 내부 서비스 접근은 정상 동작하도록 설계했습니다.

퍼블릭 액세스 차단 → "Principal": "*" 에만 영향
IAM Role 기반 접근 → "Principal": "arn:aws:iam::..." 은 차단되지 않음
항목동작 여부
ECS Task Role에서 S3 접근허용됨 (HTTPS 사용 시)
Task Role로 서명한 Presigned URL허용됨 (HTTPS 사용 시)
익명(퍼블릭) 접근차단됨

환경별 접근 권한 설계

1. ECS 서비스 → S3

컨테이너 내부 애플리케이션이 S3에 접근하려면 Task Role에 권한을 부여해야 합니다.

ECS Task Definition
    └── Task Role (컨테이너 내 코드가 AWS API 호출 시 사용)
        └── S3 접근 정책 연결

Task Execution Role이 아닌 Task Role에 부여해야 합니다. Execution Role은 이미지 Pull, 로그 전송 등 ECS 인프라 작업용입니다.

2. EC2 서버 → S3

EC2 인스턴스에 연결된 IAM Role에 정책을 추가합니다.

3. 온프레미스 서버 → S3

AWS 외부 서버이므로 EC2 Instance Role을 사용할 수 없습니다. IAM User를 생성하고 Access Key를 발급받아 사용합니다.

4. 사내 PC → S3

테스트/로그 확인/수동 파일 관리 용도로 IAM User Access Key + AWS CLI를 사용합니다.


IAM 정책 설계 (최소 권한)

특정 Prefix 하위만 접근 가능하도록 제한합니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowListSpecificPrefix",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::[bucket-name]",
      "Condition": {
        "StringLike": {
          "s3:prefix": ["outbound/*"]
        }
      }
    },
    {
      "Sid": "AllowReadWriteSpecificPrefix",
      "Effect": "Allow",
      "Action": [
        "s3:GetObject",
        "s3:PutObject",
        "s3:DeleteObject"
      ],
      "Resource": "arn:aws:s3:::[bucket-name]/outbound/*"
    }
  ]
}
  • ListBucket은 버킷 레벨 + prefix 조건으로 제한
  • Object 권한은 특정 prefix 하위만 허용
  • 불필요한 권한(DeleteObject 등)은 필요 시에만 추가

버킷 정책 설계

운영 환경 (Presigned URL 전용)

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowPresignedFromAppRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::[account-id]:role/[task-role-name]"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::[bucket-name]/outbound/*",
      "Condition": {
        "Bool": { "aws:SecureTransport": "true" }
      }
    },
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::[bucket-name]",
        "arn:aws:s3:::[bucket-name]/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    }
  ]
}

개발 환경 (IP 제한 + Role 허용)

개발 환경에서는 사내 IP 대역 허용 + 특정 Role 허용 + 그 외 전부 차단하는 구조입니다.

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowInternalIPsReadList",
      "Effect": "Allow",
      "Principal": "*",
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::[bucket-name]",
        "arn:aws:s3:::[bucket-name]/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "true" },
        "IpAddress": {
          "aws:SourceIp": [
            "[office-ip-1]/32",
            "[office-ip-2]/32"
          ]
        }
      }
    },
    {
      "Sid": "AllowPresignedFromAppRole",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::[account-id]:role/[task-role-name]"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::[bucket-name]/outbound/*",
      "Condition": {
        "Bool": { "aws:SecureTransport": "true" }
      }
    },
    {
      "Sid": "DenyOtherIPsExceptAppRole",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::[bucket-name]",
        "arn:aws:s3:::[bucket-name]/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "true" },
        "NotIpAddress": {
          "aws:SourceIp": ["[office-ip-1]/32", "[office-ip-2]/32"]
        },
        "StringNotLike": {
          "aws:PrincipalArn": [
            "arn:aws:iam::[account-id]:role/[task-role-name]",
            "arn:aws:sts::[account-id]:assumed-role/[task-role-name]/*"
          ]
        }
      }
    },
    {
      "Sid": "DenyInsecureTransport",
      "Effect": "Deny",
      "Principal": "*",
      "Action": "s3:*",
      "Resource": [
        "arn:aws:s3:::[bucket-name]",
        "arn:aws:s3:::[bucket-name]/*"
      ],
      "Condition": {
        "Bool": { "aws:SecureTransport": "false" }
      }
    }
  ]
}

핵심 구조:

  • 사내 IP → 읽기/목록 허용
  • 특정 Role → Presigned URL용 GetObject 허용
  • 그 외 IP + 그 외 Principal → 전부 Deny
  • HTTP(비SSL) → 전부 Deny

Presigned URL: STS 세션 만료 문제와 장기키 전환

문제 발생

ECS Task Role(STS 기반)로 Presigned URL을 생성했을 때, URL 유효기간을 7일로 설정해도 STS 세션 토큰이 먼저 만료되어 ExpiredToken(400) 에러가 발생했습니다.

[기존 구조 - 문제]
ECS Task Role (STS 기반)
    ↓ 임시 자격증명 (세션 만료 있음)
    ↓ Presigned URL 생성
    ↓ STS 세션 만료 시 URL 조기 사망
    → 주말/월요일 걸치는 다운로드 실패

해결: IAM User 장기키로 전환

[현재 구조 - 해결]
IAM User 장기키
    ↓ 세션 만료 없음
    ↓ Presigned URL 생성
    ↓ URL 만료시간 = 실제 유효시간 (최대 7일)

IAM User 생성 (prod/dev 분리)

IAM Console → Users → Create user
  User name: [service]-presign-signer-prod / dev
  Access type: Programmatic access (Access Key)
  태그: Service=[service], Purpose=PresignSigner, Env=prod/dev

Presigned 전용 최소 권한 정책

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowGetPutForPresignPrefixesOnly",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject"],
      "Resource": [
        "arn:aws:s3:::[bucket-name]/[prefix-1]/*",
        "arn:aws:s3:::[bucket-name]/[prefix-2]/*"
      ]
    }
  ]
}

검증 방법

생성된 Presigned URL에서 다음을 확인합니다:

항목장기키 서명 (정상)STS 서명 (문제)
X-Amz-Expires604800 (7일)604800
X-Amz-Security-Token없음있음

X-Amz-Security-Token이 있으면 아직 STS로 서명되고 있는 것입니다.

코드에서 장기키 명시

// Presigned URL 전용 클라이언트 (장기키 명시)
S3Presigner presigner = S3Presigner.builder()
    .credentialsProvider(StaticCredentialsProvider.create(
        AwsBasicCredentials.create(accessKeyId, secretAccessKey)
    ))
    .region(Region.AP_NORTHEAST_2)
    .build();

DefaultCredentialsProvider를 사용하면 ECS 환경에서 Task Role(STS)로 서명될 수 있으므로, Presigned URL 생성 시에는 반드시 StaticCredentialsProvider로 장기키를 명시해야 합니다.


온프레미스 서버 접근 설정

AWS 외부 서버에서 S3에 접근하는 경우:

# AWS CLI 프로파일 설정
aws configure --profile [profile-name]
 
# 업로드 테스트
aws s3 cp file.txt s3://[bucket-name]/outbound/ --profile [profile-name]
 
# 다운로드 테스트
aws s3 cp s3://[bucket-name]/outbound/file.txt ./downloaded.txt --profile [profile-name]
 
# Presigned URL 생성
aws s3 presign s3://[bucket-name]/outbound/file.txt \
  --expires-in 3600 --profile [profile-name]

Python에서 프로파일 지정:

session = boto3.Session(profile_name='[profile-name]')
s3 = session.client('s3')

보안 운영 통제

장기키는 편리하지만 유출 시 리스크가 있으므로 다음 통제를 적용합니다:

  • prod/dev 키 완전 분리
  • Prefix 단위 최소 권한 (버킷 전체 접근 불가)
  • TLS 강제 (aws:SecureTransport)
  • Access Key 정기 로테이션
  • CloudTrail로 키 사용 이력 추적
  • 장기키를 컨테이너 환경변수로 전역 설정하지 않음 (다른 AWS 호출까지 장기키로 갈 수 있음)

정리

  • 퍼블릭 액세스 차단 + IAM Role 기반 접근으로 보안 확보
  • ECS Task Role / EC2 Instance Role / IAM User(온프레미스)별 최소 권한 설계
  • 버킷 정책: IP 제한 + Role 허용 + TLS 강제 + 나머지 Deny
  • STS 세션 만료로 인한 Presigned URL 조기 사망 문제를 장기키 전환으로 해결
  • prod/dev 분리, Prefix 제한, StaticCredentialsProvider 명시로 보안 통제