AWS CloudFront + Lambda@Edge 로 HTTP 요청 시 특정 헤더에 접근 제어 추가하기
곽정섭
2020-01-20
Overview
외부에 HTTP 요청으로 자원 공유 시 보안에 특히 신경을 써야 합니다. AWS CloudFront + Lambda@Edge 서비스를 이용하면 HTTP 요청 시 사용자 지정 함수로 원하는 목적에 따라 처리 가능합니다.
이번 글에서는 오리진 서버에게 요청 시 헤더에 Authorization Key 값을 추가하여 인증을 통해 특정 사용자만 자원에 접근할 수 있도록 권한을 추가해보겠습니다.
작업 진행 순서는 다음과 같습니다.
- Origin Server 생성
- CloudFront 생성
- CloudFront 오류 응답 설정
- CloudFront 이벤트에 의한 Lambda@Edge 트리거
- Route 53 도메인 연결
1. Origin Server 생성 AWS S3 Bucket을 Origin Server로 사용하겠습니다.
1) AWS S3 Bucket 생성
- AWS S3 Bucket에 계층을 만들어 이미지 파일 저장 - CloudFront에서 제공하려는 모든 객체를 AWS S3 Bucket에 배치 - AWS S3 Bucket 해당 자원에 대한 GetObject 정책 추가
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::dev-kwakjs-test/ex-img/*"
}
]
}
2. CloudFront 생성
AWS CloudFront는 콘텐츠를 사용자에게 빠르고 안전하게 전송하는 서비스입니다.
1) Distributions WEB (HTTP통신) 선택
2) Origin Server 설정
- Origin Domain Name : 대상 S3 Bucket - Origin Path : 기본 경로 지정 (ex-img/)
💡이번 글에서는 특정한 최종 사용자에게 권한을 부여하는 용도이기 때문에 권한이 필요한 S3 경로 패턴을 명확하게 지정하여 “ex-img/*” 에 접근할 때만 Lambda@Edge 함수가 실행되도록 하여 비용을 절감합니다.
3) CloudFront Distributions WEB 생성
배포까지 10~15분 정도 소요됩니다.
- 배포 전
- 배포 후
3. CloudFront 오류 응답 설정
Lambda@Edge 함수 실행 후 HTTP 4xx 또는 5xx 오류를 반환할 때 CloudFront에서 최종 사용자에게 반환하려는 사용자 지정 오류 페이지를 만들 수 있습니다.
- 403 오류를 반환할 경우 기본 XML 형식으로 응답
1) 403-response.json 파일 생성
{"meta": {"code": 403, "error_message": "Forbidden"}, "data": {}}
2) S3 Bucket 기본경로 하위 폴더에 업로드
3) Error Pages 생성 Step-1
4) Error Pages 생성 Step-2
- S3 Bucket 에 업로드한 에러 페이지 경로 추가
5) Error Pages 생성 Step-3
- 필요한 오류 상태에 대한 사용자 지정 오류 페이지를 추가합니다.
- 현재 CloudFront 에서 사용자 지정 오류 페이지를 반환할 수 있는 HTTP 상태 코드입니다.
- 400, 403, 404, 405, 414, 416
- 500, 501, 502, 503, 504
6) JSON 형식의 오류 응답 확인
4. CloudFront 이벤트에 의한 Lambda@Edge 트리거
CloudFront로 생성된 4가지 이벤트 시점에 Lambda@Edge 함수 호출하여 요구 사항에 맞게 로직을 추가할 수 있습니다.
1) CloudFront로 생성된 4가지 이벤트
(1) Viewer Request
- 클라이언트에서 이벤트가 도착하고 들어오는 HTTP 요청에 접근할 때 동작 - 이 특정 이벤트는 요청된 개체가 이미 캐시되어 있는지 여부에 관계없이 동작
(2) Origin Request
- 요청된 객체가 엣지 로케이션에 캐시되지 않았을 경우에만 동작
(3) Origin Response
- 오리진이 요청에 대한 응답을 반환한 후에 동작
(4) Viewer Response
- 엣지 로케이션 뷰어에 응답을 반환하기 전에 이벤트가 동작
💡요청된 개체가 캐시 생성 여부와 상관없이 모든 요청에서 실행되어야 해서 Viewer Request 이벤트 시점에 로직을 추가하겠습니다.
2) Lambda@Edge 함수 추가
서버 관리 부담 없이 전 세계적으로 빠르게 배포 가능하며, User-Agent 헤더를 기반으로 특정 객체를 사용자에게 전송 및 특정 헤더에 대한 접근 제어를 추가할 수 있는 유용한 서비스입니다.
특정 헤더에 인증을 추가하여 응답 헤더 값을 재정의하는 Lambda@Edge 함수를 추가하겠습니다.
(1) Python 소스 코드
# -*- coding: UTF-8 -*-
import json
import logging
logging.getLogger().setLevel(logging.INFO)
def _generate_change_request_info():
"""
변경할 요청 정보 생성
401 상태 변경일 경우만 처리
Returns:
dict: request (요청데이터)
"""
headers_request = {
'cache-control': [
{
'key': 'Cache-Control',
'value': 'no-cache',
}
],
'content-type': [
{
'key': 'Content-Type',
'value': 'application/json',
}
]
}
# 변경할 상태 정보
change_status_value = 401
change_status_description = 'Unauthorized'
add_meta_dict = {
'code': change_status_value,
'error_message': change_status_description,
}
body_request_dict = {
'meta': add_meta_dict,
'data': {},
}
body_request_json = json.dumps(body_request_dict)
request_dict = {
'headers': headers_request,
'status': change_status_value,
'statusDescription': change_status_description,
'body': body_request_json,
}
return request_dict
def lambda_handler(event, context):
"""
CloudFront ViewerRequest 이벤트처리
헤더에 Authorization Key 값으로 접근권한 추가
Returns:
dict: request (요청데이터)
"""
logging.info('event::{}'.format(event))
try:
request_dict = event.get('Records')[0].get('cf').get('request')
# 헤더에 Authorization Key 값으로 접근 여부 확인
auth_key = request_dict.get('headers').get('authorization')[0]['value']
if auth_key != 'brandi.token':
logging.warning('유효하지 않은 접근')
# 변경할 요청 정보 생성 (401)
request_dict = _generate_change_request_info()
except Exception as e:
logging.error('exception::{1}'.format(e.args[0], e))
return {
'status': 500,
'statusDescription': 'Internal server error',
}
logging.info('request_dict::{}'.format(request_dict))
return request_dict
💡오류 응답 설정 시 ‘401’ 상태에 대한 사용자 지정 오류 페이지를 반환할 수 없어, 오류 상태 ‘401’ 경우에만 Body 항목에 Json 형태로 재정의하여 응답 값을 반환합니다.
(2) Lambda@Edge 설정
1. 버지니아 리전에서만 Lambda@Edge 배포 가능합니다.
2. 런타임은 Node.js 10.x, Node.js 8.10, Python 3.7 중 하나로 선택해야 합니다.
3. 이 역할에는 AWS CloudWatch Logs에 로그를 업로드 할 수 있는 권한이 추가되어야 합니다.
4. Memory 128MB 이내로 설정 가능합니다.
5. Viewer Request, Viewer Response 이벤트는 실행시간 5초 이내로 설정 가능합니다.
(3) Lambda@Edge 배포
(4) 버전 게시 (CloudFront Event 연결)
- Distribution : CloudFront Distribution 선택
- CloudFront Event : Viewer Request 선택
(5) Lambda@Edge 함수 생성
배포까지 10~15분 정도 소요됩니다.
- 배포 전
- 배포 후
(6) 결과 확인 - Authorization 미인증
- 유효하지 않은 접근일 경우 오류 코드 ‘401’ 응답 데이터 반환됩니다.
- Authorization 인증
- 인증된 정상적인 접근일 경우 요청한 컨텐츠가 보여집니다.
5. Route 53 도메인 연결
Route 53 는 가용성과 확장성이 뛰어난 클라우드 DNS 웹 서비스입니다. 등록된 도메인에 레코드를 추가 후 CloudFront CNAME 으로 매핑하여 사용자에게 노출될 도메인을 연결하겠습니다.
도메인 등록은 생략하겠습니다.
1) CloudFront CNAME 추가
- Route 53로 등록된 도메인을 CloudFront CNAME 으로 연결합니다.
2) CloudFront CNAME 추가 확인
3) Route 53 레코드 추가
- 배포된 CloudFront Domain Name으로 Target을 선택합니다.
4) 도메인 연결 확인
Conclusion
AWS CloudFront + Lambda@Edge 서비스를 이용하여 HTTP 요청 시 권한을 추가하여 특정 사용자만 자원에 접근하는 방법을 알아보았습니다.
그 외에도 Lambda@Edge 함수로 지능적으로 처리할 수 있는 여러 예제가 AWS 개발자 안내서에 설명되어 있으니 참고하면 될 거 같습니다.
참고 자료
https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html