728x90

근래에 제공되는 대부분의 서비스는 SSL 혹은 TLS 를 이용하여 보안된 채널로 통신이 수행되도록 하는 것이 일반적입니다. 하물며 개인이 만드는 토이 프로젝트부터 대학교에서 과제로 제출하는 웹 사이트도 HTTPS 를 사용하지 않는 경우를 찾아보기 힘듭니다. 네, 물론 이 와중에서 Non-secure 로 버티는 곳들도 <당연히> 있긴 합니다. 

예전에는 HTTPS 를 이용하기 위해 인증서를 구매해야 했고, 보통 년단위의 갱신을 하다보니 비용 부담이 있었습니다. 하지만, 3개월마다 주기적으로 인증서를 업데이트 받는 조건으로 도메인 단위 인증서 (Domain Validation) 를 무료로 발급해주는 렛츠인크립트 Let's Encrypt 의 대중화 이후, 인증서 구매 비용도 단지 핑계가 된지 오래입니다. 


하지만, 이런 인증서 구매와 별개로 서버측에 SSL 설정을 구성하고 적용하는 것은 생각보다 까다롭게 생각하는 경우가 많습니다. 보통 한번 설정해 두면 수정하지 않아서 보안 사고의 원인이 되기도 하고, 설정을 잘못하는 바람에 특정한 단말기들이 서비스에 접근하지 못하는 장애를 겪기도 합니다. 원체 그 파급력이 크다보니 <그리 어려운 일이 아님에도 불구하고> 인증서 교체에 대해서 전문가를 찾는 경우가 종종 생깁니다.

이러한 엔지니어들의 고충을 잘 알고 있는 곳이 모질라 재단이기 때문일까요? 모질라에서 제공하는 SSL 설정 생성기는 이 모든 두려움으로부터...는 아니겠지만, 기본적인 설정 실수로 장애가 발생하는 것을 막아줄 수 있는 여러분의 친구, 좋은 도구가 될 수 있을 것 같습니다. 

다양하다! 

일단 이 도구가 커버해주는 서버측 소프트웨어의 범주가 무척 넓습니다. 널리 사용되는 아파치나 nginx 에서부터 AWS 의 ALB, ELB 도 포함이 되어 있습니다. HAProxy 와 Go 기반으로 서버측 엔드포인트를 만들때 쓸 수 있는 옵션들도 제공하고 있어 왠만한 서버측 도구의 SSL 설정에 활용도가 높아 보입니다. 

가운데 위치한 Mozilla Configuration 은 SSL/TLS 의 어떤 버전을 지원할 것인가에 따라 선택 가능한 항목입니다. 이 항목의 변경은 설정 생성시에 추가되는 Cipher Suite 의 종류에 영향을 주는 부분입니다. SSL/TLS 버전이 상위로 갈 수록 취약한 암호화 알고리즘을 사용할 수 없게 됩니다. 하지만, 오래된 사용자들은 SSL/TLS 의 상위 버전 자체를 사용할 수 없는 경우도 있기 때문에, 사용자 분포에 따라 취약한 알고리즘을 써야 하는 경우도 있을 겁니다.

가장 오른쪽에는 각 서버 어플리케이션의 버전에 따라 달라진 부분을 반영하기 위해 버전 정보를 기입할 수 있는 입력창이 위치해 있습니다. HSTS 와 OCSP Stapling 을 사용할 것인지에 따라 옵션을 선택할 수 있게 되어 있는 것도 눈에 띕니다. 

# generated 2021-02-08, Mozilla Guideline v5.6, nginx 1.17.7, OpenSSL 1.1.1d, modern configuration
# https://ssl-config.mozilla.org/#server=nginx&version=1.17.7&config=modern&openssl=1.1.1d&guideline=5.6
server {
    listen 80 default_server;
    listen [::]:80 default_server;

    return 301 https://$host$request_uri;
}

server {
    listen 443 ssl http2;
    listen [::]:443 ssl http2;

    ssl_certificate /path/to/signed_cert_plus_intermediates;
    ssl_certificate_key /path/to/private_key;
    ssl_session_timeout 1d;
    ssl_session_cache shared:MozSSL:10m;  # about 40000 sessions
    ssl_session_tickets off;

    # modern configuration
    ssl_protocols TLSv1.3;
    ssl_prefer_server_ciphers off;

    # HSTS (ngx_http_headers_module is required) (63072000 seconds)
    add_header Strict-Transport-Security "max-age=63072000" always;

    # OCSP stapling
    ssl_stapling on;
    ssl_stapling_verify on;

    # verify chain of trust of OCSP response using Root CA and Intermediate certs
    ssl_trusted_certificate /path/to/root_CA_cert_plus_intermediates;

    # replace with the IP address of your resolver
    resolver 127.0.0.1;
}

 

