ecsimsw

개인 파일 CDN URL 보호하는 방법, CloudFront signed url 본문

개인 파일 CDN URL 보호하는 방법, CloudFront signed url

JinHwan Kim 2024. 4. 10. 16:10

개인 파일 CDN URL 보호

사용자 개인 사진, 동영상 파일을 S3에 저장하고 이를 CDN에 캐시하여 자원을 반환한다. 이 구조에서 악성 유저가 CDN URL의 resource key를 brute force로 요청하여 타인의 자원을 확인할 수 있는 문제가 있다. 유효 기간 동안만, 인증된 사용자만 자원을 허락할 수 있도록 개선하고자 한다.

 

이 글에선 CloudFront 에서 요청의 권한을 확인할 수 있는 두 가지 방법을 소개하고, PICUP에서 선택한 방법과 처리 흐름을 설명하려 한다.

 

CloudFront function 을 사용한 토큰 인증

CDN 을 처음 공부할 때 정적 자원과 함께 Lambda function 도 올릴 수 있다고 봤었다. 이걸 이용하면 요청을 선처리 할 수 있지 않을까라는 방향으로 공부했고, CloudFront function 을 사용해서 JWT 토큰을 확인하는 방법을 찾을 수 있었다. 공식 문서 에서도 잘 설명되어 쉽게 따라 할 수 있었다. 

 

https://dev.to/haintkit/cloudfront-with-jwt-authentication-46dh

 

JWT 토큰을 읽어 토큰 유효성과 payload 를 가져올 수 있다. 토큰 페이로드에서 유저 정보를 확인하고, 요청 자원 url에 포함된 사용자 정보와 일치하는지 확인하여 1. 인증된 사용자가, 2. 본인의 자원에 접근하려 하는지를 검증할 수 있었다.

 

Function 을 정의하기만 하면 요청을 쉽고 빠르게 선처리할 수 있지만, JWT 토큰을 쿠키에 저장하는 경우 Domain 정보나 HttpOnly 옵션에 따라 쿠키가 제대로 넘어가는지, Function 에서 그 값을 가져올 수 있는지 같은 관리 포인트가 생긴다. 무엇보다, 이런식의 권한 확인이 가능했던 이유는 요청하는 URL 안에 자원의 사용자 정보가 명확한 상황뿐이다.

 

예를 들어 요청하는 자원의 경로가 "https://d1mx51dsfeok14i.cloudfront.net/users/243/my-image.jpg" 이런 식이고 User 정보 243 이 JWT 페이로드에 들어있다면 이 방식이 가능하다. 그렇지 않고 URL 에 권한을 확인할 수 있는 사용자 정보가 없거나 여러 사용자가 동시에 권한을 갖고 있는 자원은 권한 확인이 불가능할 것이다.

 

쉽고 빠르지만, 한계가 명확하다. 

 

CloudFront signed url 를 사용한 CDN URL 암호화

Signed url 을 사용하면 자원에 접근할 수 있는 권한을 확인할 수 있다. CDN 에 Public key 를 등록해 두고, 인증된 사용자가 요청하는 URL을 Private 키로 암호화해 두는 방식이다. 암호화된 Url 으로 자원을 요청했을 때 CloudFront 는 Public 키로 요청 URL이 올바르게 인증된 자원인지 확인한다. URL 암호화에 권한이 포함된 자원 정보, IP 범위, 권한 유효 시작 시간, 권한 유효 종료 시간를 지정할 수 있다. 

 

 

PICUP 을 예시로, FE에서 사진 파일 정보를 BE 에서 요청하면, BE는 파일 주소를 암호화하여 Signed Url 을 반환한다. FE 에서 이 Signed Url 에 요청하게 되면 CDN 을 통해 원하는 파일을 얻을 수 있게 된다. 암호화 과정에서 요청 IP를 제한하면 해당 URL 은 외부인이 사용할 수 없고, 유효 기간을 제한하면 유효 시간 전후의 요청에선 자원을 정상 응답하지 않는다.

 

Signed url 생성 - AWS  SDK Java V2

CloudFront 에서 public key 등록 방법은 공식 문서 또는 간단한 캡처본 를 참고한다. 

 

