nginx サーバの SSL 安全性評価で A+ を獲得
前回の続きです。
前回は、SSL証明書を間違えて設定していた問題を解決した話でした。
今回は、さらにSSLの安全性評価を A+ にすることが目標です。
参考サイト
SSLの安全性評価を A+ にする方法については、すでに沢山の方がブログなどで発信されていますので、まずは、今回参考にしたサイトを以下にご紹介しておきます。
SSLの安全性評価を A+ にするためのバイブルのようなサイトが次のサイトです。
juliansimioni.com上記サイトは、日本語にも翻訳されています。翻訳版は以下より閲覧できます。
NginxでHTTPS : ゼロから始めてSSLの評価をA+にするまで Part 1 | POSTD続いて、次のサイトも参考になります。
NginxでSSLの評価をA+にする手順 - 9mのパソコン日記また、特にSSLプロトコルの話や、Chipers と呼ばれる暗号化アルゴリズム指定方式についてものすごくまとまっているサイトが次のサイトです。
httpsだからというだけで安全?調べたら怖くなってきたSSLの話!? - Qiita
設定前の評価
設定前の安全性評価は「B」でした。
特に「Key Exchange」の数値が低く対策が必要な感じです。
設定内容
冒頭でご紹介したサイトを参考にしながら、自分のサイトを A+ にした設定内容を示します。
1) ssl_protocols の設定
nginx の "ssl protocols" で、"sslv3" など脆弱なプロトコルを無効にします。*1
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
2) ssl_ciphers の設定
以下の値で設定します。こちらは httpsだからというだけで安全?調べたら怖くなってきたSSLの話!? - Qiita を参考にしています。
ssl_ciphers ECDHE+RSAGCM:ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:ECDH+AES128:DH+AES:!EXPORT:!DES:!3DES:!MD5:!DSS; ssl_prefer_server_ciphers on;
3) ssl_dhparam 設定
DH交換鍵の設定。この鍵は、次のOpenSSLコマンドで生成します。生成に結構時間を要します。
$ openssl dhparam 2048 -out dhparam.pem
これを ssl_dhparam に設定します。
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
4) HSTS ヘッダ
このヘッダ情報は、非暗号化の http 通信を抑制するために設定します。このヘッダで 必ず SSL での接続するようにクライアントに指示します。
add_header Strict-Transport-Security 'max-age=31536000; includeSubDomains;';
設定後の評価
ここまで設定した結果、安全性評価は無事に「A+」となりました。
なんだかすっきりしました。
SSLって以外に奥が深いですね。一度基礎からきちんと勉強しておく価値がありそうです。
nginx の SSL設定をしくって Facebook アプリ登録でエラーとなった件
現在開発中のアプリを 「Facebook for Developers」でアプリ登録をした際に、SSL (本当はTLSですが、便宜上SSLと表記します)に関するエラーで少々ハマってしまったので、その内容を記録しておこうと思います。なお、SSL をご存知の方にとっては、至極当たり前の結末なので、あくまでも初心者向けということでご容赦ください。
エラー内容
現在開発中のアプリでは、Facebook の「ログイン」サービスを利用しますが、その際に「Facebook for Developers」から自分のアプリを登録する必要があります。
この「Facebook for Developers」では、アプリの様々な設定を行いますが、ログインサービスの利用を一般にオープンにするためには「プライバシーポリシーのURL」というフィールドを設定しなければなりません。早速プライバシーポリシーのページを用意し、このフィールドにURLを登録しようとしたところ、次のエラーが出てしまいました。
Facebook プラットフォームに準拠するものにするには、有効なプライバシーポリソーURLを入力してください。リクエストエラー: SSL Error: Can't validate SSL Certificate. Either it is self-signed (which will cause browser warnings) or it is invalis.
エラーメッセージの特に英語の部分をみると、どうも「SSL証明書がオレオレじゃないの?」とか「じゃなかったら無効な証明書だよ」と言っています。何か SSL の設定が悪いようです。
原因調査
このアプリはウェブアプリで、Webサーバは nginx を使用しています。
SSL証明書は「Let's Encript」で発行したもので、決して「オレオレ証明書」ではありません。また、Chrome や Firefox などのブラウザで動かしても、これまでSSLに関連した警告は特に表示されませんでした。*1
SSL サイト安全性チェック
まずは、SSLに着目して調査したところ、SSLのサイトにも安全性が高いもの、低いものがあることがわかりました。そして、それをチェックしてくれる 「Qualys SSL LABS SSL Server Test」というサービスがあるとのこと、ありがたや。
早速チェックしてみたら「B」評価でした。
よく見ると「Key Exchange」の部分が黄色表示で問題があります。あと、もうひとつ "This server's certificate chain is incomplete. Grade capped to B." というメッセージがあります。
証明書チェインが不完全?
で、この "This server's certificate chain is incomplete. Grade capped to B." メッセージについて調べると、 This server's certificate chain is incomplete. ... | Qualys Community というコミュニティサイトがヒット、その中に次の投稿がありました。
When user connects to web server, web server sends certificate chain: leaf server certificate and intermediate certificate. What happens leaf certificate is signed by intermediate, intermediate is signed by already in browser stored root certificate, so certification validation is complete and because certificate chain is trusted your leaf server certificate is trusted. When modern browsers receive intermediate certificate they store it in browser cache. Now lets say your server only sends leaf certificate and does not send intermediate certificate (most probably in your case). What will happen? It depends if user has already visited some other web site and retrieved intermediate from that site and store it in browser cache. If it did then browser will use that intermediate certificate from browser cache to validate the chain of trust. But if end user did not visit any other web site using the same intermediate certificate then user will get fatal error in browser like ssllabs displays "incomplete chain".
要するに、SSL 証明書には「leaf server certificate(サーバ証明書)」と「intermediate certificate(中間証明書)」があって、もしブラウザなどクライアントで、中間証明書がキャッシュ含めて入手できないとルート証明書までのチェインが手繰れなくてエラーになります的なことが書いてあります。
ということで、”This server's certificate chain is incomplete. Grade capped to B." というメッセージは、「証明書チェインが不完全ですよ」ということで、もっと意訳すると「中間証明書が無くないすか?」ということではないかと想定されます。
nginx SSL証明書の設定誤りを発見!
これまでの調査を元に、自サイトの nginx の SSL 設定部分を再確認しましたところ、むちゃやばい間違いを発見しました。
ssl_certificate /etc/letsencrypt/live/[my-site-url]/cert.pem; ssl_certificate_key /etc/letsencrypt/[my-site-url]/privkey.pem;
Let's Encript で SSL 証明書を発行すると、/etc/letsencrypt/live 以下に 4つのファイルができます。
- cert.pem
- chain.pem
- fullchain.pem
- privkey.pem
"cert.pem" はサーバ証明書が入ったもの、"chain.pem" は中間証明書、"fullchain.pem" はサーバ証明書と中間証明書が両方入ったもの、"privkey.pem" は証明書と対の秘密鍵です。
nginx の場合、"ssl_certificate" にサーバ証明書、"ssl_certificate_key" に秘密鍵を指定するのですが、中間証明書を設定できるフィールドが用意されていないので、このような場合は "ssl_certificate" にサーバ証明書と中間証明書が両方入った "fullchain.pem" を設定しなければなりません。*2
一方、私のサイトでは、サーバ証明書だけが入った "cert.pem" が設定されており、これでは中間証明書が入手できずにエラーとなってしまうことが判明しました。
対策する
ここまでくると、対策は簡単です。"ssl_certificate" を "fullchain.pem" に変更して、Webサーバを再起動します。
ssl_certificate /etc/letsencrypt/live/[my-site-url]/fullchain.pem; ssl_certificate_key /etc/letsencrypt/[my-site-url]/privkey.pem;
これで、もういちど「Facebook for Developers」の「プライバシーポリシーのURL」を設定してみます.... と、無事に設定することができました!!
Facebook で発生したエラーは、中間証明書が入手できなかったことが原因であるということで決まりです。
なお、この状態で「Qualys SSL LABS SSL Server Test」で SSLの安全性評価を再実行しました。
相変わらず「B」評価ですが、”This server's certificate chain is incomplete. Grade capped to B." のメッセージは消えていることが確認できます。
ちなみに「Qualys SSL LABS SSL Server Test」の最高評価は「A+」らしいです。このままだと「Key Exchange」の部分とか問題が残ってますし、せっかくなので、最高評価の A+ を勝ち取るまで nginx と SSL をセットアップしてみたいと思います。
2018年3月7日追記。
最高評価の A+ を勝ち取るまで nginx と SSL をセットアップした内容をブログに書きましたのでリンクしておきます。
まとめ
- SSL にも安全性の問題があるので、単にサイトを SSL 化にしただけで安心してはいけない。
- SSL のサイトを構築した場合は、「Qualys SSL LABS SSL Server Test」などで安全性をチェックしよう。
- nginx にSSL証明書を設定する場合は、必ず中間証明書とサーバ証明書の両方入っているものを使用すること!間違えたらやばいところ。
以上です。
Vue.js 開発環境を手作りしてみた手順(自分メモ)
Vue.js の開発環境構築は、もっぱら vue-cli で "vue init webpack hogehoge" という方法が一般的ですが、最近 React から Vue に乗り換えた私は、「とりあえず最初は、開発環境を手作りしたい」とうか、そうしないとしっくりこないので、手探りながら一から構築してみた時のメモです 。あくまでも自分用です。
概要
おおよその構成
Vue.js を利用したクライアントアプリのビルドに "webpack"、ES2015 のトランスパイラは "babel"、Vue コンポーネントファイルのビルドに "vue-loader" を導入する。その他 CSS のビルドや、静的なファイルコピー、アプリ実行用サーバも仕込む。あと、Eslint も設定してみた。
大まかな流れ
- npm init で土台作り
- Vue.js インストール
- webpack 入れる
- babel 導入
- css-loader と style-loader 導入
- copy-webpack-plugin 導入
- webpack-dev-server で動かす
- vue-loader 導入
- eslint でコードチェック
Vue.js アプリ開発環境構築 基礎編
基礎編は、vueコンポーネントファイルを用いない Vue.js アプリがビルドできる環境を構築する。
1) npm init で土台作り
最初に node をインストール。nodebrew を使用すると吉。
続いて、npm init で開発環境のベースを作る。
$ npm init
いろいろ対話するが、アプリ名とか適宜入力すればOK。
2) Vue.js インストール
作成したアプリのディレクトリに移動した上で、普通に Vue.js をインストール。
$ npm install --save vue
3) webpack を入れる
$ npm install --save-dev webpack
4) Babel を導入
# webpack から babel を利用する場合の基本モジュール $ npm install --save-dev babel-loader babel-core # ES2015 をトランスパイルするためのもの $ npm install --save-dev babel-preset-es2015
babel の設定ファイル ".babel.rc" を作成
$ echo '{ "presets": ["react", "es2015"] }' > .babelrc $ cat .babelrc { "presets": ["react", "es2015"] }
ここで、babel で js をトランスパイルするための webpack 設定をおこなう。
$ vim webpack.config.js
ここまでの webpack.config.js の中身は次のとおり。
var path = require('path'); module.exports = { entry: './src/app.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'public') }, module: { loaders: [ { test: /\.js$/, exclude: /node_modules/, loader: 'babel-loader' } ] } };
ここで試しにサンプルアプリをビルドさせてみると、Vue.js のTemplateがコンパイルできないとかの以下のエラーがでた。
Vue warn: You are using the runtime-only build of Vue where the template option is not available. Either pre-compile the templates into render functions, or use the compiler-included build.
調査したところ、vue.js のビルドは、コンパイラ付のものを import する必要があるとのこと。解決するには、webpack.config.js に ビルドの alias を追加すること。*1
ということで、webpack.config.js に下記の設定を追加。これで動作OK。
resolve: { alias: { 'vue$': 'vue/dist/vue.js' } },
5) css-loader と style-loader 導入
$ npm install --save css-loader style-loader
webpack.config.js に下記の loader を追加。
module: { loaders: [ ... // ここから { test: /\.css$/, exclude: /node_modules/, loader: ['style-loader', 'css-loader'], }, // ここまで ], },
6) copy-webpack-plugin 導入
$ npm install -save-dev copy-webpack-plugin
webpack.config.js に copy-webpack-plugin の設定を追加。
plugins: [ new CopyWebpackPlugin([{ from: path.resolve(__dirname, 'src', 'index.html'), }]), ],
7) webpack-dev-server で動かす
$ npm install -save-dev webpack-dev-server
またも webpack.config.js に 設定を追加。
devServer: { contentBase: 'public', port: 3000, host: 'localhost', historyApiFallback: true, },
"npm start" でアプリを動かせるように、package.json に次の設定を追加。
"scripts": { "start": "webpack-dev-server", <--- 追加 "build": "webpack", "test": "echo \"Error: no test specified\" && exit 1" },
ここまでのまとめ
ここまでで webpack.config.js は次のとおりとなりました。
const path = require('path'); const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'public') }, module: { loaders: [ { test: /\.js$/, include: /src/, loader: 'babel-loader' }, { test: /\.css$/, exclude: /node_modules/, loader: ['style-loader', 'css-loader'], }, ], }, plugins: [ new CopyWebpackPlugin([{ from: path.resolve(__dirname, 'src', 'index.html'), }]), ], resolve: { alias: { 'vue$': 'vue/dist/vue.js' }, }, devServer: { contentBase: 'public', port: 3000, host: 'localhost', historyApiFallback: true, }, };
Vue.js アプリ開発環境構築 vue ファイルビルド編
ここからは、さらに vueコンポーネントファイルを用いた Vue.js アプリもビルドできるように環境設定を追加する。
8) vue-loader 導入
vueコンポーネントファイル をビルドさせるには、 vue-loader が必要ということで、早速導入。
$ npm install -save-dev vue-loader
さらに vue-template-compiler がないとbuildでエラーとなるのでインストールする
$ npm install --save-dev vue-template-compiler
vue-loader の設定を webpack.config.js に追加。
module: { loaders: [ // ここから追加 { test: /\.vue$/, exclude: /node_modules/, loader: 'vue-loader', options: { loaders: { } } }, // ここまで
9) eslint でコードチェック
Vue.js を利用したアプリでも eslint でリントチェックできるようにしてみる。 最初は、eslint の基本設定。今回は対話で設定するケース。
$ eslint --init ? How would you like to configure ESLint? Answer questions about your style ? Are you using ECMAScript 6 features? Yes ? Are you using ES6 modules? Yes ? Where will your code run? Browser ? Do you use CommonJS? No ? Do you use JSX? No ? What style of indentation do you use? Spaces ? What quotes do you use for strings? Single ? What line endings do you use? Unix ? Do you require semicolons? Yes ? What format do you want your config file to be in? JavaScript Successfully created .eslintrc.js file in /Users/foo/bar/vue-sample/
これで ".eslintrc.js" が生成されるはず。ただしこのままでは、vue template がきちんとチェックできないので、eslint-for-vue を導入する。
$ npm install --save-dev eslint-plugin-vue@beta
.eslintrc.js に設定を追加。今回はこのような設定内容となりました。
module.exports = { "env": { "browser": true, "commonjs": true, "es6": true }, "extends": [ "eslint:recommended", "plugin:vue/recommended" <--- eslint-for-vue の設定追加 ], "parserOptions": { "sourceType": "module" }, "rules": { "indent": [ "error", 2 ], "linebreak-style": [ "error", "unix" ], "quotes": [ "error", "single" ], "semi": [ "error", "always" ], "vue/valid-v-if": "error" <--- eslint-for-vue の設定追加 } };
まとめ
今回作成した webpack.config.js はこちら
const path = require('path'); const CopyWebpackPlugin = require('copy-webpack-plugin'); module.exports = { entry: './src/index.js', output: { filename: 'bundle.js', path: path.resolve(__dirname, 'public') }, module: { loaders: [ { test: /\.vue$/, include: /src/, loader: 'vue-loader', options: { loaders: { } } }, { test: /\.js$/, include: /src/, loader: 'babel-loader' }, { test: /\.css$/, exclude: /node_modules/, loader: ['style-loader', 'css-loader'], }, ], }, plugins: [ new CopyWebpackPlugin([{ from: path.resolve(__dirname, 'src', 'index.html'), }]), ], resolve: { alias: { 'vue$': 'vue/dist/vue.js' }, }, devServer: { contentBase: 'public', port: 3000, host: 'localhost', historyApiFallback: true, }, };
package.json はこうなりました。
{ "name": "vue-tutorial01", "version": "1.0.0", "description": "Vue.js tutorial project", "author": "hoge hoge fuga fuga", "private": true, "main": "index.js", "license": "ISC", "scripts": { "start": "webpack-dev-server", "build": "webpack", "test": "echo \"Error: no test specified\" && exit 1" }, "dependencies": { "eslint": "^4.7.1", "vue": "^2.4.4" }, "devDependencies": { "babel-cli": "^6.26.0", "babel-core": "^6.26.0", "babel-loader": "^7.1.2", "babel-preset-es2015": "^6.24.1", "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.28.7", "eslint-config-standard": "^10.2.1", "eslint-plugin-import": "^2.7.0", "eslint-plugin-node": "^5.1.1", "eslint-plugin-promise": "^3.5.0", "eslint-plugin-standard": "^3.0.1", "eslint-plugin-vue": "^3.13.0", "file-loader": "^0.11.2", "style-loader": "^0.18.2", "vue-loader": "^13.0.5", "vue-template-compiler": "^2.4.4", "webpack": "^3.6.0", "webpack-dev-server": "^2.8.2" } }
所感
なんというか、"vue init webpack-simple" で開発環境を構築した場合とほぼ同じような感じになってしまった。しかし、今回は自分の理解のために頑張ったということで良かろう。
次回からは、素直に "vue init webpack-simple" で環境構築することにしよう。
以上。
*1:本家の次の情報を参考にしました
Docker で node.js を動かすときは PID 1 にしてはいけない
これは、node.js on Docker の構成で 2〜3日ハマってしまった時の話です。忘れないように記録しておきます。なお、将来は改善・改良されているかもしれませんのでご注意ください。
何が起こったのか
node.js の Docker コンテナを、"docker stop" でコンテナを止めようとしても正常に停止せず、10秒くらい経過した後に強制終了してしまうという症状が発生しました。いつも等しくそうなるので、状態とかタイミングとかそういった要因ではなく、そもそも根本的に何かがおかしいと考えられます。
1. node on Docker の構成
Docker コンテナ上で node.js が動いているだけの極めてシンプルな構成でこの問題が発生しました。
node.js で動くアプリは、"Hello World" を出すだけの超簡単な hello.js です。こんな感じです。
const http = require('http'); http.createServer((req, res) => { res.writeHead(200, {'Content-Type': 'text/plain'}); res.end('Hello World\n'); }).listen(3000);
hello.js アプリを Docker に仕込むための Docker ファイルは以下の通りです。ベースとなる Docker イメージは、Docker Hub で node.js が提供する node:8.4 (現在のlatest) を利用しました。
FROM node:latest ADD ./hello.js /usr/src/app/ EXPOSE 3000 CMD ["node", "/usr/src/app/hello.js"]
普通にビルドして、生成したイメージからコンテナを作って起動します。たぶん … 無事に動作するはずです。
$ docker build -t nodetest . $ docker run --name hello_node -P 3000:3000 nodetest
2. 問題を発生させる
動いている node.js アプリのコンテナを docker stop で停止しますと、今回の問題が発生します。
$ docker stop hello_node
恐らく、10秒ほど経過した後にコンテナが停止しプロンプトが戻ってくると思いますが、この停止に10秒かかることが問題です。
3. docker stop で何が起こっているか?
“docker stop” コマンドを実行すると、Docker はそのコンテナのルートプロセス(すなわち、PID=1 のプロセス)に SIGTERM シグナルを投げます。
このシグナルを受信したプロセスは、該当するシグナルハンドリング処理を実行します。多くのプログラムでは、SIGTEM を受けた場合、自身を安全に停止するシグナルハンドリング処理が実装されています。
SIGTERM を投げた後、Docker は コンテナ(ルートプロセス)が終了するまで デフォルト10秒間待ちます。そして、10秒経過しても終了しない場合は、コンテナのルートプロセスに対して SIGKILL を投げます。SIGKILL は強制終了を指示するものなので、これを受信したプロセスは、直ちにABORTします。
この時、プロセスの終了に必要な処理は全てスキップされますので ABORT 後は正常な状態であることが保証されません。したがって、次回起動時にうまく立ち上がらなくなる等、思わぬ障害のリスクがあります。
今回の node.js のコンテナも、10秒後に停止しています。これはすなわち SIGKILL で強制終了していることになり、このままではやばい感じです。
原因調査
A. 物理サーバ環境と Docker 環境での違いを検証
ここで、そもそも Docker に起因する問題なのか? それとも node.js の問題なのか?を切り分けたいと思います。
ということで、仮想化しない物理サーバ環境と Docker 環境の両方で node.js の hello アプリを動かし、そのプロセスに対してそれぞれ SIGTERM を送信してみます。
(1)物理サーバ環境で検証
#### Terminal 1 で node アプリを起動 $ node hello.js #### Terminal 2 で node に SIGTERM を送信 $ ps -ef | grep node ubuntu 8526 4061 3 05:21 pts/0 00:00:00 node hello.js $ kill -TERM 8526 #### 成功、node hello.js は、直ちに問題なく終了した
うまくいきました。
物理サーバ環境において、SIGTERM を受信した node.js プロセスは、直ちに自分自身を終了させています。これにより、node.js は SIGTERM を受信した際のシグナルハンドリングがきちんと実装されていることが分かります。
(2) Docker コンテナ環境で検証
#### nodeアプリが入った Docker コンテナを起動 $ docker run --name hello_node -d nodetest $ docker ps CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES ef2b7ab09468 nodetest "node /usr/src/app..." 50 seconds ago Up 49 seconds 3000/tcp hello_node #### 起動したコンテナにアタッチ $ docker exec -it hello_node /bin/bash #### ---- hello_node コンテナ内 ---- # ps -ef UID PID PPID C STIME TTY TIME CMD root 1 0 1 01:53 pts/0 00:00:00 node /usr/src/app/hello.js root 16 0 0 01:54 pts/1 00:00:00 /bin/bash # kill 1 ### しなない!!
ダメです!!
幾ら SIGTERM を投げても反応しません。Docker コンテナの内の node は、何かの理由で SIGTERM を受信できないか、もしくはシグナルを無視しているように見えます。
B. プロセスID 1 に注目してさらに調査
Linux についてご存知の方は、Dockerコンテナ内の node.js プロセスIDが “1” であることに、限りない怪しさを覚えるかもしれません。
普通の Linux 環境において、プロセスID = 1 は init プロセスです。これは カーネルから起動されますが、(特に Linux においては)プロセスD=1 に対してシグナルを送ることはいろいろ制限されています(see ”man 2 kill" on Linux)。
ということで、今度は、特に、プロセスID 1 に何か特殊な理由が無いか?ということを留意しながら、さらに node.js のソースコードを確認したり、同様の問題がなかったか?という調査を行いました。
・
・・
・・・
見つけました
上記ページの「Handling Kernel Signals」というところに、下記の記載があります。
Node.js was not designed to run as PID 1 which leads to unexpected behaviour when running inside of Docker. For example, a Node.js process running as PID 1 will not respond to SIGTERM (CTRL-C) and similar signals. As of Docker 1.13, you can use the –init flag to wrap your Node.js process with a lightweight init system that properly handles running as PID 1.
docker run -it –init node
You can also include Tini directly in your Dockerfile, ensuring your process is always started with an init wrapper.
まじか。。そもそもnode.js は PID 1 で動くように設計されていないとか、なので Docker コンテナで動かすときは、SIGTERM とか Ctrl+C とか効かないとか、まさに、今回の問題について言及されています。
もう一つ、"lightweight init system" というツールがあって、Docker で動かすときは、それを PID 1 として動作させ、node はその子プロセス にすればいいじゃん的なことも書いてあります。
というか、"Tini" という便利なものがあったのか!! しかも、Docker 1.13 からは “–init” オプションでこれが自動的にラップするように組み込まれているということです。知らなかった。
GitHub - krallin/tini: A tiny but valid `init` for containers
対処内容
問題の原因が分かったので、もう簡単に対策ができます。ということで、早速 Docker 本体にも組み込まれている “Tini” を活用します。
結局、今回は、以下のように Dockerfile に Tiny の設定を追加するだけで、万事 OK となりました。
FROM node:latest # Add Tini ENV TINI_VERSION v0.15.0 ADD https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini /tini RUN chmod +x /tini ENTRYPOINT ["/tini", "--"] # MyApp ADD ./test3.js /usr/src/app/ EXPOSE 3000 CMD ["node", "/usr/src/app/hello.js"]
まとめ
node は PID1 で動作するようにデザインされていないので、 docker にいれるときは、直接 Docker の管理プロセス(つまりPID1)にするのはやめよう!
Docker で PID 1 で Linux の init みたいに制御してくれる Tiny という便利なツールがあるので、それを活用するといいかも
そもそも、node やその他 サーバプログラムをDocker が直接起動するプロセス(つまり PID 1 となるプロセス)とするのは、いろいろ問題があるなぁと思います。
以前、Unicorn が SIGTERM ではなく SIGQUIT を期待しており、Docker stop で異常終了してしまう問題にも遭遇しています。
なので、個別に作成したスーパバイザ Shell でラップするとか、今回ご紹介した Tiny をかませるとか、その方が安全ではないか?と、最近はそう思っています。
なお、PID 1 とする Tiny のような lightweight init プログラムは、例えば、自分自身が SIGTERM を受信したときに、そのシグナルを、子プロセスまできちんと伝播させたり、子プロセスがゾンビにならないよう wait したり、そういった init の作法に則ったものでなければなりません。その辺は十分ご注意ください。
おまけ
Docker で使える lightweight init たち
ここでは、Docker コンテナで PID 1 のプロセスとして使える Tiny をご紹介しました。その後調べたところ、この他にも、幾つか似たようなものがありましたので、まとめとして、以下にリンクしておきます。
Tiny
本記事でご紹介した lightweight init 、前途した通りDocker にも組み込まれており “docker run” の “–init” オプションで自動的にラップできます。
dumb-init
“Yelp” で使用している lightweight init
my_init
“baseimage-docker” という本来の Linux に近いプロセス環境を Docker で提供している。そのイメージで使用している my_init、こちらはマルチプロセスを管理できるようです。
inits_on_docker/init_node.sh
ちなみに、私も Shell で簡易に作成してみました。node.js 専用ですが、せっかくだし、ご紹介させてください。(つっこみ歓迎)
inits_on_docker/init_node.sh at master · ngzm/inits_on_docker · GitHub
#!/bin/bash # # init_node.sh # - init process for aplications using node.js. # # Usage # - add your Dockerfile as follows. # ------------ # ADD ./init_node.sh /usr/local/bin # RUN chmod +x /usr/local/bin/init_node.sh # CMD ["init_node.sh", [path_to_your_app]] # ------------ # echo "start init_node.sh" # set application path path_to_your_app=${1:-''} if [ -z ${path_to_your_app} ]; then echo "require first argment for path_to_your_app" echo "quit this container" exit 1 fi if [ ! -e ${path_to_your_app} ]; then echo "${path_to_your_app} is not exists" echo "quit this container" exit 2 fi # Application PID initialize your_app_pid=0 # SIGINT handler int_handler() { echo "int_handler called" if [ ${your_app_pid} -ne 0 ]; then kill -INT ${your_app_pid} fi } # SIGTERM handler term_handler() { echo "term_handler called" if [ ${your_app_pid} -ne 0 ]; then kill -TERM ${your_app_pid} fi } # trap SIGINT - usually caught by Ctrl+c trap 'int_handler' INT # trap SIGTERM - sent when 'docker stop' trap 'term_handler' TERM # run application echo "run ${path_to_your_app}" node ${path_to_your_app} & your_app_pid="${!}" # wait untill the application (child process) will be killed wait ${your_app_pid} your_app_pid=0 echo "finish init_node.sh"
補足とか
何もこんなに苦労しなくても最初から “npm run” で起動するようにしておけば問題なく起動・終了ができそうです!という情報もあって、一応動作させてみたら、よさげに見えました。確かにこれだと、node の PID は 1 ではなくなるのでいい感じです。
ただし、本当にこれで、子プロセス、孫プロセスまで管理できているのか?すなわち、ゾンビにならずに、また強制終了せずに健全に起動、終了しているのか?といったところまでは裏付けができていません。なので継続して調べていきたいと思います。
誰か知っている人が居たらぜひ教えてください。
最後に苦情です
docker-node/README のページ、本家のトップページで思いっきり PID 1 で起動するインストラクションしてるじゃん
docker-node/README.md at master · nodejs/docker-node · GitHub
あと、こんなシビアな問題があるなら、もっと目立つところで情報公開してください
ubuntu ファイアーウォール :Docker が UFW 管理外のポートを開けてしまう問題
UFW とは ファイアーウォールを設定・管理するツールで、Ubuntuでは標準的に利用します。このツールを使用すれば、iptables を使うよりはるかに簡単にファイアーウォールを構築できます。
UFW - Community Help Wiki
Docker はご存知ですね。
Docker - Build, Ship, and Run Any App, Anywhere
遭遇した問題
今回 UFW と docker の両方を使用するサーバで、UFW で許可していないポート番号がいつのまにか空いてしまった問題に遭遇しました。 次のような感じです。
- ubuntu xenial サーバに対し、UFW で 22番と443番だけアクセス可能、その他は遮断というファイアーウォールをセットアップ
- 同サーバに Docker 入れて、nginx コンテナ起動、この時 “-p 80:80” オプションを付与
- 外部からこのサーバにアクセスしたら、UFW で開けていないはずの 80番ポートのページが見れてしまった
- UFW の設定を再確認してみたが、やっぱし 80番ポートは Allow してない
なんだかやばい感じです。
ちな、UFW の設定内容は以下の通りです。
$ sudo ufw status verbose Status: active Logging: on (low) Default: deny (incoming), allow (outgoing), deny (routed) New profiles: skip To Action From -- ------ ---- 443 ALLOW IN Anywhere 22 LIMIT IN Anywhere 443 (v6) ALLOW IN Anywhere (v6) 22 (v6) LIMIT IN Anywhere (v6)
問題の原因
Docker の問題
調査したところ、ひとつの原因は … Docker でした。docker run の -p や -P オプションでコンテナを起動すると、Docker は、その指定されたポート番号について、ファイアーウォールに穴を開ける処理をします。
UFWの問題
では、どうして UFW で確認できなかったのでしょうか? それは … 直接 iptables のルール一覧を確認すれば分かるかもしれません。
$ sudo iptables -L Chain INPUT (policy DROP) target prot opt source destination ufw-before-logging-input all -- anywhere anywhere ufw-before-input all -- anywhere anywhere ufw-after-input all -- anywhere anywhere ufw-after-logging-input all -- anywhere anywhere ufw-reject-input all -- anywhere anywhere ufw-track-input all -- anywhere anywhere Chain FORWARD (policy DROP) target prot opt source destination DOCKER-USER all -- anywhere anywhere DOCKER-ISOLATION all -- anywhere anywhere ... 省略 ... Chain DOCKER (2 references) target prot opt source destination ACCEPT tcp -- anywhere 172.18.0.4 tcp dpt:https ACCEPT tcp -- anywhere 172.18.0.4 tcp dpt:http Chain DOCKER-ISOLATION (1 references) target prot opt source destination DROP all -- anywhere anywhere DROP all -- anywhere anywhere RETURN all -- anywhere anywhere Chain DOCKER-USER (1 references) target prot opt source destination RETURN all -- anywhere anywhere Chain ufw-after-forward (1 references) target prot opt source destination ... 省略 ... Chain ufw-user-input (1 references) target prot opt source destination ACCEPT tcp -- anywhere anywhere tcp dpt:https ACCEPT udp -- anywhere anywhere udp dpt:https ... 省略 ...
UFW が設定したルールには、"ufw-[hogehoge]“ という名前が付いています。一方、"DOCKER” で始まるチェーンも見ることができますが、こちらは明らかに Docker によって追加されたルールですね。
どうやら UFW は 自分が設定したルール、すなわち “ufw-[hogehoge]” についてのみ管理対象としているようです。従って、Docker など他のプログラムや、手作業で追加したルールについては関与していません。これこそがもう一つの原因であり、運用する上で大変注意が必要である事項であります。
ちなみに、Docker が勝手にポートを開けるのを抑える方法
ubuntu の場合だと、”/etc/default/docker” の ”DOCKER_OPTS" オプションに、"–iptables=false" を追加すればいいようです。
DOCKER_OPTS="--dns 8.8.8.8 --dns 8.8.4.4 --iptables=false"
なお、オプションを有効にするため Docker の再起動を忘れないようにしてください。
まとめ
-p や -P オプション(also EXPOSE)をつけて “docker run” すると Docker が 該当するポートを許可する設定を iptables に追加する。
UFW は自分で設定したルール以外は関与しない。このため、UFW で確認できる設定内容がファイアーウォールの全てではないことに注意すること。
ファイアーウォールについて、最終的には iptables コマンドでも確認した方が良いと思われます。