무료 SSL/TLS 인증서로 인기를 끌고 있는 Let’s Encrypt 인증서를 기반으로 최근 보안 트렌드를 반영해 인증서를 세팅하는 방업에 대해서 알아봅니다.
우선 SSL 보안 수준은 ssllabs.com에서 체크해 볼 수 있습니다.
1. 이제 A에 불과한 과거 SSL 인증서 세팅법
최근 우연한 기회에 sslabs.com에서 SSL Server Test를 하게 되었습니다. 그리고 충격을 먹었죠.
2~3년전, Let’s Encrypt를 이용한 SSL/TLS 세팅은 SSL Server Test에서 A+를 받기 에 충분했던 것 같은데요.
그런데 같은 세팅으로 이번에 테스트해보니 A정도만 나오고 군데 군데 보안이 취약하다는 메세지도 발견할 수 있습니다. 시간이 많이 흐른 것일까요?
사실 알고보면 이 정도 점수라면 예전에는 A+이 나왔습니다. 평가하는 4개 항목의 점수가 평균 95점(?)이 넘으면 A+이라고 정의했던 것입니다. 그리고 그 당시에는 점수를 표시해주었죠.
그런데 최근에 A+의 기준을 아주 엄격하게 4개 항목을 모두 100점을 맞아야 하는 것으로 강화했습니다. 그러니 예전엔 A+이었던 것이 이제는 A밖에 나오지 않는 것이죠.
그래도 우선 어떻게 개선해야하는지 살펴보죠. 우선 예전에 적용했던 세팅들을 살펴봅니다.
add_header X-Frame-Options SAMEORIGIN;
add_header X-Content-Type-Options nosniff;
add_header X-XSS-Protection "1; mode=block";
ssl_certificate /etc/letsencrypt/live/MyDomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/MyDomain.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/MyDomain.com/chain.pem;
ssl_dhparam /etc/nginx/ssl/dhparams.pem;
add_header Strict-Transport-Security "max-age=31536000";
ssl_session_timeout 10m;
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA';
Code language: PHP (php)
2. 어떻게 SSL 인증서 보안을 강화할 것인가?
인증서 보안을 강화하기 위해서 아래 글들을 참조했습니다. 이를 통해서 전반적으로 개선 방향을 살펴보았습니다.
Getting a Perfect SSL Labs Score
우리가 목표로하는 A+에 도전하기 위해서는 점수가 어떻게 구성되는지를 알아야 합니다. 위에서 소개한 ssllabs.com에서는 아래와 같은 네가지 항목으로 나누어 평가하고 있습니다.
- Certificate
- Protocol Support
- Key Exchange
- Cipher Strength
2.1. Certificate – Let’s Encrypt 적용만으로 충분
SSL 인증해주는 툴들은 굉장히 많이 있습니다. 그러나 우리는 무료로 적용 가능한 Let’s Encrypt를 사용하기로 했죠.
이 Let’s Encrypt는 시작한지 얼마되지 않았지만 기존 유료 SSL 인증서에 못지않은 뛰어난 성능을 가지고 있습니다.
Let’s Encrypt은 이 SSL인증 시장에서 점유율이 겨우 0.1%에 불과하지만 무려라는 장점을 기반으로 점점 그 영향력을 넓힐 것으로 보입니다.
다만 이 Let’s Encrypt는 3개월에 한번씩 인증을 갱신해야하는 단점이 있지만 자동 갱신 기능이 있으므로 어느정도 커버됩니다.
그리고 중요한 것은 Let’s Encrypt 적용 자체만으로도 Certificate부분은 만점을 받을 수 있습니다.
ssl_certificate /etc/letsencrypt/live/MyDomain.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/MyDomain.com/privkey.pem;
ssl_trusted_certificate /etc/letsencrypt/live/MyDomain.com/chain.pem;
Code language: PHP (php)
2.2. Protocol Support – 지원 protocol을 제한한다.
다음으로 SSL 인증서의 보안을 강화하기 위해서는 지원 protocol이 적절한지를 보는 것입니다.
대부분 이미 SSLv2나 SSLv3를 지원하지 않으며 오직 TLS만 지원하는 경우가 일반적입니다.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
Code language: PHP (php)
그러나 보안을 더 고려한다면 TLSv1.0과 TLSv1.1 지원을 중단하는 것이 좋다고 합니다. 이미 TLSv1.0은 시장에서 지원이 중단된 상태이기도 합니다.
그냥 점수로만 따지만 TLSv1 지원 중단시 95점(TLSv1 TLSv1.1 TLSv1.2 지원)에서 97점으로 높아지고 여기에 TLSv1.1까지 지원 중단한다면 100점이라는 평가를 합니다.
결국 T100점으로 완벽을 기하기 위해서는 LSv1.2와 TLSv1.3만 지원하는 것으로 정리합니다.
ssl_protocols TLSv1.3 TLSv1.2; # Requires nginx >= 1.13.0 else use TLSv1.2
Code language: PHP (php)
2.3. Key Exchange – 보안성이 높은 보안 키 생성 및 적용
일반적으로 SSL/TLS만 적용해도 되지만 보안을 더욱 강화하기 위해서 보안키 수준을 높입니다.
근래에 등장한 웹서버로 인기를 끌고 있는 nginx는 openssl에서 제공하는 기본 DHE (Ephemeral Diffie-Hellman) 파라메터를 사용합니다.
이는 1024비트에 기반하기 때문에 낮은 점수를 받을 수 밖에 없습니다. 그래서 nginx 자체가 아닌 사용자가 더 높은 보안을 담보하는 키를 생성해 줍니다.
보통 이럴경우 예전에는 2048비트를 적용하지만 더 완벽한 보안을 추구하는 경우 4096비트 보안키를 적용합니다.
조금 오래되었지만 2048비트에 비해서 4096비트는 시간이 7배정도 더 걸린다는 지적도 있었고 2048비트면 보안으로 충분하다는 지적도 있었던 적이 있습니다.
그러나 최근에는 2048비트로는 보안에 취약해진다는 평가가 많아지는 것 같구요. 그래서 4096비트 적용을 많이 추천하는 것으로 보입니다.
이를 위한 명령은 아래와 같습니다. 4096비트다보니 생성 시간이 많이 걸립니다.
참고로 ssl 폴더를 nginx 아래에 만들기도하지만 다른 곳에 만들어 conf 파일에 정확한 폴더 위치만 지정해주면 됩니다.
mkdir /etc/nginx/ssl
openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096
Code language: PHP (php)
이후 nginx conf 파일에 아와 같은 명령을 추가합니다.
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
Code language: PHP (php)
2.4. Cipher 강화
보안강화와 호환성과의 아슬아슬한 줄타기가 필요한데요.
서버에서 직접 확인해보는 방법
서버에서 어떠한 Cipher을 사용해야하는지를 아래 명령으로 알아 볼 수 있습니다.
/usr/bin/openssl ciphers -s -v
Code language: PHP (php)
그러면 아래 그림처럼 cipher 리스트가 주룩 뜹니다. 여기를 기반으로 cipher 리스트를 정리해도 됩니다. 다만 이 방법이 효욜적이고 정답을 찾을 수 있는 길인지는 모르겠습니다. 전문가의 영역이라는 생각이 들기 때문에..
모질라 재단 추천 이용 방법
이렇게 어렵게 하지말고 모질라 재단(theMozilla Foundation)에서 추천하는 명령을 이용하는 방법이 있습니다. .
여기에서는 nginx, Apache 등의 웹서버 버젼과 OpenSSL버젼 그리고 모던 브라우져 중심인지 아니면 구 부라우져까지 지원할 것인지 옵션에 따라 최적의 세팅 방안을 제안하고 있습니다.
웹서버등드의 버젼이 올라가면 세팅 방업이 변경되므로 종종 들러서 참고하는 게 좋을 것 같네요.
아래는 모던 브라우저 지원용 명령입니다. 테스트해보니 대부분의 운영체제에서 아주 완벽하게 호환됩니다.
다만 일부 보안이 취약하다는 경고가 나오고 덕분에 점수는 낮아져 목표한 100점 달성에는 실패합니다.
아래는 nginx 1.14, openssl 1.1.1e에서 모던 브라우저 지원 기준입니다.
ssl_prefer_server_ciphers on;
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
Code language: PHP (php)
만약 IE6/WinXP를 지원한다면 아래 명령이 추천됩니다. 이는 훨씬 더 호환성을 높인 것이죠. 아래는 물로 모질라 재단 추천 명령을 인용했습니다.
마찬가지로 이 명령 또한 nginx 1.14, openssl 1.1.1e 기준입니다.
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
ssl_prefer_server_ciphers on;
Code language: PHP (php)
2.5. SSL Stapling
SSL Stapling이 더 보안을 강화할 수 있느냐에 대해서는 설왕설래가 있지만 적용해 봅니다.
ssl_stapling on;
ssl_stapling_verify on;
Code language: PHP (php)
2.6. SSL Sessions
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
Code language: PHP (php)
2.7. ssl_ecdh_curve
ssl_ecdh_curve secp384r1;
Code language: PHP (php)
2.8. HTTP Headers
HTTP header에 최소한 추가가 좋다고 합니다.
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options: nosniff;
add_header "X-XSS-Protection" "1; mode=block";
add_header Strict-Transport-Security: max-age=15768000; includeSubDomains
Code language: PHP (php)
X-Frame-Options
위 명령 중 첫번째에 나오는 X-Frame-Options은 페이지 안에 프레임과 같은 다른 페이지를 불러오는 것을 제어하는 것으로 아래처럼 세가지 옵션이 있는데요.
X-Frame-Options: deny
X-Frame-Options: sameorigin
X-Frame-Options: allow-from https://example.com/
Code language: PHP (php)
- deny는 모든 프레임을 무효하하는 것입니다.
- sameorigin은 도메인 기준으로 같은 사이트 내에서는 불러올 수 이있지만 다른 사이트 페이지는 불러올 수 없습니다.
- allow-from https://example.com/은 불러올 수 있는 주소를 설정하는 것입니다.
nginx에서는 아래와 같은 방식으로 적용합니다.
add_header X-Frame-Options sameorigin always;
Code language: PHP (php)
X-Content-Type-Options
잘못된 MIME타입이 포함된 응답이 있으면 거부하라는 옵션입니다.
add_header X-Content-Type-Options: nosniff;
Code language: PHP (php)
X-XSS-Protection
세번째의 add_header X-XSS-Protection 옵션은 XSS를 통한 세션 하이제킹을 막도록하는 명령으로 아래 네가지 옵션이 있습니다.
X-XSS-Protection: 0
X-XSS-Protection: 1
X-XSS-Protection: 1; mode=block
X-XSS-Protection: 1; report=<reporting-uri>
Code language: PHP (php)
nginx에서는 아래와 같은 방식으로 적용합니다.
add_header "X-XSS-Protection" "1; mode=block";
Code language: PHP (php)
Strict-Transport-Security
리스폰스 헤더가 HHTP대신 HTTPS만 통과토록 하라는 것으로 아래와 같은 옵션이 있습니다.
Strict-Transport-Security: max-age=<expire-time>
Strict-Transport-Security: max-age=<expire-time>; includeSubDomains
Strict-Transport-Security: max-age=<expire-time>; preload
Code language: PHP (php)
2.9. Gzip사용 중지
사이트 속도를 위해서 Gzip 사용이 적극 궈장되고 있지만 SSL 보안을 위해서는 Gzip 사용을 중지하라고 권고합니다.
gzip off;
Code language: PHP (php)
3. SSL 인증서 세팅 최종 제안
위와 같은 여러 항목을 검토해 최적 설정으로 제안된 코드입니다.
nginx 사용 기준이구요. 모던 브라우저 지원 기준으로 구형 브라우져에 대한 지원은 채택하지 않았습니다.
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 10m;
ssl_protocols TLSv1.2 TLSv1.3; # Requires nginx >= 1.13.0 else use TLSv1.2
ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';
ssl_prefer_server_ciphers on;
ssl_stapling on;
ssl_stapling_verify on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_ecdh_curve secp384r1;
add_header X-Frame-Options SAMEORIGIN always;
add_header X-Content-Type-Options: nosniff;
add_header "X-XSS-Protection" "1; mode=block";
add_header Strict-Transport-Security: max-age=15768000;
Code language: PHP (php)
만약 쇼핑몰 중에서 다양한 구형 브라우저도 지원하겠다면 아래 내용으로 변경하면 좋을 것 같습니다.
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_ciphers 'ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA:ECDHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES256-SHA:ECDHE-ECDSA-DES-CBC3-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:DES-CBC3-SHA:!DSS';
Code language: PHP (php)
4. 마치며
이렇게 설정을 했음에도 불구하고 4가지 항목에 대해서 100점을 맞아 A+점수를 획득하기는 어렸웠습니다.
모던 브라우저 내에서의 호환성 유지를 고수한다면 key exchange나 Cipher Strength 측면에서 97점이상을 확보하기가 쉽지는 않더군요.
4가지 할목 모두에서 100점을 맞아 A+ 도전은 좀 더 연구해야할 것 같습니다.
[참고]
이 글은 쇼핑몰을 구축하는 가정의 삽질기를 기록하고 있는 puripia.com의 “SSL 보안 등급 A+에 도전하는 Let’s Encrypt 인증서 세팅 방법”에 실린 글을 협의해 여기에서도 같이 공유하고 했습니다.