tech memo

調べたこと、試したことの覚書です

nginx 1.15.10 + njs で ssl_certificate, ssl_certificate_key に変数内のデータをロードする

nginx ngx_http_ssl_module 関連記事

 

この記事の概要

nginx 1.15.10Changes に 下記の Feature が 書かれていました 。

*) Feature: loading of SSL certificates and secret keys from variables.

http://nginx.org/en/CHANGES より抜粋 ) 

ssl_certificate, ssl_certificate_key に変数内のデータが利用できようになりました。この件について、動作検証した結果、njs との組み合わせで動作可能なことを確認したので、その内容について、書き留めておきます。

nginx.org のドキュメント に 下記の記述が追記されていました。

The value data:$variable can be specified instead of the file (1.15.10), 
which loads a certificate from a variable without using intermediate files.
Note that inappropriate use of this syntax may have its security implications,
such as writing secret key data to error log.

It should be kept in mind that due to the HTTPS protocol limitations for
maximum interoperability virtual servers should listen on different IP addresses.

http://nginx.org/en/docs/http/ngx_http_ssl_module.html#ssl_certificate より抜粋 ) 

これまでの nginx バージョンの ssl_certificate, ssl_certificate_key においては、ファイル名 を指定して ファイル経由でデータをロードしていましたが、1.15.10 からは data:$変数名 という記述を使って変数内のデータをロードできるようになりました。

ssl_certificate  data:$変数名 ;
ssl_certificate_key data:$変数名 ;

 

変数の定義方法について、njs で試した理由

この件の ユースケース について、 nginx forum 内で Maxim Dounin さんが下記の内容を発言されていました。

This is intended to be used with some external means of providing 
certificates and keys, such as perl or njs code, or a keyval
database (http://nginx.org/r/keyval).

https://forum.nginx.org/read.php?2,283522,283536#msg-283536 より抜粋 )

  • ngx_http_perl_module
  • ngx_http_js_module
  • ngx_http_keyval_module (このmoduleに関してはcommercial subscriptionです)

などと組み合わせて使うことが意図されているとのことです。

ここに挙げられている 3つ以外でも nginx変数に値がセットできるなら動作するのでは?と思ったので、set (http://nginx.org/en/docs/http/ngx_http_rewrite_module.html#set) を試してみたのですが、

set $(変数名) "-----BEGIN CERTIFICATE-----\n (証明書の中身をPEM形式で記述, 改行は\nとして記述) \n-----END CERTIFICATE-----\n";

SSLハンドシェイク時点ではnginx変数内に値が設定できていないという旨の下記のようなエラーが出力されました。この挙動からは、この記述パターンにおける set は処理順序がSSLハンドシェイクより後だと思われます。(個人的な推定です)

2019/04/02 20:32:00 [warn] 20231#20231: *14 using uninitialized "(変数名)" variable while SSL handshaking, client: (接続元アドレス), server: 0.0.0.0:443

2019/04/02 20:32:00 [error] 20231#20231: *14 cannot load certificate "data:": PEM_read_bio_PrivateKey() failed (SSL: error:0906D06C:PEM routines:PEM_read_bio:no start line:Expecting: ANY PRIVATE KEY) while SSL handshaking, client: (接続元アドレス), server: 0.0.0.0:443

※ "data:" なので nil値になっているようです。

また、lua_nginx_modulengx.var.VARIABLE を使ってnginx変数の設定するということも試みてみましたが、こちらも同様に、SSLハンドシェイクより前にnginx変数に値を設定することができませんでした。

こちらについては下記のドキュメントベースの情報としても、ngx.var.VARIABLE が利用可能な directive としては、SSLハンドシェイク前にフック可能なタイミングに利用できるなものが(現時点では)存在しないことを確認しました。

 という経緯から、ユースケースとして挙げられていたものの1つである  njs (ngx_http_js_module) を使って、今回の検証を実施しました。

 

検証の準備

① ngx_http_js_module の取得&配置

ngx_http_js_module は デフォルトではビルドされていません。公式の手順などを参考に取得&配置しました。

 

② moduleのロードの記述

ngx_http_js_module は、動的モジュールのため下記のような記述でロードします。

# nginx.conf上部に記述
# 配置場所が"modules/ngx_http_js_module.so"の場合

load_module modules/ngx_http_js_module.so;

 

③ httpディレクティブの記述

# 呼び出すjsファイルを指定します
# js_include ファイル名
js_include http.js ;

# 変数名に、呼び出す関数を紐づけます
# js_set $変数名 関数名
js_set $cert cert;
js_set $pkey pkey;

 

④ httpディレクティブ または serverディレクティブ の記述

# data:$変数名 で呼び出します
ssl_certificate data:${cert} ;
ssl_certificate_key data:${pkey} ;

 

⑤ jsファイルの用意

// 呼び出す関数を記述しておきます

function cert() {
var c ;
c = "-----BEGIN CERTIFICATE-----\n (証明書の中身をPEM形式で記述, 改行は\nとして記述) \n-----END CERTIFICATE-----\n";
return c ;
}

function pkey() {
var p ;
p = "-----BEGIN PRIVATE KEY----\n (秘密鍵の中身をPEM形式で記述, 改行は\nとして記述) \n-----END PRIVATE KEY----\n" ;
return p ;
}

 

動作検証の内容と結果

今回も、workerを1つにし、そのworkerに対して straceで挙動を確認しつつ、ログを確認しつつ、接続確認を行いました。

① nginx reload 時 に jsファイルがロードされることを確認

# strace nginx -s reload
execve("/sbin/nginx", ["nginx", "-s", "reload"], [/* 18 vars */]) = 0
brk(NULL) = 0x55db33d1d000
mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7fe50d6e5000


<!ここでjsファイルがOPEN!>
open("/etc/nginx/http.js", O_RDONLY) = 6
fstat(6, {st_mode=S_IFREG|0644, st_size=1994, ...}) = 0
read(6, "function cert() {\n var s;\n "..., 1994) = 1994

 

② 初回アクセスする ( 初回ハンドシェイク )

■ curlで アクセスする

$ curl -k --no-keepalive https://(テストドメイン名)
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>


   

■ straceの抜粋

[{EPOLLIN, {u32=2132214024, u64=140520577237256}}], 512, -1) = 1
accept4(7, {sa_family=AF_INET, sin_port=htons(25509), sin_addr=inet_addr("61.211.224.11")}, [16], SOCK_NONBLOCK) = 3
epoll_ctl(11, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=2132214520, u64=140520577237752}}) = 0
epoll_wait(11, [{EPOLLIN, {u32=2132214520, u64=140520577237752}}], 512, 60000) = 1
recvfrom(3, "\26", 1, MSG_PEEK, NULL, NULL) = 1
setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
read(3, "\26\3\1\0\323\1\0\0\317\3\3", 11) = 11
read(3, "\211\334\1\202\335\0230}\f\224'\1\245! \221>|\341\231y\236\1\320\204|6|\232ff2"..., 205) = 205