TLS 1.3 을 기준으로 NGINX 를 위한 설정을 생성해 보았습니다. HTTP 로 인입된 경우 HTTPS 로 스킴을 바꾸도록 301 리디렉션 하는 서버 컨텍스트가 앞에 위치해 있고, 443 포트로 HTTP/2.0 을 지원하는 엔드포인트를 열고 있습니다. 센스있게 IPv6 를 위한 포트 바인딩도 함께 추가되어 있네요!

그 외의 지시자들은 일름에서 유추할 수 있는 것처럼 프로토콜 버전의 지정, 인증서 파일의 위치, 세션 티켓 사용을 위한 옵션들이 지정되어 있습니다. 서버측의 prefer_server_ciphers 가 off 로 생성된 부분은 그 의도가 있을텐데 (아마도 NGINX 에서 TLS 1.3 사용시 기본 Cipher Suite 의 영향?) 자세한 건 문서를 좀 살펴봐야 할 것 같네요.


우리가 코드를 수정하거나 서버의 설정을 변경할 때 늘 하는 말이 있습니다. <지금 니가 무슨짓을 하는지 모른다면, 아무것도 하지 말아라> 라는 말이 바로 그것입니다. 각 설정이나 코드가 존재하는 것은 다 이유가 있어서 입니다. (예, 정말 가끔 이유 없이 존재하는게 없진 않습니다만...) 자동 생성된 설정을 참고하고 사용하는 것은 좋지만, 추가된 설정 항목들이 "왜" 들어갔는지에 대해서는 한번쯤 문서를 찾아보고 고개를 끄덕인 다음 Ctrl-C, V 하는 아름다운 자세를 견지하시기 바랍니다!

 

 

728x90
728x90

CORS (Cross Origin Resource Sharing) 은 서로 다른 도메인 간에 리소스를 활용할 필요가 있을때, 어떤 규칙으로 누구에게 허용할 것인지를 정의하는 HTTP 표준의 일부입니다. 모던 브라우저들은 CORS 헤더에 대한 지원을 충실히 하고 있습니다만, 서버측에서는 어떤식의 구현이 필요한지, 그리고 커스텀 클라이언트를 사용하는 경우에는 어떤 요청을 보내는 것이 적절한지에 대해 오해도 많고 시행착오도 많습니다.

CORS 에 대한 기본적인 정의를 먼저 살펴보고 CORS 구현을 위한 RFC 규격을 살펴보면서 어떤 요청 헤더와 응답이 필요한지 살펴봄으로써, 혹시나 CORS 에 대하여 시행착오를 겪는 분들이 적어지기를 바라며 포스팅을 시작해 보도록 하겠습니다. 


CORS 란 무엇일까?

웹이 태동하고 급성장하던 시기에는 서로 다른 리소스를 가져다 쓰는 것에 대하여 큰 제약이 없었습니다. 하지만 리소스를 사용하는 것은 분명 서버측의 비용이 발생하는 일이고, 보안 관점에서도 접근하는 사용자를 적절히 통제할 필요가 생겼습니다. 이런 요구사항을 수용하기 위하여 정의된 것이 CORS (Cross Origin Resource Sharing, 서로 다른 원본 서버간의 리소스 공유에 관한 규칙) 입니다. 

2009년경에 출시된 파이어폭스 Firefox 3.5 와 사파리 Safari 4.0 에서부터 강화된 동일 오리진 정책 SOP (Same Origin Policy) 이 적용되기 시작했고 현재 시장에서 사용되는 대부분의 브라우저는 이 정책을 준수하고 있습니다. 이 즈음부터 XHR (XML Http Request) 을 이용해 서로 다른 원본 서버에서 리소스를 <비동기> 로 가져다 쓰는 것에 대한 혼란(?)이 시작되었다고 봐도 무방합니다. 

근래의 브라우저들에서는 Fetch 도 제공하기 시작했고 Fetch 역시 XHR 과 마찬가지로 CORS 의 영향권 아래에 있습니다. 따라서 CORS 에 대한 정확한 이해를 바탕으로 구현을 해야 커머셜 브라우저는 물론이고 커스텀 클라이언트 개발에 대응할 때도 불필요한 시행착오를 줄일 수 있습니다. 

브라우저보다 조금 늦게 (늘 그렇듯) 2010년에 Drafting 된 CORS

 

다른 Origin 에서 리소스를 가져오는 두가지 방법

JSONP 는 논외로 두고 다른 Origin 에서 리소스를 가져오는 방법은 앞서 이야기 한 것처럼 XHR 을 이용한 방식와 Fetch 를 이용한 방식으로 나뉘어 집니다. 많은 자바스크립트 프레임웍 (jquery, axios...) 도 이들을 래핑하고 있기 때문에 동일하게 CORS 규칙의 영향을 받게 됩니다. 

