AWS의 Content Delivery Network(CDN)이다. CDN이란 정적 콘텐츠를 보다 효율적으로 제공하기 위한 특별한 시스템이다. 만약 특정 서버에서 모든 콘텐츠를 제공한다면 클라이언트와 해당 서버 사이의 물리적 거리로 인해 제공하는데 수요되는 지연시간이 증가할 것이다. 하지만 CloudFront와 같은 CDN은 전세계 곳곳에 캐시 역할을 하는 엣지 로케이션을 통해 더욱 빠르게 콘텐츠를 제공할 수 있다.
캔바와 같은 서비스를 생각해보자. 캔바에서는 왼쪽의 패널에 있는 다양한 요소를 사용하여 에디터를 꾸밀 수 있다. 하지만 이때 조심할 것이 있다! 캔바는 웹 서비스이기 때문에 개발자 모드를 켜면 요소의 URL을 찾을 수 있고, 이를 공유하여 마음대로 사용하는 악성 사용자가 있을 수 있다.
캔바도 결국 요소를 외부 업체와 계약을 맺고 제공하는 것이기 때문에 이는 곤란할 수 있다. 불특정 다수에게 콘텐츠가 제공되어야하는 이와 같은 상황에서 어떻게 접근 제어를 해야할까? CloudFront는 이런 사용성을 위해 서명된 URL 기능을 제공한다
서명된 URL이란 만료 시간과 같은 추가 정보를 통해 콘텐츠에 대한 접근을 세부적으로 제어할 수 있는 기능이다. 일단 만료 시간을 통해 URL을 서명한 아래 예시를 통해 바로 알아보자.
https://content.test.com/some/static/file/to/serve.jpg?Expires=1664974447&Signature=askEm~nFo4x736ky7plV6a1HMV6Ngugstmr8cKtM0cB3how8RiYt0y9PnYCW7~Mm4H9AamEBQz4gB34ZmQm1M8Gs6WOw1UNj3ArakE6hj8iYIXTLHAf4S3FZnb-CT0bUVnyFD4zgIdgkesm1d0IwrZcG9DeNaJyiBdyuHphtPIH5AF9l5HEN24VrB2Ge1TZHuyqMxrOCZitWb0qi9AqNN6ksugycd1whn50DjO7W0QWqIw5VLFRna0ISPep0osKdOzZ7-j5mhlScvNet1QaivYUiWjqx~DsPYD1saxl077dM1LmT3OiOZxOrs914uUo8tNIRhp895-8PKdkxgwx~Eg__&Key-Pair-Id=K31DYHTOA6ENGO
뭔가 엄청 길어보이지만 사실 https://content.test.com/some/static/file/to/serve.jpg
이부분만 실제 URL이고 이후 긴 쿼리 스트링은 접근 제어를 위해 서명한 내용이다. 각 쿼리 스트링에 대한 설명은 다음과 같다.
파라미터 | 포멧 | 설명 |
---|---|---|
Expires | Integer | 초 단위로 된 유닉스 타임스탬프. URL의 만료 시간을 지정한다. 만료 시간 이후에는 URL에 접근할 수 없다 |
Signature | String | URL과 쿼리 스트링을 비공개 키를 통해 서명한 내용. 덕분에 URL 변조를 확인할 수 있다 |
Key-Pair-Id | String | 비공개 키로 서명된 Signature을 검증할 수 있는 등록된 공개 키 |
이처럼 서명된 URL 기능을 통해 URL은 여전히 사람이 식별 가능하면서 쉽게 접근 제어를 할 수 있다! 한가지 주의할 점은 서명 이후 URL을 조작할 시 변조가 되기 때문에 쿼리 파라미터를 포함시켜야한다면 서명 이전에 추가해야한다.
위에서는 만료시간으로만 접근을 제어했지만, 이보다 더 복잡한 조건으로 제어하고 싶다면 어떻게 할까? 이를 위해 CloudFront는 Canned 정책과 사용자 지정 정책을 제공한다. Canned 정책을 만료 시간을 통해서만 접근 제어를 할 수 있다. 위 예시가 Canned 정책을 사용한 경우였다. 사용자 지정 정책은 이와 다르게 더 많은 옵션을 제공한다. 접근 만료 시간을 지정할 뿐만 아니라 접근 가능한 시작 시간을 지정할 수도 있고, IP 주소를 통해 접근 제어를 할 수도 있다. 이 글은 간단하게 만료 시간만 다룬다.
이 글에서는 서명된 URL을 위주로 설명하지만 서명된 쿠키라는 옵션도 있기 때문에 잠깐 설명하고 지나간다. 서명된 URL은 말 그대로 URL, 즉 특정 리소스에 대해 접근을 제어하는 기능이다. 하지만 리소스가 아닌 사용자를 기준으로 접근을 제어하고 싶을 수도 있다. 이때 사용하면 좋은 것이 서명된 쿠키이다. 서버에서 서명된 쿠키를 전달하면 클라이언트는 해당 쿠키가 유효한 동안 사용하여 리소스에 접근할 수 있다. 리소스마다 각각 서명하는 서명된 URL과 달리 사용자마다 서명하기 때문에 서명 횟수가 줄어든다는 것도 부가적인 장점이다. CloudFront에서는 서명을 위해 비대칭키 암호화 방식을 사용하는데, 이는 비용이 큰 연산이다. 따라서 적게 서명하므로 성능을 높일 수 있다. 더 자세한 내용은 서명된 쿠키에 대한 레퍼런스를 참고하자.
이제 실제로 서명된 URL을 설정하고 사용해보자. 과정이 매우 직관적이기 때문에 대략적인 설명만 작성하겠다.
openssl genrsa -out private_key.pem
openssl rsa -in private_key.pem -out public_key.pem -pubout
implementation ("com.amazonaws:aws-java-sdk-cloudfront:1.12.239")
File privateKeyFile = new File("/.../private_key.pem");
String keyPairId = "3단계에서 생성한 키 그룹 ID";
Date dateLessThan = DateUtils.parseISO8601Date("2022-11-14T22:20:00.000Z");
try {
PrivateKey privateKey = SignerUtils.loadPrivateKey(privateKeyFile);
return CloudFrontUrlSigner.getSignedURLWithCannedPolicy(
resourceUrl,
keyPairId,
privateKey,
dateLessThan
);
} catch (InvalidKeySpecException e) {
log.error("", e);
throw new RuntimeException(e);
} catch (IOException e) {
log.error("", e);
throw new RuntimeException(e);
}
마지막으로 서명된 URL은 무엇을 어떻게 서명하는지 간단하게 알아보자. 앞서 설명했듯 서명된 URL은 다음과 같은 비대칭 키로 서명 및 검증을 한다.
가장 대표적인 비대칭키 암호화 알고리즘으로 동일한 키로 암호화와 복호화를 하는 대칭키 암호화와 대비되는 개념이다. 비대칭키 암호화에서 공개키와 비공개키는 한 쌍을 이룬다. 공개키로 암호화한 내용은 비공개키로만, 비공개키로 암호화한 내용은 공개키로만 복호화할 수 있다. 예를 들어 서버에서 평문을 비공개키로 암호화한다면, 클라이언트는 공개키를 통해 복호화할 수 있다.
이 덕분에 암호화와 복호화가 단방향으로만 될 수 있어서 클라이언트는 공개키만 가지고는 서버로 위장하여 암호화 할 수 없다. 이러한 특성 때문에 인증된 사용자(서버)가 발급한 것이 맞는지 검증해야하는 서명에서 많이 사용된다. 서명된 URL도 이러한 방식으로 RSA를 사용한다.
RSA 키 페어는 아래와 같은 과정을 거쳐 생성된다.
이렇게 구한 n, e, d에서 (n, e)를 공개키에 (n, d)를 비공개키에 사용한다. 예시로 다시 한번 복기해보자
n = 187, e = 7, d = 23
키를 생성했다면 암호화는 매우 쉽다! 어떤 키로 암호화하고 어떤 키로 복호화 하는지에 따라 평문을 e 또는 d로 제곱 후 n으로 모듈러 연산을 한다.
M = 평문, C = 암호문
// 공개키로 암호화
M^e mod n = C
// 비공개키로 암호화
M^d mod n = C
복호화는 반대로 암호문을 암호화한 키와 다른 키로 제곱한후 n으로 모듈러 연산을 한다
M = 평문, C = 암호문
// 비공개키로 복호화
C^d mod n = M
// 공개키로 복호화
C^e mod n = M
마찬가지로 위에서 예시로 생성한 비공개키로 암호화하고, 공개키로 복호화 해보자
// 평문 150을 비공개키로 암호화
150^23 mod 187 = 57
// 암호문 57을 공개키로 복호화
57^7 mod 187 = 150
오, 정리를 잘 해주셨네요.
이름만 알고있던 클라우드 프론트가 뭔지 드디어 깨달았습니다.
서명 알고리즘도 예시까지 잘 설명해주셔서 이해가 잘되네요 감사합니다