AWS

[AWS S3] IAM Role로 만든 Presigned URL이 7일을 못 버티는 이유

디버거러너 2026. 8. 6. 14:28

문제: 코드에 적은 기간과 실제 유효기간이 다르다

프로젝트에서 회원 프로필 이미지를 S3에 저장하고, 다운로드는 Presigned URL로 처리하도록 구현했다.
과제 요구사항은 "Presigned URL의 유효기간을 7일로 설정"하는 것이었고 코드에도 그대로 반영했다.

private static final Duration PRESIGNED_URL_EXPIRATION = Duration.ofDays(7);

public String createPresignedUrl(String key) {
    GetObjectRequest getObjectRequest = GetObjectRequest.builder()
            .bucket(bucket)
            .key(key)
            .build();
    GetObjectPresignRequest presignRequest = GetObjectPresignRequest.builder()
            // 여기서 7일을 요청하지만, 실제로는 아래에서 보듯 이 값이 그대로 지켜지지 않는다
            .signatureDuration(PRESIGNED_URL_EXPIRATION) 
            .getObjectRequest(getObjectRequest)
            .build();
    return s3Presigner.presignGetObject(presignRequest).url().toString();
}

signatureDuration에 7일을 넣었으니 당연히 7일짜리 URL이 나올 거라고 생각했다.

근데 실제로는 그렇지 않았다.

로컬에서는 설정한 7일이 정상적으로 적용됐지만, IAM Role을 쓰는 EC2 환경에서는 같은 코드인데도 다른 결과가 나왔다. 강의를 들으며 이 사실을 알게 됐고, 코드는 동일한데 왜 실행 환경에 따라 결과가 달라지는지 직접 확인해보았다.

직접 확인하는 법

IAM 콘솔에서 Role의 최대 세션 지속 시간을 확인할 수 있지만, 이 값이 현재 EC2가 사용 중인 자격증명의 실제 만료 시간을 의미하지는 않는다 .

AWS IAM 공식 문서에 "Amazon EC2 IAM rolecredentials are exempt from the maximum session duration configured for the role"(EC2 IAM 역할 자격증명은 이 설정에서 아예 제외된다)라고 명시돼 있다.

실제 수명은 EC2가 지금 들고 있는 자격증명을 인스턴스 메타데이터 서비스(IMDS)에서 직접 조회해야 알 수 있다(IMDSv2라 토큰을 먼저 발급받아야 한다):

