Let’s EncryptとNginx : セキュアなWebデプロイメントの現状

最近まで、SSL暗号化通信は「あると好ましい機能」という程度にしか考えられていませんでした。そのため、安全なのはアプリのログインページだけというサービスが数多く存在していました。

しかし、状況は良い方向へと変化しています。現在では暗号化は必須と考えられ、ほとんどの開発者が導入を義務付けています。また、巨大検索エンジンGoogleでは、SSLの導入が検索結果の順位を決定する要因にさえなっています。

しかし、SSLが広範に普及しているにも関わらず、セキュアなWebサービスを構築することは、未だに面倒で、時間がかかり、エラーの原因になりやすいと考えられています。

最近この分野では、Let’s Encryptが、SSL証明書をより広く普及させ、Webサイトのセキュリティ維持に係るワークフローを大幅に簡略化しようと取り組んでいます。

強力なWebサーバNginxや、他のハードニング方法と組み合わせれば、Qualys SSL LabsSecurityHeaders.ioといった人気のアナライザで最高グレードのセキュリティ評価Aの獲得が可能です。

この記事では、評価Aを獲得するために必要な手順を説明していきます。

これから行うこと

以下がこの記事で扱う内容です。

  • デモサイトをホストするクラウドインスタンスを生成する。
  • サーバの基本的なハードニングをいくつか行い、Nginxをセットアップする。
  • 新しいLet’s encryptの証明書をインストールし、自動更新のセットアップを行う。
  • Nginxのコンフィギュレーションのハードニングを行う。
  • セキュリティヘッダのハードニングを行う。
  • 目標とする栄光のセキュリティ評価A+を獲得する。

このチュートリアルでは、Exoscaleが統合型のファイアウォールとDNS管理システムを提供する点を考慮し、これをクラウドプロバイダとして利用します。更に、Exoscaleはデータの安全性/プライバシー及びセキュリティを重視しているという点も選択した理由の一つです。もちろん、他のクラウドや従来のホスティングサービスを使っている場合でも、この記事の内容は対応しています。

Let’s Encryptの概要

Let’s Encryptは、SSL/TLSサーバ証明書を無料で発行するとともに、証明書の発行プロセスを自動化した新しいオープンソースの認証局(CA)です。そのルート証明書は多くのブラウザで高い信頼を得ています。また、Let’s Encryptでは証明書の生成、検証、署名、インストール、更新といった面倒なワークフローを減らそうと積極的に取り組んでいます。

次に進む前に、次の注意事項を確認してください。Let’s Encryptの使用を検討する場合、いくつかの懸念事項が残っています。

  • まだベータ版である。
  • ルート権限が必要である。
  • クライアントは、まだNginxを”正式に”サポートしていない(ただし、問題なく動作する)。
  • 依存関係にあるパッケージを自動的にインストールする(AugeasgccPythonなど)。
  • 制限があり、証明書の発行を要求できるのは、与えられたドメイン1つにつき、1週間に5回までである。
  • 証明書の有効期限は90日である。

軽くて制約の少ない他のクライアントを代わりに使って証明書を発行することもできますが、このチュートリアルでは説明しません。

公式のドキュメンテーションはこちらからご覧いただけます。一読の価値がある内容となっています。

インフラストラクチャのセットアップ

それでは、新しいクラウドインスタンスの生成から始めましょう。まずは、SSH公開鍵が手元に必要です。自分の鍵を持っていない場合や、急いでセットアップしたい場合は、Exoscaleを使うとマシンが起動するより先にオンザフライで鍵を生成できます。SSH鍵のメニューから、自分の鍵を生成しましょう。SSH鍵を初めて扱う場合は、ガイドラインをご覧ください。このチュートリアルでは、SSH鍵とは何かを知っていて、使用方法を理解していることを前提としています。

Exoscaleポータル(または各自が選んだクラウドプロバイダ)で、Linux Ubuntu 14.04を起動させます。このデモ用には、microインスタンス(RAM 512MB、1vCPU、ディスク容量10GB)で十分過ぎるくらいでしょう。生成物からSSH鍵を選び、”デフォルト”のセキュリティグループがチェックされていることを確認します(詳しくは後述)。

数秒以内にインスタンスが入手でき、使用できる状態になります。この段階で、DNSのセットアップを続けるために必要なIPアドレスを記録することができます。

Our instance detailed view
ExoscaleはDNSゾーンのホスティングサービスを提供しているので、インターフェースを離れる必要はありません。DNSで新しいゾーンを作成すればいいのです(この例では「letsecure.me」の部分です。ここに自分のドメインを記入することになります)。