비동기로 리소스를 가져오는 방식은 다른 관점에서 보면 단순 요청 Simple Request 와 예비 요청 Pre-flight Request 로 다시 나뉘어 집니다. 이 두가지의 차이점은 간단합니다. 메소드와 헤더에 관한 규격들이 더 있지만, 일단 크게 아래의 구분이 있다는 점을 인식하는 것이 중요합니다. 

구분 내용
단순 요청 Simple Request - GET, POST, HEAD 메소드로 하나의 요청에 필요한 CORS 요청 헤더를 포함하여 전송
예비 요청 Pre-flight Request - OPTIONS 메소드를 이용하여 본 요청에 대한 스펙을 CORS 요청 헤더를 포함하여 전송
- 성공 응답을 받은 경우 본 요청을 전송

 

CORS 의 기본, Origin 요청 헤더

앞서 설명한 두 요청의 상세한 차이점은 다음 포스팅에서 소개할까 합니다. 절단 신공이라기 보다는... 두 요청의 공통 요소 중 하나인 Origin 헤더의 규격에 대해서 살펴보고 가는 것이 더 중요하기 때문입니다. 다른 Origin 으로 리소스를 요청하는 경우, 원래의 요청이 어떤 도메인에서 시작된 것인지를 다른 Origin 으로 알려주어야 할 필요가 있습니다. 이 때 사용하는 것이 Origin 요청 헤더입니다. 

가령 https://a-server.nopd-genius.com 이라는 도메인에서 XHR 혹은 자바스크립트 프레임워크를 이용하여 https://b-server.nopd-not.com 이라는 도메인으로 요청을 보내는 경우를 생각해 보겠습니다. 자바스크립트 코드는 첫번째 서버(a-server)에서 사용자 브라우저로 전달해 주었기 때문에, 이 코드가 만든 두번째 서버(b-server)로의 요청은 `Origin: https://a-server.nopd-genius.com` 이라는 헤더를 포함해야 합니다. 

이 요청을 받은 두번째 서버(b-server)는 Origin 값에 해당하는 정책을 확인하여 이를 CORS 응답 헤더로 내려주게 됩니다. 이 과정에서 Origin 헤더 값이 사전에 약속된 원본 도메인이 아니라면 에러 응답을 하거나 CORS 헤더 없이 응답하게 되어 결과적으로 브라우저에서는 응답을 사용하지 못하는 상황이 되게 됩니다. 

출처 : 모질라 CORS 문서 

 

그런데 말입니다, 모질라의 CORS 문서에서 발췌한 위의 내용은 한가지가 잘못되어 있습니다. RFC 의 규격 문서에 따르면 Origin 헤더의 값은 반드시 스킴 Scheme (http 혹은 https) 을 포함해야만 합니다. 위의 그림처럼 `Origin: foo.example` 로 던지면 규격에 대한 위반이 되게 됩니다. 왜냐하면 http://foo.example 과 https://foo.example 은 서로 다른 Origin 으로 활용될 수 있기 때문입니다. WHATWG 의 규격 문서를 보면 이 헤더의 규격은 아래와 같습니다. 

특정한 포트를 지정해주는 ":port" 는 필요 없는 경우 생략해 줄 수 있지만, 스킴은 생략 가능한 항목으로 명기되어 있지 않습니다. 따라서 Origin 요청 헤더는 위의 규격을 준수하여 전송할 필요가 있습니다. 모질라의 문서 그림도 업데이트가 되어야 하겠죠? ^^ 이렇게 Origin 헤더가 중요합니다. 

다행히도 근래의 모던 브라우저들은 CORS 요청을 하는 경우 항상 스킴을 포함한 Origin 요청 헤더를 보내주고 있습니다. 따라서 Origin 헤더 값은 상용 브라우저가 아닌 클라이언트를 사용하는 경우에 유의해 주시면 큰 문제는 발생하지 않을 것으로 생각됩니다. 다음 포스팅에서는 Simple Request 와 Pre-flight Request 의 요건에 대하여 보다 자세히 살펴보도록 하겠습니다. 


참고자료

 

교차 출처 리소스 공유 (CORS)

교차 출처 리소스 공유(Cross-Origin Resource Sharing, CORS)는 추가 HTTP 헤더를 사용하여, 한 출처에서 실행 중인 웹 애플리케이션이 다른 출처의 선택한 자원에 접근할 수 있는 권한을 부여하도록 브라

developer.mozilla.org

 

 

Cross-Origin Resource Sharing

Must be deployable to IIS and Apache without requiring actions by the server administrator in a configuration where the user can upload static files, run serverside scripts (such as PHP, ASP, and CGI), control headers, and control authorization, but only d

www.w3.org

 

 

cross-site xmlhttprequest with CORS – Mozilla Hacks - the Web developer blog

Editor's Note: This article sure is a popular one! The Fetch API is now available in browsers and makes cross-origin requests easier than ever. Check out this Hacks post or ...

hacks.mozilla.org

 

 

Fetch Standard

 

fetch.spec.whatwg.org

 

728x90

+ Recent posts