Java 에서 Signed URL 을 만들기 위해선 aws-sdk-java-v2 가 필요하다. 이때 공식 문서에서 소개하는 " 'software.amazon.awssdk:aws-sdk-java:2.X.X'"가 아니라 아래처럼 필요한 모듈만 가져오길 추천한다. 앞선 aws-sdk-java:2.x.x 은 모든 AWS 리소스를 다루기 위한 의존성을 가져온다. 이 글에서 다루는 S3 와 Cloudfront signed url 을 위해선 아래 두 의존성이면 충분하다.

dependencies {
    implementation 'software.amazon.awssdk:s3:2.25.27'
    implementation 'software.amazon.awssdk:cloudfront:2.25.27'
}

 

아래 코드로 Signed url 을 만들 수 있다. 사전 준비로 필요한 요소는 CloudFrontKeyPairId 와 CloudFront 도메인 주소, public 키와 함께 생성된 private key의 로컬 경로이다. 예시에서 생성된 Url 은 7일 동안, /my-image.jpg 자원에 한하여 유효하다. 

 

var cloudFrontKeyPairId = 0;
var cloudFrontDomainName = "";
var privateKeyPath = "";
var resourcePath = "/my-image.jpg";

var sign = CannedSignerRequest.builder()
    .privateKey(Path.of(privateKeyPath))
    .resourceUrl(new URL("https", cloudFrontDomainName, resourcePath).toString())
    .keyPairId(cloudFrontKeyPairId)
    .expirationDate(Instant.now().plus(7, ChronoUnit.DAYS))
    .build();
var signedUrl = cloudFrontUtilities.getSignedUrlWithCannedPolicy(sign);
return signedUrl.url();

 

CloudFrontKeyPairId 는 CloudFront -> 퍼블릭 키에서 생성 또는 확인할 수 있다.

 

 

CustomSignerRequest 를 사용하면 접근 가능한 자원에 와일드카드를 사용하거나, IP 범위, 유효 시작 시간을 지정할 수 있다. ( 공식 문서 )

 

var sign = CustomSignerRequest.builder()
    .privateKey(Path.of(PRIVATE_KEY_PATH))
    .ipRange("0.0.0.0")
    .resourceUrl(new URL(CDN_PROTOCOL, CLOUD_FRONT_DOMAIN_NAME, "/users/1/*").toString())
    .keyPairId(publicKeyId)
    .activeDate(Instant.now())
    .expirationDate(Instant.now().plus(EXPIRATION_AFTER_DAYS, ChronoUnit.DAYS))
    .build();
var signedUrl = cloudFrontUtilities.getSignedUrlWithCustomPolicy(sign);
return signedUrl.url();

 

캐싱

매번 자원의 url 을 새로 암호화하면 암호화 시간도 문제겠지만, 매번 바뀌는 Url 에 Content-Cache 정책이 적용되지 않는다. Picup 에서는 url 암호화에 사용자 remote ip 를 사용하여 외부인이 접근하지 못하도록 막는데, {ip:url}으로 암호화된 url 을 캐싱하여 암호화 시간을 제거하고 Content-Cache 를 사용할 수 있도록 하였다.

 

주의해야 할 것은 CDN URL에 만료 기간이 있다는 것이다. 만료 기간이 지난 CDN Url은 캐시를 사용하지 않고 재발급, 캐시를 업데이트 할 수 있는 API 정의가 필요하다.

 

아래는 암호화된 CDN URL 캐시 사용으로 브라우저 Content cache가 적용된 사진 로딩의 모습이다.

 

 

암호화

- 암호화에는 RSA 를 사용한다.

- 암호화에 필요한 시간은 생각보다 크지 않다.

- 애플리케이션에서 처음 Signed url 을 만드는데만 시간이 걸리고 (16ms), 그 이후부터는 1ms, 0ms 가 소요된다. 

signed url duration : 16ms
signed url duration : 5ms
signed url duration : 6ms
signed url duration : 3ms
signed url duration : 2ms
signed url duration : 2ms
signed url duration : 2ms
signed url duration : 1ms
signed url duration : 2ms
signed url duration : 2ms
signed url duration : 1ms
signed url duration : 1ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 0ms
signed url duration : 1ms
signed url duration : 1ms
signed url duration : 1ms
Comments