DNS zone creation
この段階で、”多様な用途で使える”(ワイルドカードのような)CNAMEレコードを加えるだけでなく、新たに生成されたインスタンスのIPアドレスの値と共に”A”レコードも加えることができます。

DNS record creation
これでDNSレコードが設定できました。Exoscaleを使ってこのチュートリアルを受ける場合は、以下に示すドメインのネームサーバを忘れずにアップデートしてください。ドメインレジスタのアドミニストレーションコンソールで操作できるはずです。

  • ns1.exoscale.com
  • ns1.exoscale.ch
  • ns1.exoscale.net
  • ns1.exoscale.io

サーバの基本的なセキュリティハードニング

ようやくクラウドインスタンスを扱う準備ができました。しかし、証明書やWebサービスに取り掛かる前に、まずは初歩的なセキュリティのベストプラクティスをいくつか取り入れていきます。

ファイアウォール側では、要求されたトラフィックのみを許可し、それ以外のトランジットを拒否する必要があります。具体的には以下のルールを加えなければいけません。

  • ポート番号22(SSH)
  • ポート番号80(HTTP)
  • ポート番号443(HTTPS)
  • ICMP ping(強制ではありませんが便利です)

Exoscaleのファイアウォールはセキュリティグループと呼ばれるインターフェースを通して管理されています。デフォルトでは上りトラフィックは全て拒否され、下りトラフィックは全て許可されています。マシンの詳細について、”デフォルト”のセキュリティグループに登録されているか確認しなければいけません。また、”デフォルト”のグループを言及されているルールに基づき修正する必要があります。他のクラウドプロバイダでは、似たようなシステムがある場合もありますし、自分のファイアウォールソフトウェアをインストールしなければならない場合もあります。また、UbuntuではUFWを選ぶのが妥当で簡単でしょう。

Firewall rules
他に推奨されているマシンのハードニング方法は、SSHと鍵ペア認証を通して処理する方法だけです。最近では、ほとんどのクラウドプロバイダがこのオプションを提供しています。ここまでの説明に沿って作業を進めていれば、既にExoscaleでデプロイされた鍵があるはずです。しかし、説明に沿って作業をしていなかった場合や、選んだクラウドプロバイダで似たようなワークフローが提供されていない場合は、このタイミングで鍵をアップロードしてください。このチュートリアルでは、この部分に関しては理解していることを前提とし、詳細を省きます。

これで、ubuntuユーザとしてSSH経由でログインできます。

 ssh ubuntu@yourdomain.here

そして、SSH鍵認証を使っているのであれば、その場合のみ、SSHパスワード認証を無効にすることができます。

sudo sed -i 's|PasswordAuthentication yes|PasswordAuthentication no|g' /etc/ssh/sshd_config
sudo service ssh restart

UFWを使っている場合は、下記のルールを追記してください。

sudo ufw allow out 22/tcp
sudo ufw allow out 80/tcp
sudo ufw allow out 443/tcp

次に、全てのソフトウェアアップデートとパッチを適用し、インスタンスを再起動させます。

sudo apt-get update && sudo apt-get dist-upgrade -y && sudo reboot

これによって、全てのソフトウェアが最近のバグ修正やセキュリティパッチを含む最新状態になります。更に、システムが常に最新のパッチが適用された状態になっていると良いと思いませんか? 自動セキュリティアップデートを有効にすれば、可能です。

sudo dpkg-reconfigure --priority=low unattended-upgrades

このようにすれば、いつ重要なセキュリティアップデートがリリースされても、システムが自動的にアップデートされて、全てがセキュアな状態を維持します。

fail2banをインストールしてSSH総当たり攻撃を予防するのも、グッドプラクティスです(具体的に、パスワード認証を使っている場合)。

sudo apt-get install -y fail2ban

基本的なNginxのセットアップ

これで全てがセキュアな状態になったので、Nginxに取り掛かることができます。Ubuntuリポジトリからパッケージをインストールするつもりはありません。最新の”メインライン”ブランチのみで見られる機能(HTTP/2など)が必要になるからです。以下を使ってNginxの公式リポジトリを追加することができます。

