まだプログラマーですが何か?

プログラマーネタとアスリートネタ中心。たまに作成したウェブサービス関連の話も http://twitter.com/dotnsf

タグ:https

Node.js + Express で作るウェブアプリケーションを HTTPS 対応し、更に「HTTPS 以外の通信を認めない」ように設定してみます。なお SSL 対応用のドメイン取得や証明書の取得は完了しているものとし、ドメインは mydomain.com 、秘密鍵ファイルと証明書ファイルがそれぞれ、
 ・/etc/letsencrypt/live/mydomain.com/privkey.pem (秘密鍵)
 ・/etc/letsencrypt/live/mydomain.com/cert.pem (証明書)
に存在しているものと仮定して以下を説明します。


【Node.js + Express で HTTPS 通信】
Node.js + Express で HTTPS 通信を行うには https モジュールを利用します:
var express = require( 'express' );
var app = express();

var fs = require( 'fs' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var https_port = 8443;

https_server.listen( https_port );

console.log( "server starging on " + https_port + ' ...' );

https モジュールを使って 8443 番ポートで待ち受ける https サーバーを作ります。その際に秘密鍵や証明書を指定することで SSL 通信を行うことができるようにしています。

このファイルを Node.js で実行して、ウェブブラウザや curl コマンドで https://mydomain.com:8443/hello にアクセスすることで動作を確認することができます(curl コマンドで localhost など、異なるドメインにアクセスする場合は $ curl https://localhost:8443/hello --insecure といった具合に --insecure オプションを付けて実行することで確認できます)。

また、上記の方法だと https のみアクセスできます(http だと接続エラー)が、http / https 両方でのアクセスに対応するには以下のように改良します:
var express = require( 'express' );
var app = express();
var settings = require( './settings' );

var fs = require( 'fs' );
var http = require( 'http' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );
var http_server = http.createServer( app );

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var http_port = 8080;
var https_port = 8443;

https_server.listen( https_port );
http_server.listen( http_port );

console.log( "server starging on " + http_port + ' / ' + https_port + ' ...' );

これを実行すると、http を 8080 番ポートで、https を 8443 番ポートで同時に待受けてレスポンスを返すことができるようになりました。ウェブブラウザや curl コマンドで http://mydomain.com:8080/hello や https://mydomain.com:8443/hello にアクセスして挙動を確認してください。


【HTTP 通信をしない(HTTPS へ転送)】
最後に、上記プログラムを更に改良して HTTP 通信をしない(強制的に HTTPS 通信に転送する)ための設定を加えます。それには Strict-Transport-Security ヘッダを付けてレスポンスを返すことで実現できます:
var express = require( 'express' );
var app = express();
var settings = require( './settings' );

var fs = require( 'fs' );
var http = require( 'http' );
var https = require( 'https' );
var options = {
  key: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/privkey.pem" ),
  cert: fs.readFileSync( "/etc/letsencrypt/live/mydomain.com/cert.pem" )
};
var https_server = https.createServer( options, app );
var http_server = http.createServer( app );

//add HSTS
app.use( function( req, res, next ){
  res.setHeader( 'Strict-Transport-Security', 'max-age=15552000' );
  next();
});

app.get( '/hello', function( req, res ){
  res.contentType( "text/plain" );
  res.writeHead( 200 );
  res.end( "Hello World." );
});

var http_port = 8080;
var https_port = 8443;

https_server.listen( https_port );
http_server.listen( http_port );

console.log( "server starging on " + http_port + ' / ' + https_port + ' ...' );

HSTS(HTTP Strict Transport Security) という仕組みを強制することで HTTP でリクエストを受け取っても HTTPS に強制的に変更(リダイレクト)されて通信を続ける、というものです。リダイレクトさせるには HTTP そのものを閉じるわけにはいかない(リダイレクト直前のリクエストを受け取らないといけない)ので、このようなレスポンスヘッダによって実現しています:

2019112500



【参考】
良い感じにHTTPS対応したexpress(Node.js)サーバを立てる
node.jsによるHTTPSサーバの作り方

IBM Cloud から提供されている、Web コンテンツ向けにセキュリティやパフォーマンスといった非機能要件を統合的に設計するサービス、それが CIS(Cloud Internet Services)です。mybluemix.net などの IBM Cloud から提供される標準ドメインではなく、独自ドメインを使うことが前提となりますが、そのドメインで運用するウェブアプリケーションやコンテンツ全般を保護して、安全なインターネット環境を提供することができる便利なサービスです:

https://www.ibm.com/jp-ja/cloud/cloud-internet-services
2019111600


この CIS は(独自ドメインの)DNS を使うことでオリジンサーバーとは別の( Cloudflare 社の)サーバーを経由してオリジンサーバーにアクセスします。この中間サーバーを経由することで必要なセキュリティ要件やコンテンツ保護要件を提供する、というものです。

この CIS を利用して、クライアント(ウェブブラウザ)からの SSL(https) 通信を実現するには大きく3種類の方法があります:

(1)クライアント→中間サーバー→オリジンサーバー 間すべての通信をセキュアに行う方法
(2)クライアント→中間サーバー 間はセキュアに行い、 中間サーバー→オリジンサーバー 間の通信は自己証明書を使った暗号化通信を行う方法
(3)クライアント→中間サーバー 間はセキュアに行い、 中間サーバー→オリジンサーバー 間の通信は HTTP 通信で行う方法
2019110703


①、②、③いずれも共通でクライアントと中間サーバーとの間はセキュアな SSL 通信を行います。中間サーバーとオリジンサーバー間の通信をどう考えるか、という点が異なります。ざっくりいうと①がもっともセキュアで、③がもっともセキュアでない方法となります。

①は理想的といえる方法で、全ての区間がセキュアな通信となります。②も通信自体は SSL で暗号化通信となりますが、その際に利用する証明書はいわゆる「自己証明書」となります。①と比較すると(外部の業者などを使わずに)自分で証明書を作ることができるので、コストや有効期限の自由度などの面でメリットがありますが、「要は誰でも(ドメインのオーナーでなくても)作れる証明書」を使った暗号化通信を行う点が異なります。この証明書を普通に使って直接 SSL 通信を行うと、普通は「セキュアな通信でないが、それでも通信するか?」といった確認メッセージを経て通信可能になったりしますが、中間サーバーとして CIS を入れることにより、クライアントはそういった手順を経ずに通信できるようになります。

そして③の方法ですが、これは中間サーバーとオリジンサーバーとの間の通信は暗号化しない方法です。セキュアかどうか、という観点では暗号化しない経路が存在しているわけで他の2つよりは劣っています。一方で、この方法では証明書の作成を必要としません。クライアントから見た証明書は CIS の中間サーバーが自動で発行されたものが使われ、オリジンサーバーには(そもそも暗号化通信をしないので)証明書が不要です。開発段階や試験段階など、インフラに関わる権限が小さい状況下で、かつ(API の仕様など)https 通信が必須となるような場合においては非常に有要な選択肢となります。またこの方法であれば(そもそも取得しないので)証明書の有効期限切れを心配する必要もありません。 また特にオリジンサーバーとして IBM Cloud の PaaS 環境を利用する場合であれば、オリジンサーバーのルーティングを編集してカスタムドメインでのホスト名でしかオリジンサーバーにアクセスできないように設定することで(HTTPS を使わずにアクセスするような)迂回路を遮断する設定にすることも可能です。

今回はこの③の方法でカスタムドメインの https アクセスを行うための設定を紹介します。

まず IBM Cloud の CIS サービスを利用してプロビジョン済みの状態とし、普通にドメインを登録して設定します。その上で「セキュリティ」カテゴリーの「TLS」内で動作モードを「クライアントからエッジ」に設定します(これが③の通信のための設定となります):
2019110701


なお、このモードの設定内容によって上記図の①~③の通信方法が変わります。以下のような関係です:
2019110704


また DNS の CNAME でオリジンサーバーとの紐付けを行います(下図の「信頼性」-「DNS」)。このカスタムドメインの頭につけるホスト名を指定して、CNAME の参照先となる別名を指定します。IBM Cloud の PaaS (米国南部リージョン)を使う場合は参照先を us-south.cf.cloud.ibm.com を指定します。また「プロキシー」を有効にします:
2019110702


なお、ここで指定する参照先と IBM Cloud PaaS のリージョンとの関係については以下を参照ください:
https://cloud.ibm.com/docs/cloud-foundry-public?topic=cloud-foundry-public-custom-domains

最後にオリジンサーバー側を用意します。IBM Cloud PaaS の場合であれば(CNAME で指定した名前).mybluemix.net でアクセスできるようなアプリケーションサーバーを CloudFoundry ランタイムとして作成します(このルーティングは最終的に削除します)。


また IBM Cloud PaaS の場合は「管理」メニューからカスタムドメインを登録する必要があります。この手順の詳細はこちらを参照ください(このリンク先では証明書をインポートする手順まで紹介していますが、今回ここでは SSL の設定を行う必要がないので不要です):
http://dotnsf.blog.jp/archives/1075713365.html


最後に上記で作成した Cloud Foundry ランタイムのルーティングを編集します。経路の編集から「経路の追加」を実行し、mybluemix.net ドメインと同じ名前を指定したカスタムドメイン用の経路を追加します(証明書をインポートしていないので鍵マークには×がついているはずです)。そしてもともとの mybluemix.net 側の経路を削除します。これでカスタムドメインを使った指定でないとアクセスすることはできなくなりました。最後に「保存」します:
2019110705


この状態でカスタムドメインを指定したホスト名で https アクセスを行ってみます。まず最初に CIS がリクエストを受け取ってアクセス内容を(アタックに相当するアクセスでないか)判断します:
2019110706


問題がないと判断されるとオリジンサーバーにルーティングが行われます。結果的に証明書を独自に発行することなくカスタムドメインを使った https 通信ができるようになりました:
2019110707



【参考】
IBM Cloud Certificate Manager で Let's Encrypt 無料 SSL 証明書を取得する



CentOS 7 で運用する独自ドメインのウェブサーバーを Certbot(Let's encrypt) で無料の証明書を取得して SSL 化しました。その手順の備忘録です:
le-logo-standard



【環境】
OS: CentOS 7.6(64bit)
HTTP: Apache HTTPD 2.4.6※

※正確には WordPress 5.2.2(ja) を MariaDB 5.5.60 で運用中。Apache と MariaDB は yum で導入、WordPress は公式サイトから最新版 latest-ja.tar.gz を取得して展開

Document Root: /var/www/html/wordpress
ドメイン名(仮): mydomain.com (DNS 設定済み)

1サーバーで1ドメイン運用、のシンプルなケースを想定しています。http では既に稼働中、という状態だと思ってください。


【準備】

Certbot のインストール
$ sudo yum install -y epel-release
$ sudo yum install -y certbot python-certbot-apache

root で動作確認
$ sudo -i
# certbot --version
certbot 0.36.0

certbot コマンドを実行し、バージョン番号が表示される(=インストールされている)ことを確認します。

【作業】

以下のコマンドを実行するにあたり、証明書を導入するサーバーに HTTP(TCP/80) および HTTPS(TCP/443) でパブリック・アクセスできるようにポート設定できているものとします。またDocument Root に Basic 認証などがかかっている場合は外しておきます(必須)。

証明書作成
# certbot certonly --webroot -w /var/www/html/wordpress -d mydomain.com

-w オプションで DocumentRoot、-d オプションでドメイン名を指定します。E メールアドレスの入力を求められるので自分のメールアドレスを入力して Enter 、そして利用規約に A(Agree) を押して、最後に N(No) を押します:
# certbot certonly --webroot -w /var/www/html/wordpress -d mydomain.com
Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator webroot, Installer None
Enter email address (used for urgent renewal and security notices) (Enter 'c' to
cancel): myname@xxx.co.jp
Starting new HTTPS connection (1): acme-v02.api.letsencrypt.org

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Please read the Terms of Service at
https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf. You must
agree in order to register with the ACME server at
https://acme-v02.api.letsencrypt.org/directory
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(A)gree/(C)ancel: A

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Would you be willing to share your email address with the Electronic Frontier
Foundation, a founding partner of the Let's Encrypt project and the non-profit
organization that develops Certbot? We'd like to send you email about our work
encrypting the web, EFF news, campaigns, and ways to support digital freedom.
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
(Y)es/(N)o: N

処理が成功すると以下のような "Conguratulations!" というメッセージが表示されます:
2019082900


ここまで完了していると、以下の4ファイルが作成されているはずです(mydomain.com 部分には指定したドメイン名が入ります):
  1. /etc/letsencrypt/live/mydomain.com/cert.pem (サーバー証明書(公開鍵))
  2. /etc/letsencrypt/live/mydomain.com/chain.pem (中間証明書)
  3. /etc/letsencrypt/live/mydomain.com/fullchain.pem (サーバー証明書と中間署名書の結合ファイル 今回は使いません
  4. /etc/letsencrypt/live/mydomain.com/privkey.pem (秘密鍵)

証明書を有効にする

/etc/httpd/conf.d/ssl.conf ファイルの以下3箇所を書き換えて&コメントを外すなどして有効にします:
  • SSLCertificateFile /etc/letsencrypt/live/mydomain.com/cert.pem
  • SSLCertificateKeyFile /etc/letsencrypt/live/mydomain.com/privkey.pem
  • SSLCertificateChainFile /etc/letsencrypt/live/mydomain.com/chain.pem
  • (fullchain.pem は使わない)

また、Basic 認証をかける場合はこのタイミングで元に戻して(有効にして)おきます。

HTTP サーバーを再起動
# systemctl restart httpd


以上です。僕が以前にウェブサーバーやそのアプリケーションを大量に作る仕事していた頃は無料で証明書作ってもらえるサービスはなくて、いわゆる「オレオレ証明書」に頼っていたのでした。改めていい時代になりました。


今月(2018年5月)から GitHub ページがカスタムドメインでも HTTPS 対応された、という発表がありました:
Custom domains on GitHub Pages gain support for HTTPS

2018051001


というわけで、自分が取得しているドメインを使って試してみました。以下、GitHub ページの紹介と、その手順を含めた報告です。


GitHub ページとは?

GitHub ページとは GitHub の仕組みを使った静的ウェブサイトのホスティングサービスです。GitHub アカウントを持っていれば、誰でも簡単にウェブサイトを作って公開することができるので、非常に便利です。


GitHub ページの作り方

index.html 一枚だけの非常にシンプルな例で紹介します。まずは普通に Github でリポジトリを作成し、GitHub ページ(ウェブサイト)に含めたい HTML ファイルその他をまとめてプッシュし、公開します:
2018051002


この時点で、同リポジトリが GitHub で普通に公開されている状態になりました:
https://github.com/dotnsf/githubpagessample


(今回、公開する index.html ファイルの中身)
https://raw.githubusercontent.com/dotnsf/githubpagessample/master/index.html


次にこのリポジトリを GitHub ページとして公開します。リポジトリのページから "Settings" メニューを選択します:
2018051003


少しスクロールして GitHub Pages の設定欄を表示し、Source が None になっている所を "master branch" に変更して "Save" します。これでこのリポジトリのマスターブランチが Github Pages として公開されることになります:
2018051004


"Save" が成功すると画面内に GitHub Pages として参照する場合の URL が表示されます。一般的にはここは https://(ログイン名).github.io/(リポジトリ名)/ となります:
2018051005


この URL にアクセスしてみると、先程のリポジトリのマスターブランチが HTTP サーバーのドキュメントルートとして扱われる感じになり、index.html ファイルが表示されます。またこの画面からもわかるように、このページは HTTPS 対応しています:
2018051102


以上、ここまでが GitHub ページの説明です。


カスタムドメインで GitHub ページを使ってみる

今回の更新で、上記の GitHub ページがカスタムドメインでも HTTPS 対応されるようになりました。この点を確認したいので、まずは GitHub ページをカスタムドメイン対応してみます。

そのためには GoDaddyお名前.comなどのドメイン業者でドメインを取得しておく必要があります。ここはどうしても無料というわけにはいかず、ドメインの種類にもよりますが、1年間 $10 程度かかってしまうことを理解した上で取得してください。ちなみに自分は(B'z ファンでもないのに) welove.bz というドメインを GoDaddy で取得しているので、このドメインを使って GitHub ページをカスタムドメイン対応する手順を以下に紹介します。

最初に、対象ドメインの DNS 設定を変更する必要があります。このページの "Configuring A records with your DNS provider" という項目によると、ホスト名の A レコードの IP アドレスを以下のように設定する必要があるようです:
2018051103


A レコードの IP アドレスを複数設定できる場合はこの4つの IP アドレスを設定します。僕が使っている GoDaddy では1つしか設定できないようだったので、一番上のアドレスに指定しました:
2018051104


この部分は同様の作業をドメイン業者の用意したツールを使って設定してください。 なおこの作業について、詳しくは GitHub のドキュメントも参照ください:
https://help.github.com/articles/setting-up-an-apex-domain/


この作業の後で、GitHub の該当リポジトリに CNAME という名前のファイルを1つ追加します。CNAME ファイルの中身にはカスタムドメイン名だけを記載します:
2018051105


このファイルをリポジトリに add して commit して push します:
2018051106

2018051107


最後に再びリポジトリの settings 画面を確認して、GitHub ページのカスタムドメインが設定されている(されていない場合は、ドメイン名を入力して "Save")ことを確認します。これで GitHub ページのカスタムドメイン設定ができました:
2018051108


この状態で http://(カスタムドメイン名) にアクセスすると、先程まで ***.github.io ドメインで動いていた GitHub ページが表示されます:
2018051101


これで GitHub ページのカスタムドメイン化が完了しました。



カスタムドメイン GitHub ページの HTTPS 対応

やっと本題です(苦笑)。ここまでの作業で GitHub ページがカスタムドメインで利用できるようになりました。今回の目的は「このページを HTTPS 対応にする」ことです。

そのための設定はリポジトリの settings で、カスタムドメインを指定した下に "Enforce HTTPS" というフィールドがあるので、ここにチェックを入れるだけ・・・
2018051101



・・・なのですが、まだ証明書が有効になっていないようです(チェックできない)。こうなると作業としてできることはないので、ひたすら待つ必要があります。

ちなみに、この状態で無理やり HTTPS を指定して https://(カスタムドメイン名)/ にアクセスすると「安全な接続ではない」と言われます。ここから例外を承認して・・・ということもできないわけではないですが、せっかくなのでしばらく待ちましょう:
2018051102


しばらく待つとこの部分のメッセージが変わり、"Enforce HTTPS" がチェック可能になります:
2018051103


チェックするとこのような画面になり、このリポジトリの GitHub ページは強制的に(HTTP でリクエストしても)HTTPS でアクセスされるようになります:
2018051104


実際にアクセスしてみました。ちゃんと HTTPS に転送されています:
スクリーンショット 2018-05-11 15.38.40


HTTPS のインフォメーションを確認しても(オレオレ証明書とかではなく)"Secure Connection" になっていることが確認できます:
スクリーンショット 2018-05-11 15.39.12



(おまけ)
ではこの証明書は誰がどうやって発行しているのだろう? と疑問に思ったのですが、"Verified by: Let's Encrypt" だそうです。なるほどね・・・:
スクリーンショット 2018-05-11 15.38.58


めでたし、めでたし。
ところでこの welove.bz ドメイン、なんかいい使いみちないかな? (^^;


Node.js + Express の環境で SSL を使う(https でアクセスできるようにする)方法を調べたのでまとめました。


まず SSL を使うための鍵ファイルと証明書ファイルを用意します。公式なドメインを所有していて、本物の鍵/証明書ファイルを持っているのであればそれを使っても構いません。試験的に試すのであれば、いわゆる「オレオレ証明書」を作成します。Linux 環境であれば openssl コマンドを使って、以下のように入力します:
$ openssl genrsa -out server_key.pem 2048
Generating RSA private key, 2048 bit long modulus
...............+++
..........................+++
e is 65537 (0x10001)

$ openssl req -batch -new -key server_key.pem -out server_csr.pem -subj "/C=JP/ST=Chiba/L=Funabashi/O=Jugeme/OU=Dev/CN=juge.me"

$ openssl x509 -in server_csr.pem -out server_crt.pem -req -signkey server_key.pem -days 73000 -sha256
Signature ok
subject=/C=JP/ST=Chiba/L=Funabashi/O=Jugeme/OU=Dev/CN=juge.me
Getting Private key

$

↑この例では juge.me というホスト名で運用する前提での、サーバーの鍵ファイル(server_key.pem)と証明書ファイル(server_crt.pem)を作成しています。

この2つのファイルと同じディレクトリに app.js というファイル名で、Node.js のソースコードを以下の内容で作成しました。アプリケーションそのものはドキュメントルート(/)にアクセスがあった場合に「ハローワールド」と表示するだけの単純なものです:
//. app.js

var express = require( 'express' ),
    http = require( 'http' ),
    https = require( 'https' ),
    cfenv = require( 'cfenv' ),
    fs = require( 'fs' ),
    app = express();
var appEnv = cfenv.getAppEnv();

//. 鍵ファイルと証明書ファイル var options = { key: fs.readFileSync( './server_key.pem' ), cert: fs.readFileSync( './server_crt.pem' ) };
//. 鍵ファイルと証明書ファイルを指定して、https で待受け var server = https.createServer( options, app ).listen( appEnv.port, function(){ console.log( "server stating on " + appEnv.port + " ..." ); });
//. ドキュメントルートにリクエストがあった場合の処理 app.get( '/', function( req, res ){ res.write( 'ハローワールド' ); res.end(); });

このコードを実行すると待受ポート番号が動的に決定して表示されます(↓の例だと 6015 番ポートで https が待ち受けていることが分かります):
$ node app.js
server stating on 6015 ...

ではウェブブラウザで実際にアクセスしてみます。本当に使っている本物のドメイン/ホスト名であればそのままアクセスできると思いますが、試験的に実行している場合はこのホストに "juge.me" という名前でアクセスできる必要があります。必要に応じて hosts ファイルを編集するなどして、目的のホスト(node app.js を実行したホスト)に "juge.me" というホスト名でアクセスできるような準備をしておいてください。

そしてウェブブラウザで "https://juge.me:(ポート番号)" にアクセスします:
2017041301


オレオレ証明書名物「安全な接続ではない」という警告画面になると思います。ここからの手順はウェブブラウザの種類にもよりますが、FireFox の場合であれば「エラー内容」ボタンをクリックしてから「例外を追加」をクリックして、このサイトの警告を無視するための設定を行います。

そして「セキュリティ例外の追加」ダイアログにて、この URL の「セキュリティ例外を承認」します:
2017041302


すると警告が消えて、プログラムで用意したコードが実行され、「ハローワールド」というメッセージが表示されることが確認できます:
2017041303


IBM Bluemix のような PaaS 環境だと、このあたりも含めてアプリケーションサーバー環境が用意されるので楽ですが、素で Node.js 環境を構築する場合はアプリケーション側でも https 対応を実装する必要があり、意外と面倒ね。

このページのトップヘ