Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

Lets' Encrypt로 무료로 HTTPS 지원하기

웹에서 HTTPS는 보안을 위해서 기본적으로 지원해야 하는 부분이다. 구글에서도 작년부터 HTTPS를 지원 여부를 사이트의 신뢰할 수 있다는 척도로 판단하고 검색 순위에서 올리겠다고 발표했다. 이 블로그에서도 HTTPS를 붙이고 싶었지만, SSL 인증서를 구매해야 하다 보니 개인 블로그에서는 부담돼서 적용을 못 하고 있었다.

Lets' Encrypt

Lets' Encrypt는 HTTPS를 사용하기 위해 SSL을 구매해야 하는 부분이 HTTPS 보급에 방해된다고 생각해서 SSL을 무료로 제공해서 HTTPS를 보급하기 위해 작년 말에 만들어졌다. 초기에는 Mozilla, Cisco, Akamai, EFF, id entrust 등이 모여서 ISRG(Internet Security Research Group)라는 새로운 SSL 인증기관을 만들어서 올해 SSL을 무료로 제공하겠다고 발표했다. 지금은 이 Lets' Encrypt에 Facebook, 워드프레스를 만드는 Automattic, shopify 등 많은 회사가 스폰서로 참여하고 있다.

Let's Encrypt 홈페이지 헤드라인

올여름부터 약간씩 구체적인 내용이 나오기 시작하면서 클로즈 베타로 신청해서 진행하다가 지난 12월 3일부터 퍼블릭 베타를 시작했다. 나는 클로즈 베타를 신청해서 초대권을 받았었지만 귀차니즘에 설치를 못 하고 있다가 이번에 퍼블릭 베타가 시작되어 설치하고 적용해 봤다. 설치는 공식 문서를 참고했다.

Let's Encrypt 클라이언트

Lets' Encrypt를 통해서 인증서를 발급받으려면 Let's Encrypt 클라이언트를 사용해야 한다. 아직 퍼블릭 베타이므로 클라이언트도 아직 베타 상태이지만 내 경우에는 적용을 해봤을 때 큰 문제가 없었다. 내 서버 환경은 Ubuntu 13.04이다.

$ git clone https://github.com/letsencrypt/letsencrypt

클라이언트 저장소에서 소스를 다운받는다.

$ ./letsencrypt-auto --help
Bootstrapping dependencies for Debian-based OSes...
Get:1 http://apt.newrelic.com newrelic Release.gpg [198 B]
Hit http://archive.ubuntu.com trusty Release.gpg
Get:2 http://apt.newrelic.com newrelic Release [3,364 B]
Get:3 http://apt.newrelic.com newrelic/non-free amd64 Packages [16.3 kB]
Get:4 http://archive.ubuntu.com trusty-updates Release.gpg [933 B]
Hit http://ppa.launchpad.net raring Release.gpg
Get:5 http://archive.ubuntu.com trusty-security Release.gpg [933 B]
...

다운받은 폴더에 들어가서 letsencrypt-auto --help를 실행하면 자동으로 관련 의존성을 다운받아서 설치한다. 의존성이 많아서 꽤 많은 시간이 걸린다. 설치를 다 한 뒤에 다시 --help의 내용을 보면 다음과 같다.

$ ./letsencrypt-auto --help
Updating letsencrypt and virtual environment dependencies.......
Running with virtualenv: sudo /home/outsider/.local/share/letsencrypt/bin/letsencrypt --help

  letsencrypt [SUBCOMMAND] [options] [-d domain] [-d domain] ...

The Let's Encrypt agent can obtain and install HTTPS/TLS/SSL certificates.  By
default, it will attempt to use a webserver both for obtaining and installing
the cert. Major SUBCOMMANDS are:

  (default) run        Obtain & install a cert in your current webserver
  certonly             Obtain cert, but do not install it (aka "auth")
  install              Install a previously obtained cert in a server
  revoke               Revoke a previously obtained certificate
  rollback             Rollback server configuration changes made during install
  config_changes       Show changes made to server config during installation
  plugins              Display information about installed plugins

Choice of server plugins for obtaining and installing cert:

  --apache          Use the Apache plugin for authentication & installation
  --standalone      Run a standalone webserver for authentication
  (nginx support is experimental, buggy, and not installed by default)
  --webroot         Place files in a server's webroot folder for authentication

OR use different plugins to obtain (authenticate) the cert and then install it:

  --authenticator standalone --installer apache

More detailed help:

  -h, --help [topic]    print this message, or detailed help on a topic;
                        the available topics are:

   all, automation, paths, security, testing, or any of the subcommands or
   plugins (certonly, install, nginx, apache, standalone, webroot, etc)