# Sadly the key is being served over http, so you need to check it's sha256 hash to ensure that it hasn't been somehow compromised
wget --quiet http://nginx.org/keys/nginx_signing.key -O nginx_signing.key && sha256sum nginx_signing.key
# At the time of writing the sha256sum is "dcc2ed613d67b277a7e7c87b12907485652286e199c1061fb4b3af91f201be39"
# Please ensure that you get the same result before proceeding further
sudo apt-key add nginx_signing.key
echo "deb http://nginx.org/packages/mainline/ubuntu/ trusty nginx" | sudo tee --append /etc/apt/sources.list.d/nginx_org_packages_mainline_ubuntu.list
sudo apt-get update && sudo apt-get install -y nginx

Webサイトが保存される予定の場所に対象フォルダを作ってください。

sudo mkdir /var/www/
# download our demo website
wget https://github.com/llambiel/letsecureme/releases/download/1.0.0/demo.tar.gz
sudo tar zxf demo.tar.gz -C /var/www
sudo chown -R root:www-data /var/www/

デフォルトのNginxの設定ファイルを削除し、空のファイルから新規作成してください。

sudo mv /etc/nginx/conf.d/default.conf /etc/nginx/conf.d/default.conf.orig
sudo touch /etc/nginx/conf.d/default.conf

Let’s Encryptのクライアントは、証明書を要求しているドメインの認証に必要なテンポラリファイルを作らなければなりません。そのために、/etc/nginx/conf.d/default.confにあるNginxの設定ブロックを次のように調整する必要があります。

server {
    listen 80;
    server_name default_server;
    root /var/www/demo;
}

Nginxをリロードしてこの設定変更を適用してください。差し当たりのNginxの設定は以上です。

sudo nginx -t && sudo nginx -s reload

Let’s Encryptセットアップ、SSL証明書とNginx HTTPS設定

Let’s Encryptを適用しましょう。公式ドキュメンテーションにある通り、Gitリポジトリをcloneしてletsencrypt-autoを起動させる必要があります。

sudo apt-get install -y git
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt
/opt/letsencrypt/letsencrypt-auto

最初に言ったように、セットアップスクリプトは、必要な依存関係にあるパッケージを全て自動的にインストールします。便利ですが、自分のマシンにインストールするものをコントロールできないということになります。

もうこれであなたのドメインの証明書を要求できます。失効通知のためのEメールアドレスを入力して、利用規約に同意するよう求められます。

export DOMAINS="yourdomain.here,www.yourdomain.here"
export DIR=/var/www/demo
/opt/letsencrypt/letsencrypt-auto certonly --server https://acme-v01.api.letsencrypt.org/directory -a webroot --webroot-path=$DIR -d $DOMAINS

もちろん、DOMAINSリストにある自分のドメイン名を使う必要があります。

これで、証明書が発行されてインストールされるはずです。

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at
  /etc/letsencrypt/live/letsecure.me/fullchain.pem. Your cert will
  expire on 2016-XX-XX. To obtain a new version of the certificate in
  the future, simply run Let's Encrypt again.
- Your account credentials have been saved in your Let's Encrypt
  configuration directory at /etc/letsencrypt. You should make a
  secure backup of this folder now. This configuration directory will
  also contain certificates and private keys obtained by Let's
  Encrypt so making regular backups of this folder is ideal.
- If you like Let's Encrypt, please consider supporting our work by:

Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate

Let’s Encryptは、コンフィギュレーションと証明書を/etc/letsencryptディレクトリの中で整理しています。Let’s Encryptドキュメンテーションに、このディレクトリの構造と内容についての詳細情報があります。

新しい証明書を使うには、Nginxにその扱いを命令して、443ポートをSSLに設定する必要があります。/etc/nginx/conf.d/default.confにある次の最小限のコンフィギュレーションブロックを使うこともできます。

server {
    listen 443 ssl;
    server_name yourdomain.here www.yourdomain.here;
    root /var/www/demo;
    ssl_certificate /etc/letsencrypt/live/yourdomain.here/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.here/privkey.pem;
}

Nginxをもう一度リロードしましょう。

sudo nginx -t &&  sudo nginx -s reload

Webブラウザでhttps://yourdomain.hereを見ましょう。
Webサイトのホームページは、HTTPSで見られるようになっているはずです。

前述したように、Let’s Encryptが発行する証明書の有効期限は90日のみです。確実に証明書を自動的に更新するために、短いスクリプトとcrontabを使います。

renewCerts.shという名前のファイルに以下のコードを保存してください。

#!/bin/sh
# This script renews all the Let's Encrypt certificates with a validity < 30 days

if ! /opt/letsencrypt/letsencrypt-auto renew > /var/log/letsencrypt/renew.log 2>&1 ; then
    echo Automated renewal failed:
    cat /var/log/letsencrypt/renew.log
    exit 1
