회사 서비스에서 S3을 통해 이미지를 전송할 때 버킷의 경로가 그대로 노출되어 있었다. 그나마 현재는 웹툰, 게시판 등 공개적인 것들이라 심각한 문제가 되지는 않으나, 이후 개인정보나 특정 보안에 노출될 위험에 있기에 이를 막아보려 한다.
이번 포스팅은 S3, cloudfront에 대한 세팅은 다루지 않고 서명된 URL을 중심으로 다루려 한다.
문제점
S3 URL을 직접 가져오게 될 경우 aws 버킷의 경로가 그대로 노출되고, 임의로 추적하고자 하면 버킷에 있는 파일들을 스캔할 수 있다.
해결방안
이를 방지하기 위해서 AWS에서는 cloudfront 서비스를 제공한다. 이는 직접 S3 URL을 입력하지 않게끔 대체할 url을 생성해준다. 더불어 직접 S3에 접근하지 않도록 막을 수 있고, 그 뿐 아니라 S3에 직접 접근하지 않고 캐싱의 장점이 있기 때문에 S3으로 직접 접근하는 것보다 속도가 빠르다. 이를 통해 보다 안전하게 파일을 제공할 수 있다.
왜 하는걸까?
결과를 먼저 살펴보자.먼저 아래 스샷은 cloudfront를 적용한 후 원본에 접근을 막고 S3의 원본 URL에 접속했을 때의 모습이다. 버킷이 노출되는 부분은 역시 가렸다. 이 부분이 노출되는 것이 문제인 것이고, cloudfront를 적용하고 원본에 접근을 막으면 이렇게 AccessDenied 로 접근할 수 없게된다.
그리고 아래 스샷은 정상적으로 접근하기 위한 cloudfront url이다.
그런데 이마저도 서명된 URL만 접근할 수 있도록 하면 cloudfront url로 접근했을 때 권한이 있는 사용자만 접근을 허가하기 때문에 이렇게 확인된다.
그리고 서명되어있는 동일한 URL임에도 서명할 때 expireTime을 통해 url의 유효시간을 설정할 수 있다. 테스트를 위해 1분으로 설정했더니 포스팅을 하면서 지금 확인해봤더니 위 스샷과 같이 다시 access denied가 확인되었다.
해결 과정
먼저 아래의 과정은 cloudfront에 key를 등록하여 서명된 url만 접근이 가능하도록 설정하는 과정이다.
1. 위 스샷의 메세지와 같이 key가 없으면 접근할 수 없기 때문에 key-pair-id를 얻는다.
2. public key, private key로 나누고, 그 중 public key를 aws에 등록한다.
3. cloudfront 의 설정을 등록한 key로 제한하도록 한다.
4. 서명된 url을 적용하는 코드를 구현한다.
이제 시작해보자.
1-1. 먼저 key를 만드는 방법은 간단하다. 다음과 같이 입력하고 private_key를 생성한다.
openssl genrsa -out private_key.pem 2048
1-2. private_key를 통해 public_key를 추출한다.
openssl rsa -pubout -in private_key.pem -out public_key.pem
2-1. public key를 확인한다.
cat public_key.pem
2-2. AWS CloudFront - public key 메뉴에서 public key를 생성한다.
2-1에서 확인한 key를 그대로 넣고 생성하면 된다.
그리고 Key Group 메뉴에서 해당 public key를 선택하여 key group을 만들어준다.
3. CloudFront > Distributions > CloudFront id 선택 > behavior > edit 에서 해당 cloudfront에 서명된 url에 대해서만 접근 권한을 제한하도록 Restrict viewer access에 yes를 선택하고, 2-2에서 만든 Key group을 선택한다.
그리고 이제 만들어둔 key를 AWS에서는 알고 있는데 정작 서버에서는 모르고 있으니, 적용해보자.
나는 Node.js를 사용하지만, 다른 언어의 경우 링크를 참조하자.
다음과 같이 서명된 url을 적용하도록 구현했다.
import { getSignedUrl } from 'aws-cloudfront-sign';
export const convertToCloudFrontURL = (originalURL: string) => {
try {
const url = new URL(originalURL);
const parts = url.pathname.split('/');
parts.shift();
parts.unshift(env.awsConfig.cloudfrontURL);
const cloudFrontURL = `https://${parts.join('/')}`;
const privateKey = env.awsConfig.cloudfrontKey;
const publicKeyId = env.awsConfig.cloudfrontKeyId;
const key = privateKey;
const options = {
keypairId: publicKeyId,
privateKeyString: key,
expireTime: Date.now() + 60 * 1000,
};
const signedUrl = getSignedUrl(cloudFrontURL, options);
return signedUrl;
} catch (error) {
console.error(error);
throw error;
}
};
실제로 DB에서 url을 보여줘야 하는 api에 해당 함수를 적용하고 매개변수로 AWS 원본 URL을 넣도록 했다.
그리고 환경변수에 .cloudfront.net으로 끝나는 aws에서 설정한 cloudfront url로 변환해주었다.
그리고 이번에 세팅한 cloudfront에 대한 세팅은 다음과 같다.
1. privateKey에는 cat private_key.pem으로 환경변수에 넣어주었다.
AWS_CLOUDFRONT_KEY=`-----BEGIN PRIVATE KEY-----
~~~
==
-----END PRIVATE KEY-----`
2. publicKeyId는 AWS의 public key메뉴에서 확인할 수 있는 생성한 public key의 id이다.
3. expireTime에는 임의로 1분을 세팅했는데, 얼마동안 url이 유효하도록 할지 설정하면 되겠다.
4. npm install aws-cloudfront-sign 을 설치하여 간단하게 url과 설정한 값을 매개변수로 넣어 서명된 url을 반환한다.
코멘트
AWS에서 제공하는 기능들이 어렵지 않게 잘 구성되어 있는 것 같다. AWS에서 권장하는 기본적인 내용들을 바탕으로 전반적인 네트워크의 구조를 이해해가고 있는데, 이제 어떻게 해야 동일하거나 더욱 보안을 신경쓰면서 서버관련한 비용을 절감할 수 있는지 여러모로 알아봐야겠다.