はじめに
AWS lambdaのNode.js(https標準モジュール)で実装した、WEBサイトへhttpsのリクエストを投げる処理で、以下の2つのエラーが発生した際の対応についての記事です。
※急いでいる方、ソースコードだけ見たい方はここから見ればOKです
これ何
調べたところ、リクエスト先のWEBサーバから送出されているサーバ証明書に対してのnodejs内部での検証失敗のため生じたエラーでした。
2つのエラーの違いは、
- 自分自身が署名(オレオレ証明書(self-signed))
- サーバ証明書の発行元が信頼されていないか
👉2つのエラーは共に、Node.jsとして信頼1していないCA証明書にチェーンしていたため生じていました。
環境
- AWS lambda
- Node.js ランタイム12.x
- 接続イメージ
[AWS lambda]-->(WAN HTTP over SSL)-->[WEBサーバ]
対処案
3つ考えましたが、妥協点で私の環境では3つ目で実装しましたので以下にまとめます。
1.WEBサーバ側にちゃんとしたサーバ証明書2に変更してもらう
2.TLSハンドシェイクエラーを無視・無効にする
NODE_TLS_REJECT_UNAUTHORIZED
を環境変数に定義し、値を0
とすれば検証自体がdisableになる模様。これは、curlでいうところの--insecure
オプションと類似していますが、検証を全て無視するため有効期限やトラストアンカーとのチェーン等々を丸っとすっ飛ばす模様。(詳細は未検証のため割愛)
https://nodejs.org/api/cli.html#cli_node_tls_reject_unauthorized_value
3.オレオレ証明書(self-signed)を一時的に信頼する
公式ドキュメントに書いてありました。
https://nodejs.org/api/tls.html#tls_tls_connect_options_callback
具体的には、通信先のWEBサーバのオレオレ証明書(サーバ証明書自体)ないしは、サーバ証明書のissuerとなっている現状信頼されていない自局CA証明書(pem形式)を取得し、https.requestのoptionにca
を追加するというものです。以下にサンプルを用いて実装例を示しています。
※今回の実装ではpemファイルの読み込みを、諸般の事情によりlambdaのみで完結したかったため、環境変数に事前に設定して、そこから読み込ませるという方法を使っています。(S3に入れて読み込むという方法もありかと思います。)
3-1.pemファイルを1行化する
環境変数にpemを入れるため、改行を¥nに置換します。
[work@localhost]$ awk 'NF {sub(/\r/, ""); printf "%s\\n",$0;}' cacert.pem
-----BEGIN CERTIFICATE-----\nAIIDQjCCAiHCEQCQzvg6BX4eF(中略)v2wk3xtME7i8Jb2aUQH9vFVYuXUN2\nUO+j8l1OA4p1ew0kCsit2HOn\n-----END CERTIFICATE-----\n [work@localhost]$
余談ですが、当初は、pem形式って改行まで含んで形式であるということを理解しておらず、改行を単純に削除して環境変数に入れていたためエラーが発生して、ここで地味に悩みました。ちなみに、pemの改行は、Windows形式(CR+LF)、Unix形式(LF)どちらで良い模様なのでCRを削除後に置換している。4
3-2.AWS lamda環境変数の設定
上コマンドで表示された内容を環境変数にCA_PEMとして保存します。
3-3. 実際のスクリプト(sample)
const https = require('https');
//ここで環境変数からcacertにPEMを読み込んでいる
var cacert = process.env['CA_PEM'].replace(/\\n/g, '\n');
exports.handler = (event, context) => {
//caを追加してcacertを設定
const options = {
protocol: 'https:',
host: 'example.com',
path: '/index.html',
port: 443,
method: 'GET',
timeout: 8000,
ca: cacert,
headers: {
'User-Agent': 'AWS-lambda',
}
};
let req = https.request(options, (res) => {
res.setEncoding('utf8');
let body = '';
res.on('data', (chunk) => {
body += chunk;
});
res.on('end', () => {
console.log(body);
});
});
req.end();
}
3-4. エラー解消。取れました
Response:
null
Request ID:
"XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX"
Function Logs:
START RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX Version: $LATEST
2020-04-16T14:26:29.930Z XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX INFO
<!DOCTYPE html>
<html lang="jp">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello</title>
</head>
<body>
This is test page ;>
</body>
</html>
END RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
REPORT RequestId: XXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX
Duration: 210.24 ms Billed Duration: 300 ms Memory Size: 128 MB Max Memory Used: 68 MB
-
Node.jsのSSL通信時のトラストアンカーってなんだろ?と思い調べた。https://github.com/nodejs/node/blob/bf7409e9740ce602b09e088aac70b7c817f5d27c/doc/guides/maintaining-root-certs.md 読んでみると、このソースに書いてあるよとのこと。Mozilla NSSのトラストアンカーと合わせているみたいですね、定期的にメンテはされている模様。ここに自己署名をここに入れるのは影響が大きそうです。 ↩ ↩2
-
Node.jsのトラストアンカー1に含まれるCAに直接または間接的に署名されているサーバ証明書のこと。
この対応ができたらこんな記事はいらない気もする。
自局署名の問題については、ネット上でも従前から議論されおり3、本記事をご覧になっている方でも既知のことと思います。WEBサーバ管理者に対しては、セキュリティの観点で懸念点を伝えてさしあげる程度にしました。 ↩ -
Qiitaですと、こちらの記事がとても参考になりました。オレオレ証明書を使いたがる人を例を用いて説得する ↩
-
出典:https://stackoverflow.com/questions/57870914/how-to-create-a-single-line-x509-certificate-that-can-be-parsed-by-openssl-comma ↩