최근에 S3의 정적 URL을 외부에 직접 노출하지 않고, 허가된 사용자만 접근할 수 있도록 보안을 강화해야 하는 요구사항이 생겼습니다. 단순히 URL을 숨기는 것을 넘어, 특정 사용자만 접근할 수 있는 인증 체계가 필요했죠.
어떤 방법이 최선일까? 선택지 검토
S3 콘텐츠 접근을 제어하는 방법은 보통 세 가지를 먼저 떠올리게 됩니다.
- Pre-signed URL (S3): S3 객체 하나에 대해서만 임시 접근 권한을 주는 URL입니다. 클라이언트가 서버를 거치지 않고 S3에 직접 파일을 업로드할 때처럼, 단일 파일에 대한 제한된 권한이 필요할 때 유용합니다.
- Signed URL (CloudFront): CloudFront를 통해 특정 리소스 하나에 접근할 수 있는 서명된 URL입니다.
- Signed Cookie (CloudFront): CloudFront를 통해 여러 리소스에 대해 쿠키 기반으로 접근을 제어합니다. 저희 서비스는 사용자가 한 페이지에서 이미지, 동영상, 문서 등 다양한 리소스를 봐야 하는 웹사이트였습니다. 여기서 Signed URL을 사용한다면, 페이지를 로드할 때마다 필요한 모든 리소스에 대한 URL을 서버에 일일이 요청해서 받아와야 합니다. 이건 네트워크 요청 낭비가 너무 심하죠.
반면에 Signed Cookie는 로그인 시 한 번만 쿠키를 발급받으면, 그 쿠키가 유효한 동안에는 허용된 모든 리소스에 자유롭게 접근할 수 있습니다. 마치 놀이공원 자유이용권처럼요. 효율성 면에서 저희 상황에는 Signed Cookie가 훨씬 적합하다고 판단했습니다.
Signed Cookie 설정 과정
이제부터 실제로 CloudFront와 S3를 설정했던 과정을 단계별로 설명해 보겠습니다.
1. RSA 키 페어 생성하기
가장 먼저 할 일은 우리 서버와 CloudFront가 서로를 신뢰할 수 있도록 암호 키를 만드는 것이었습니다. CloudFront가 직접 키를 만들어주진 않기 때문에, openssl
을 사용해 서버에서 직접 생성해야 합니다.
# 2048비트 RSA 개인키 생성openssl genrsa -out private_key.pem 2048
# 개인키에서 공개키 추출openssl rsa -in private_key.pem -pubout -out public_key.pem
여기서 private_key.pem
은 우리 백엔드 서버가 쿠키를 서명할 때 사용할 비밀 키이고, public_key.pem
은 CloudFront에 등록해서 서명을 검증할 때 사용할 공개 키입니다.
2. CloudFront에 공개키 등록하기
생성한 공개키를 CloudFront에 알려줘야 합니다.
- AWS 콘솔에서 CloudFront > 키 관리 > 퍼블릭 키로 이동하여,
public_key.pem
파일의 내용을 붙여넣어 퍼블릭 키를 등록합니다. - 다음으로 CloudFront > 키 관리 > 키 그룹에서 방금 등록한 퍼블릭 키를 포함하는 키 그룹을 생성합니다. 이 키 그룹이 나중에 CloudFront 배포 설정에서 접근 제어의 기준이 됩니다.
3. S3 버킷 및 CloudFront 배포 설정
테스트를 위해 새 S3 버킷을 만들고, CloudFront 배포를 구성했습니다.
-
S3 버킷 (
my-test-bucket
) 생성- ‘모든 퍼블릭 액세스 차단’ 에 체크합니다. 이제 이 버킷은 외부에서 직접 접근할 수 없고, 오직 CloudFront를 통해서만 접근해야 합니다.
-
CloudFront 배포 생성
- 원본 설정: 생성한 S3 버킷을 원본으로 지정하고, **OAC(Origin Access Control)**를 설정합니다. 이렇게 하면 S3는 CloudFront로부터의 요청만 허용하게 됩니다.
- 동작 설정: 기본 동작(Default)의 ‘뷰어 액세스 제한(Restrict Viewer Access)’ 설정을 ‘Yes’로 변경하고, 위에서 만든 키 그룹을 선택합니다. 이게 바로 “이 경로의 콘텐츠는 서명된 쿠키가 없으면 볼 수 없다”고 선언하는 핵심 설정입니다.
4. S3 버킷 정책 수정
CloudFront 배포 설정에서 OAC를 설정하면 S3 버킷 정책을 업데이트하라는 안내가 나옵니다. 해당 정책을 복사해서 S3 버킷의 권한 정책에 붙여넣습니다. 내용은 특정 CloudFront 배포에서 오는 s3:GetObject
요청만 허용한다는 것입니다.
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "cloudfront.amazonaws.com" }, "Action": "s3:GetObject", "Resource": "arn:aws:s3:::YOUR_BUCKET_NAME/*", "Condition": { "StringEquals": { "AWS:SourceArn": "arn:aws:cloudfront::YOUR_ACCOUNT_ID:distribution/YOUR_DISTRIBUTION_ID" } } } ]}
5. 백엔드에서 Signed Cookie 생성하기 (Java 예제)
이제 백엔드 서버에서 실제로 쿠키를 생성할 차례입니다. AWS SDK를 사용하면 편리하게 만들 수 있습니다. 아래는 10초 동안만 유효한 테스트용 쿠키를 생성하는 예제 코드입니다.
public class Main { public static void main(String[] args) throws Exception { CloudFrontUtilities cloudFrontUtilities = CloudFrontUtilities.create(); String resourcePath = "/*"; // 모든 파일에 접근 허용 String cloudFrontUrl = new URL("https", DISTRIBUTION_DOMAIN_NAME, resourcePath).toString(); // 만료 시간: 지금부터 10초 후 Instant expireDate = Instant.now().plus(10, ChronoUnit.SECONDS); Path privateKeyPath = Paths.get(PRIVATE_KEY_FILE);
CustomSignerRequest request = CustomSignerRequest.builder() .resourceUrl(cloudFrontUrl) .privateKey(privateKeyPath) .keyPairId(KEY_PAIR_ID) .expirationDate(expireDate) .build();
// 서명된 쿠키 생성 CookiesForCustomPolicy cookies = cloudFrontUtilities.getCookiesForCustomPolicy(request);
// 생성된 쿠키 값 확인 System.out.println(cookies.createHttpGetRequest().headers()); }}
운영 환경에서는 보통 만료 시간을 길게 설정하거나, 세션 쿠키(브라우저 종료 시 만료)로 발급하여 로그인 상태와 생명주기를 맞춥니다.
6. 테스트
IntelliJ의 .http
파일이나 Postman 같은 도구로 생성된 쿠키를 포함하여 요청을 보내보면, 이미지가 잘 보이는 것을 확인할 수 있습니다. 그리고 정확히 10초 후에 새로고침하면 Access denied
오류가 발생하는 것도 볼 수 있죠.
실제 적용 시 마주쳤던 문제들
여기까지 순조로워 보였지만, 실제 개발 서버에 적용하는 과정에서 몇 가지 예상치 못한 문제들을 만났습니다.
- SSL 문제: 서버에서 분명 쿠키를 발급했는데, 브라우저의 개발자 도구를 보니 CloudFront로 보내는 요청 헤더에 쿠키가 포함되지 않았습니다. 원인은 개발 서버가
http
프로토콜을 사용하고 있었기 때문입니다. 보안상의 이유로 브라우저는https-secure
속성이 없는 쿠키를 다른 도메인으로 잘 보내지 않습니다. 개발 서버에 임시로 SSL 인증서를 적용하고https
로 접속하니 문제가 바로 해결되었습니다. - Public 콘텐츠 문제: 생각해보니 모든 리소스를 막아버리면, 로그인 페이지에 사용되는 로고 이미지나 CSS 파일처럼 인증 없이도 접근해야 하는 파일들을 불러올 수 없었습니다. 이 문제는 CloudFront 배포에 새로운 ‘동작(Behavior)‘을 추가하여 해결했습니다.
/static/*
같은 특정 경로에 대해서는 ‘뷰어 액세스 제한’을 ‘No’로 설정하여 누구나 접근할 수 있는 예외 경로를 만들어 주었습니다.
서비스 중단 없는 운영 환경 배포 전략
가장 신경 썼던 부분은 이미 Public S3 URL을 사용하고 있는 운영 환경에 중단 없이 이 보안 기능을 적용하는 것이었습니다. 저희는 아래와 같은 순서로 배포를 진행했습니다.
- CloudFront 설정 선적용: 새 도메인으로 CloudFront 배포를 미리 설정하고, Public 경로와 제한 경로 동작을 모두 추가합니다. 아직 S3가 Public 상태이므로 기존 서비스에는 영향이 없습니다.
- Signed Cookie 발급 코드 배포: 백엔드에 로그인 시 쿠키를 발급하는 로직을 추가하여 배포합니다.
- URL 저장 로직 변경: 신규 파일 업로드 시 S3 URL이 아닌 CloudFront URL이 데이터베이스에 저장되도록 코드를 수정하고 배포합니다.
- 데이터베이스 마이그레이션: 기존의 모든 S3 URL을 새로운 CloudFront URL로 변경하는 작업을 진행합니다.
- S3 Public 액세스 비활성화: 모든 URL이 CloudFront를 바라보게 된 것을 확인한 후, 마지막으로 S3 버킷의 Public 액세스를 완전히 차단합니다.
추가: 사용자 친화적인 에러 페이지 설정
마지막으로, 쿠키가 없거나 만료되었을 때 CloudFront가 보여주는 XML 에러 메시지는 너무 불친절합니다. CloudFront의 ‘오류 페이지’ 설정에서 403 같은 HTTP 오류 코드에 대해 미리 만들어둔 error.html
페이지를 반환하도록 설정하여 사용자 경험을 개선했습니다.
이렇게 S3 콘텐츠 보안을 적용하는 과정을 거치면서, 인프라 설정부터 백엔드 코드, 배포 전략까지 전체적인 흐름을 고려해야 한다는 점을 다시 한번 배울 수 있었습니다.