fi
nginx -t && nginx -s reload

cronが毎日スクリプトを起動します。crontabがこれを読み込むようセットアップするには・・・

sudo crontab -e

そして、@dailyマクロの行を追加してください。

@daily /path/to/renewCerts.sh

保存してエディタを終了します。以下のコードを使って、忘れずにスクリプトを実行可能にしてください。

chmod +x /path/to/renewCerts.sh

おめでとうございます! これであなたのコンテンツは、自動更新される有効な証明書を持った状態でHTTPS経由で公開されます。

もしSSLアナライザでデフォルトのSSL/TLSコンフィギュレーションを使ってグレードをチェックすると、まだ、結果はあまり良くないでしょう。評価を上げるために、Nginxのコンフィギュレーションを少し変更しましょう。

Nginx SSL/TLSのハードニング

/etc/nginx/conf.d/default.confにある実際のコンフィギュレーションを削除し、下記のブロックと入れ替えましょう。自分のドメイン名に変更するのを忘れないでください。

server {
    listen 80;
    listen 443 ssl http2;
    server_name yourdomain.here www.yourdomain.here;
    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
    ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
    ssl_prefer_server_ciphers On;
    ssl_certificate /etc/letsencrypt/live/yourdomain.here/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/yourdomain.here/privkey.pem;
    ssl_session_cache shared:SSL:128m;
    add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";
    ssl_stapling on;
    ssl_stapling_verify on;
    # Your favorite resolver may be used instead of the Google one below
    resolver 8.8.8.8;
    root /var/www/demo;
    index index.html;

    location '/.well-known/acme-challenge' {
        root        /var/www/demo;
    }

    location / {
        if ($scheme = http) {
            return 301 https://$server_name$request_uri;
        }
    }
}

これが終わったら、Nginxをリロードしてください。

sudo nginx -t && sudo nginx -s reload

SSL/TLSコンフィギュレーションの詳細

今、追加した重要なコンフィギュレーション項目をいくつか見直しましょう。

listen 443 ssl http2;

この命令で、クライアントブラウザがサポート/要求している場合、NginxにSSL経由でリッスンし、また新しいHTTP/2規格での接続もサポートするよう指示しています。HTTP/2は、SSL/TLSだけです。

ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

古くて脆弱なSSLv2/SSLv3プロトコルを無効にして、TLSプロトコルだけを有効にしてください。

ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
ssl_prefer_server_ciphers On;

これは、Nginxにサポートを指示する暗号のリストです。古いWebブラウザによるサポートとセキュリティのバランスが最もよく取れているリストの1つだと思います。Nginxでは、クライアントから要求された暗号よりこのリストの暗号の方が好まれます。

ssl_stapling on;
ssl_stapling_verify on;

OCSP Staplingを有効にしましょう。OCSP Stapling についての詳細はこちらをご覧ください。

add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";

これでHTTPヘッダが追加され、クライアント側のブラウザに対して、ドメインとサブドメイン全てに1年間強制的にHTTPS接続をするよう指示することができます。
警告! 本番環境に適用する前に以下の点に注意してください。まず、全てのサブドメイン(もしあれば)も同様に保護されていることを確認しなければなりません。サブドメインも強制的にHTTPS接続を行うように指示するため、正しく設定されていない場合、サブドメインにアクセスできなくなります。

Qualys SSLでこのセットアップを再テストしてみましょう。

Qualys SSL final check
ほら、ご覧の通り良くなりました。最適なSSL/TLSコンフィギュレーションを使って保護されたセットアップになり、私たちの最初の目的が達成されました。

セキュリティヘッダのハードニング

さて、Webサイトのコンテンツや動作はどうでしょうか。Scott Helmeは、ヘッダを基にコンテンツのセキュリティグレードを評価する素晴らしいHTTPレスポンスヘッダアナライザを作成しました。

現在のセットアップをテストする場合(HTTPSを使ってテストするようにしてください)、結果は…あまり良くありません。

securityheaders.io first check
再び、HTTPヘッダを少し追加してNginxコンフィギュレーションを調整してみましょう。

add_header X-Content-Type-Options "nosniff" always;

X-Content-Type-Optionsヘッダは、ブラウザがMIMEスニッフィングを行ってコンテンツタイプを判断するのをやめさせて、強制的に宣言されたcontent-typeに基づいた動作を行わせるようにします。

add_header X-Frame-Options "SAMEORIGIN" always;