$ TOKEN=$(curl -s -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
$ curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  http://169.254.169.254/latest/meta-data/iam/security-credentials/mbti-ec2-role
{
  "Code" : "Success",
  "LastUpdated" : "2026-08-05T12:54:43Z",
  "Type" : "AWS-HMAC",
  "AccessKeyId" : "ASIA******",  # ASIA로 시작 = 임시 자격증명
  "SecretAccessKey" : "[REDACTED]",
  "Token" : "[REDACTED]",
  "Expiration" : "2026-08-05T19:05:40Z"    # <- 이 시각에 만료
}

조회 결과 EC2가 사용하는 자격증명은 IAM User의 장기 Access Key가 아니라, Expiration이 약 6시간 뒤로 찍힌 임시 자격증명이었다.

콘솔의 "1시간"과도 다른 숫자였다.

그렇다면 여기서 의문이 생긴다.

"Presigned URL의 만료 시간은 URL에 설정한 7일을 기준으로 할까, 아니면 이 임시 자격증명의 만료 시간에도 영향을 받을까?"

이를 확인하기 위해 AWS 공식 문서를 찾아보았다

AWS 공식 문서

AWS S3 User Guide의 "Using Presigned URLs" 문서에 정확히 이 내용이 나온다.

"Presigned URLs created with temporary security credentials cannot exceed the lifetime of
the credentials themselves... A URL will expire at its configured expiration time or when
the associated credentials expire, whichever happens first. For example, AWS STS AssumeRole
sessions typically default to one hour, while EC2 instance profile credentials rotate
periodically."

(번역: 임시 보안 자격증명으로 만든 Presigned URL은 그 자격증명의 수명을 넘어설 수 없다. URL은 설정된 만료 시간 또는 연결된 자격증명이 만료되는 시점 중 먼저 도래하는 쪽에서 만료된다. 예를 들어 AWS STS AssumeRole 세션은 기본적으로 1시간, EC2 인스턴스 프로파일 자격증명은 주기적으로 갱신된다.)

여기서 헷갈릴 수 있는 점은 URL 자체에는 7일이라는 정보가 정상적으로 들어간다는 것이다.

Presigned URL을 보면 X-Amz-Expires=604800(7일)이 포함되어 있어 애플리케이션은 분명 7일짜리 URL 생성을 요청한 상태다.

하지만 이 URL은 서명에 사용된 임시 자격증명에도 의존한다. 따라서 설정한 만료 시간이 남아 있더라도, 자격증명이 먼저 만료되면 사용할 수 없다.

이걸 식으로 쓰면:

실제 유효기간 = min(코드의 signatureDuration, 서명에 쓴 자격증명의 남은 유효 기간)

앞서 IMDS 조회로 확인했듯 EC2 Instance Profile의 자격증명은 AWS가 자동으로 관리하며 수 시간 단위로 만료된다. 즉 아무리 signatureDuration을 7일로 설정하더라도 실제 유효기간은 그 자격증명의 만료 시각을 넘을 수 없다.

반대로 로컬 개발 환경에서 IAM User의 장기 Access Key(AKIA...)로 서명하는 경우에는 해당 자격증명 자체의 세션 만료 시간이 없기 때문에 signatureDuration으로 설정한 만료 시간이 그대로 적용된다.

"왜 로컬에서는 되는데 EC2에서는 안 되지?"라는 질문의 답이 여기에 있다.

결과: IAM Role은 그대로 유지, 제출 방식만 다르게

Access Key를 코드에 넣지 않는 게 원칙이라, IAM Role 방식 자체를 바꾸지는 않았다. 대신 과제
원문을 다시 보니 이 상황을 위한 조항이 이미 있었다.

"IAM Role로 진행하신 수강생의 경우는 발제에서 요구하는 Presigned URL 대신, 접근 성공
스크린샷을 확보하여 README.md에 첨부 부탁드리겠습니다."

처음에는 "대체 제출 옵션이 있구나" 정도로만 생각했는데, 임시 자격증명의 동작 원리를 알고 나니 왜 이런 예외 조항이 필요한지 이해가 됐다. EC2 Instance Profile이 제공하는 임시 자격증명을 그대로 사용하는 구조에서는, 해당 자격증명의 남은 유효 기간을 넘어서는 Presigned URL을 만들 수 없기 때문이다.

진짜 7일로 설정 하고 싶다면?

이번 문제는 IAM Role 구조 자체를 바꾸지 않는 방향으로 해결했다. 하지만 실제 서비스에서 사용자가 며칠 뒤에도 접근해야 하는 다운로드 링크처럼 긴 유효기간이 필요한 경우에는 S3 Presigned URL만으로 해결하지 않을 수도 있다. 대표적으로:

  • 다운로드 API에서 자체 인증 토큰을 발급하는 방식
  • CloudFront Signed URL을 사용하는 방식

CloudFront Signed URL이 되는 이유도 앞서와 같은 구조다. IAM 세션이 아니라 별도의 키페어로 서명하기 때문에, 애초에 세션 만료라는 개념 자체가 없다.

이 과제의 LV6이 마침 CloudFront다. 지금 마주친 "7일을 못 채우는" 문제의 실무 해법이 이미
다음 단계 로드맵에 있었던 셈이다.

그럼 왜 만료시간이 필요한가?

중요한 건 "다운로드 API냐 CloudFront냐" 같은 방식 선택이 아니었다.

"링크 하나가 며칠씩 살아있어야 하는가"라는 전제 자체가 상황마다 다르다는 게 진짜 이유다.

생각해보면 프로필 이미지는 조금 다르다.
프로필 이미지는 페이지 요청 시 서버가 새로운 URL을 발급하면 된다. 사용자가 URL 자체를 장기간 보관해야 하는 요구사항이 아니기 때문에, 긴 만료 시간을 설정하는 것은 보안 측면에서 오히려 불필요할 수 있다. 만료 시간을 길게 가져가면 URL이 유출됐을 때 접근 가능한 기간만 늘어나기 때문이다.
반대로 이메일로 보낸 다운로드 링크처럼 "사용자가 며칠 뒤에 열어볼 수도 있는" 경우에는 이 전제가 안 통한다. 링크 발급 이후에도 일정 기간 동안 접근 가능해야 한다는 요구사항이 있기 때문이다.

결국 CloudFront든 다운로드 API 토큰이든, 링크가 얼마나 오래 살아있어야 하는지부터 정하고 그에 맞는 방식을 고르면 된다.

참고 자료

GitHub 링크

https://github.com/trex1004/mbti

 

GitHub - trex1004/mbti

Contribute to trex1004/mbti development by creating an account on GitHub.

github.com