stat("/usr/share/nginx/html/index.html", {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
open("/usr/share/nginx/html/index.html", O_RDONLY|O_NONBLOCK) = 9
fstat(9, {st_mode=S_IFREG|0644, st_size=612, ...}) = 0
pread64(9, "<!DOCTYPE html>\n<html>\n<head>\n<t"..., 612, 0) = 612
write(3, "\27\3\3\3k\340\275u+K\376\211\245\23Xg=U!\357v\344\252\242\362\325\220K\330\364k\257"..., 880) = 880
write(5, "61.211.224.11 - - [02/Apr/2019:1"..., 94) = 94
close(9) = 0
epoll_wait(11, [{EPOLLIN, {u32=2132214520, u64=140520577237752}}], 512, 65000) = 1
read(3, "\25\3\3\0\32\3709\262\310\364\350,\251/_A\273\224\36\266\250C\243\306\320u\245W\223\210A", 33093) = 31
close(3) = 0


※ SSLハンドシェイク時に jsファイルは OPENされない ことを確認

 

③ jsファイルを書き換え (reloadしない)、再度アクセスする

故意に不正な証明書/鍵のデータに書き換えます。

function cert() {
var c ;
// 文字列を欠けさせます
c = "EGIN CERTIFICATE-----\n (証明書の中身をPEM形式で記述, 改行は\nとして記述) \n-----END CERTIFICATE-----\n";
return c ;
}

function pkey() {
var p ;
// 文字列を欠けさせます
p = "EGIN PRIVATE KEY----\n (秘密鍵の中身をPEM形式で記述, 改行は\nとして記述) \n-----END PRIVATE KEY----\n" ;
return p ;
}

  

■ curlで アクセスする

$ curl -k --no-keepalive https://(テストドメイン名)
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>

挙動:https://(テストドメイン名) が適切に表示される

straceのログを見ていると②とフローが一致している。よって、jsファイルはreload時にのみ読み込まれるという挙動が推定できる。

 

④ reload の実行し、再度アクセスする

# nginx -s reload
$ curl -k --no-keepalive https://(テストドメイン名)
curl: (35) Peer reports it experienced an internal error.

挙動:証明書/鍵が不正だという旨のエラーになる

2019/04/02 21:43:31 [error] 25266#25266: *29 cannot load certificate "data:EGIN CERTIFICATE-----
(証明書の中身)) while SSL handshaking, client: (接続元アドレス), server: 0.0.0.0:443

jsファイルはreload時にのみ読み込まれるという挙動であると推定できる。

また、冒頭に紹介した nginx.org のドキュメントにも記載されていますが、error_logに証明書や鍵の中身が出力されるので扱いに注意が必要そうです。

 

nginx 1.15.10 + njs を使って、ssl_certificate, ssl_certificate_key に変数内のデータをロードする検証をした紹介でした。

# SSLハンドシェイクより前に nginx変数に設定できるのであれば njs 以外の他の方法でも試せそうです。