X-Frame-Optionsヘッダは、サイトのページをフレーム内に表示することを許可するかどうかをブラウザに伝えます。ブラウザに対し、サイトのページをフレーム内に表示させないように指示すれば、クリックジャッキングのような攻撃を防ぐことができます。

add_header X-Xss-Protection "1";

X-Xss-Protectionヘッダは、ほとんどのブラウザに組み込まれているクロスサイトスクリプティングフィルタに対するコンフィギュレーションを設定します。

add_header Content-Security-Policy "default-src 'self'";

Content-Security-Policyヘッダは、ブラウザがロードする可能性のあるコンテンツの承認されたソースを定義します。クロスサイトスクリプティング(XSS)攻撃に対して効果的な対策をとることができます。警告!このヘッダによって、ページが正常に動作しなくなり、Webサイトによるコンテンツのロードが妨げられる可能性があるので、本番環境のWebサイトにデプロイする前に慎重に計画を立てなければなりません。幸い、”レポートモード”が利用できます。このモードでは、ブラウザはデバッグコンソール内で問題を報告するだけで、実際にコンテンツをブロックすることはありません。これは、このヘッダをスムーズにデプロイするのに本当に役に立ちます。

report mode
このポリシーのコンフィギュレーションについては、こちらで詳細をご覧いただけます。

レポートモードは、以下を使って有効にすることができます。

Content-Security-Policy-Report-Only instead of Content-Security-Policy

Nginxのセキュアな最終コンフィギュレーション

最終的なNginxコンフィギュレーションは以下のようになります。

server {
     listen 80;
     listen 443 ssl http2;
     server_name yourdomain.here www.yourdomain.here;
     ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
     ssl_ciphers EECDH+AES128:RSA+AES128:EECDH+AES256:RSA+AES256:EECDH+3DES:RSA+3DES:!MD5;
     ssl_prefer_server_ciphers On;
     ssl_certificate /etc/letsencrypt/live/yourdomain.here/fullchain.pem;
     ssl_certificate_key /etc/letsencrypt/live/yourdomain.here/privkey.pem;
     ssl_session_cache shared:SSL:128m;
     add_header Strict-Transport-Security "max-age=31557600; includeSubDomains";
     add_header X-Frame-Options "SAMEORIGIN" always;
     add_header X-Content-Type-Options "nosniff" always;
     add_header X-Xss-Protection "1";
     add_header Content-Security-Policy "default-src 'self'; script-src 'self' *.google-analytics.com";
     ssl_stapling on;
     ssl_stapling_verify on;
     # Your favorite resolver may be used instead of the Google one below
     resolver 8.8.8.8;
     root /var/www/demo;
     index index.html;

     location '/.well-known/acme-challenge' {
        root        /var/www/demo;
      }

     location / {
              if ($scheme = http) {
                return 301 https://$server_name$request_uri;
              }
     }
}

Nginxをもう一度リロードして、新しいヘッダを適用してみましょう。

sudo nginx -t && sudo nginx -s reload

securityheaders.ioを使ってサイトを再スキャンしてください(繰り返しになりますが、https://プレフィックスをつけてスキャンするのを忘れないでください)。

securityheaders.io final check
評価”A”がもらえるはずです。はるかに良くなります。

A+を獲得できると思われるHPKP(HTTP Public Key Pinning、HTTP公開鍵ピンニング)を有効にしなかったことに気づいた人がいるかもしれません。実は、機能の理解が不十分で、慎重に計画されていない場合、そのヘッダはWebサイトをダメにしてしまう可能性があります。このテーマについては、今後このページで取り上げるかもしれないので、お楽しみに。

結論

WebサイトにSSL/TLSをデプロイする理由はたくさんあります。当然ながら、最も重要で理由の明らかなものはセキュリティです。閲覧者の一部にとっては信頼構築の印でもあります。Webサイトでアクティブな証明書を持っていることが欠点になることはありません。Let’s Encryptからの無料証明書があって、このチュートリアルに書かれた手順を踏めば、心配する理由は全くありません。

Let’s Encryptは、Nginxの上で簡単にデプロイし、保守できます。特定のSSL/TLSとブラウザヘッダのハードニングによって、最新のセキュアなWebデプロイメントを成し遂げることができます。

このプロジェクトのソースファイルは、GitHubから直接ダウンロードできます。

最先端のセキュアなWebデプロイメントにおいて今後出てくるベストプラクティスは言うまでもなく、Let’s EncryptとNginxの新しいリリースや改善に従って、このプロジェクトは拡大し、アップデートされ続けるでしょう。