퍼블릭 베타 이전에는 설치시도를 안 해봤지만 관련 글을 보면 명령어가 약간은 달라진 부분이 있다. 이 명령어를 이용해서 인증서를 받아야 하는데 Let's Encrypt 클라이언트에서 설치를 위한 여러 가지 플러그인을 제공하고 있다. 그래서 Apache 웹서버를 사용하고 있다면 --apache 옵션으로 Apache 플러그인을 사용하면 인증서 발급부터 서버 설정까지 자동으로 해주는 것으로 보인다. Nginx 플러그인도 제공하고 있지만, 아직 실험단계로 사용을 적극적으로 추천하지 않고 클라이언트에도 기본으로 포함되어 있지 않다.

나 같은 경우 nginx 웹서버를 사용하고 있었으므로 플러그인을 사용하는 대신 수동 인증서 발급 후 설치하는 방법을 사용했다.

Let's Encrypt 인증서 발급

플러그인을 사용하지 않고 수동으로 인증서만 받으려면 ./letsencrypt-auto certonly --manual 명령어를 사용하면 된다.

$ ./letsencrypt-auto certonly --manual
Updating letsencrypt and virtual environment dependencies.......
Running with virtualenv: sudo /home/outsider/.local/share/letsencrypt/bin/letsencrypt certonly --manual

이 명령어를 실행하면 인증서발급을 위한 정보를 받기 위한 화면이 나온다.

이메일을 입력받는 설치 과정

먼저 인증서에 관련된 연락을 받을 이메일을 입력한다. 그다음에는 약관에 대한 동의를 물어본다.

도메인을 입력받는 설치 과정

인증서를 사용할 도메인을 물어본다. 여기서는 블로그에 사용할 것이므로 하나만 입력했지만 여러 도메인을 사용하려면 콤마나 공백으로 분리해서 여러 도메인을 입력하면 된다.

IP 수집동의를 묻는 설치 과정

마지막으로 현재 도메인이 공개적으로 수집되는 부분에 대한 안내가 나온다. 인증서만 발급받는 경우에는 운영 서버에서 직접 하지 않고 로컬에서 발급받아서 서버에 업로드 후 적용할 수도 있으므로 이 부분에 대한 안내이다. 나 같은 경우에는 운영서버에서 직접 발급을 받았으므로 Yes를 선택했다. 여기까지 완료하고 나면 다음과 같은 안내메시지가 나오고 대기상태가 된다.

Make sure your web server displays the following content at
http://blog.outsider.ne.kr/.well-known/acme-challenge/6J19PJntZgEYHiGDKQp97PtdZ4KsNare1jP3dotIB2U before continuing:

6J19PJntZgEYHiGDKQp97PtdZ4KsNare1jP3dotIB2U.ez2wa0J1JFO5k_X6K6nOXT_rklKlENka8WuYTIrfVmc

If you don't have HTTP server configured, you can run the following
command on the target server (as root):

mkdir -p /tmp/letsencrypt/public_html/.well-known/acme-challenge
cd /tmp/letsencrypt/public_html
printf "%s" v6jTNjl519uT9PQO0JKEOoiaY2mA8sy4wloh0HpMV7M.ez2wa0J1JFO5k_X6K6nOXT_rklKlENka8WuYTIrfVmc > .well-known/acme-challenge/v6jTNjl519uT9PQO0JKEOoiaY2mA8sy4wloh0HpMV7M
# run only once per server:
$(command -v python2 || command -v python2.7 || command -v python2.6) -c \
"import BaseHTTPServer, SimpleHTTPServer; \
s = BaseHTTPServer.HTTPServer(('', 80), SimpleHTTPServer.SimpleHTTPRequestHandler); \
s.serve_forever()"
Press ENTER to continue

이 단계는 내가 발급받는 인증서의 주인이 정말 나인지를 확인하는 단계이다. 이 확인 방법으로는 특정 URL에 요청을 보내서 지정된 키가 응답으로 오는지 확인하는 방법을 취한다. 위에 나온 안내대로 http://blog.outsider.ne.kr/.well-known/acme-challenge/6J19PJntZgEYHiGDKQp97PtdZ4KsNare1jP3dotIB2U URL로 요청을 보냈을 때 중간에 나온 6J19PJntZgEYHiGDKQp97PtdZ4KsNare1jP3dotIB2U.ez2wa0J1JFO5k_X6K6nOXT_rklKlENka8WuYTIrfVmc을 반환해야 이 인증단계가 마무리된다. 하단에 나온 부분은 웹서버가 따로 없는 경우 python으로 웹서버를 임시로 띄어서 이 인증단계를 하는 방법을 알려주는 것이다.

나 같은 경우는 nginx를 이미 사용하고 있으므로 nginx에 이 설정을 추가했다.

location /.well-known {
  root /home/outsider/www/well-known;
}

위처럼 추가하고 /home/outsider/www/well-known/.well-known/acme-challenge/B-4nYiiS8NlRP4Lq4UhRZOZiREDlmSqzFiFzM6B3MAI 경로에 파일을 생성했다.(파일이름이 B-4nYiiS8NlRP4Lq4UhRZOZiREDlmSqzFiFzM6B3MAI이다.) 그리고 이 파일에 위의 키 내용을 입력하고 nginx를 리로드해서 위 URL로 요청을 보냈을 때 정상적으로 응답이 와야 한다.

이 설정을 완료한 뒤에 엔터를 누르면 확인을 진행하는데 이 인증단계에 실패하면 다음과 같은 오류가 나온다.

IMPORTANT NOTES:
- The following 'urn:acme:error:unauthorized' errors were reported by
  the server:
  Domains: blog.outsider.ne.kr
  Error: The client lacks sufficient authorization

인증이 제대로 이뤄졌다면 다음과 같은 성공 메시지와 함께 인증서가 생성된다.

IMPORTANT NOTES:
 - Congratulations! Your certificate and chain have been saved at
   /etc/letsencrypt/live/blog.outsider.ne.kr/fullchain.pem. Your cert
   will expire on 2016-03-04. To obtain a new version of the
   certificate in the future, simply run Let's Encrypt again.
 - If like Let's Encrypt, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le

위 내용을 보면 인증서가 2016-03-04에 만료된다고 나온 것을 볼 수 있다. 한번 발급받으면 평생 쓸 수 있는 것은 아니다. Let's Encrypt의 인증서는 90일 동안 유효한 인증서이므로 90일마다 새로 갱신을 해야 한다. 갱신에 대해서는 문서에 안내가 되어 있는데 이제 처음 사용했으므로 아직 갱신을 해보지 않았다.(갱신은 나중에 하게 될 때 다시...) /etc/letsencrypt/live/blog.outsider.ne.kr/ 아래를 보면 다음과 같은 파일이 생긴 것을 볼 수 있다.

cert.pem -> ../../archive/blog.outsider.ne.kr/cert1.pem
chain.pem -> ../../archive/blog.outsider.ne.kr/chain1.pem
fullchain.pem -> ../../archive/blog.outsider.ne.kr/fullchain1.pem
privkey.pem -> ../../archive/blog.outsider.ne.kr/privkey1.pem


nginx에 SSL 인증서 설치

웹사이트에서 HTTPS를 지원하려면 앞에서 발급받은 인증서를 nginx에 설정해야 한다. nginx의 SSL 설정은 Mozilla SSL Configuration Generator를 사용했다. 사용하는 웹서버와 관련 버전을 명시하면 권장하는 설정파일을 만들어 주므로 이 파일을 그대로 사용했다.

나 같은 경우는 nginx를 사용하고 많은 브라우저를 지원하기 위해 Intermediate를 선택했다. nginx는 1.6.2버전을 사용하고 OpenSSL 버전은 1.0.1c였다.(OpenSSL 버전 확인은 openssl version)

server {
  listen 443 ssl;

  # certs sent to the client in SERVER HELLO are concatenated in ssl_certificate
  ssl_certificate /path/to/signed_cert_plus_intermediates;
  ssl_certificate_key /path/to/private_key;
  ssl_session_timeout 1d;
  ssl_session_cache shared:SSL:50m;
  ssl_session_tickets off;

  # Diffie-Hellman parameter for DHE ciphersuites, recommended 2048 bits
  ssl_dhparam /path/to/dhparam.pem;

  # intermediate configuration. tweak to your needs.
  ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
  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:ECDHE-RSA-DES-CBC3-SHA:ECDHE-ECDSA-DES-CBC3-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';
  ssl_prefer_server_ciphers on;

  # HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
  add_header Strict-Transport-Security max-age=15768000;

  # OCSP Stapling ---
  # fetch OCSP records from URL in ssl_certificate and cache them
  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;

  resolver <IP DNS resolver>;

  ....
}

여기서 몇 가지 정보는 실제 자신의 정보로 바꾸어 주어야 한다.

ssl_certificate /etc/letsencrypt/live/blog.outsider.ne.kr/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/blog.outsider.ne.kr/privkey.pem;

ssl_certificatessl_certificate_key를 발급받은 파일 중에 fullchain.pemprivkey.pem로 지정한다.

ssl_dhparam /etc/letsencrypt/live/blog.outsider.ne.kr/dhparam.pem;

ssl_dhparam는 openssl dhparam -out dhparam.pem 2048명령어를 통해서 새로 생성해야 한다.

ssl_trusted_certificate /etc/letsencrypt/live/blog.outsider.ne.kr/chain.pem;

resolver 8.8.8.8 8.8.4.4 valid=86400;
resolver_timeout 10;

ssl_trusted_certificate부분에는 앞에서 발급받은 chain.pem 파일을 사용하고 resolver에는 구글의 Public DNS를 사용했다. 이렇게 하면 SSL 설정은 완료되었으므로 server { }부분 안에 서버의 관련 설정을 추가하면 된다.

브라우저에 정상적으로 표시된 SSL 인증서

인증서 정보를 보면 Lets' Encrypt에서 발급받은 인증서라면서 녹색 자물쇠 표시가 예쁘게 잘 나온다. 덕분에 무료로 블로그에서도 HTTPS를 지원하게 되었다. ㅎㅎ

2015/12/06 05:00 2015/12/06 05:00