nginx 1.15.9 の ssl_certificate, ssl_certificate_key の挙動確認

nginx ngx_http_ssl_module 関連記事

 

この記事の概要

nginx 1.15.9 で ssl_certificate, ssl_certificate_key に変数が使えるようになりました。

この変更により証明書・秘密鍵の動的な読み込みが可能になったのでは?と話題になっていました。

この件について、ちょうど挙動を確認したので、書き留めておきます。
深追いはできてないので、参考までに。

 

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

nginx.org

そのなかで下記の忠告が書かれていました。

Note that using variables implies that a certificate will be loaded for each SSL handshake, and this may have a negative impact on performance.
It should be kept in mind that due to the HTTPS protocol limitations for maximum interoperability virtual servers should listen on different IP addresses.

変数を使用すると、SSLハンドシェイク毎に 都度証明書(と鍵)がロードされ、パフォーマンスにはネガティブな影響を与えるようです。

パフォーマンスについては未だ計測できてないのですが、この都度ロードされるという挙動について確認してみました。

 

検証の準備

■serverディレクティブの記述

server {
listen 443 ssl;
server_name _ default;

access_log /var/log/nginx/default_access.log ltsv;
error_log /var/log/nginx/default_error.log notice;

set $ssl_server_name $http_host;

ssl_certificate /etc/nginx/cert/$ssl_server_name.crt;
ssl_certificate_key /etc/nginx/cert/$ssl_server_name.key;

~略~
}

・[set $(任意の変数名) $(http_hostなどの識別名としてつかう組込変数)]を定義
・定義した任意の変数名を ssl_certificate や ssl_certificate_key にパスの一部として渡します

※ssl_certificate や ssl_certificate_keyに直接$(http_hostなどの識別名としてつかう組込変数)を書くのはNGでした。(file open対象がnil値になります)

 

■Let's Encryptで証明書&鍵を準備(2セット)

# ls -l /etc/nginx/cert/
total 20
drwxr-xr-x 2 root root 4096 Mar 4 12:18 old
-rw-r--r-- 1 root root 1907 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.crt
-rw-r----- 1 root root 1704 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.key
-rw-r--r-- 1 root root 1907 Mar 4 12:51 /etc/nginx/cert/yyy.1773.work.crt
-rw-r----- 1 root root 1704 Mar 4 12:51 /etc/nginx/cert/yyy.1773.work.key

片系を退避 (動作確認を分かりやすくするため)

# mv yyy.* old/

# ls -l /etc/nginx/cert/
total 12
drwxr-xr-x 2 root root 4096 Mar 4 12:18 old
-rw-r--r-- 1 root root 1907 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.crt
-rw-r----- 1 root root 1704 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.key

 

検証の内容と結果

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

この間、 nginx の reload, restart は行わず、動的更新に対しての動作を確認しています。

 

① https://xxx.1773.work/ への初回アクセス

挙動:https://xxx.1773.work/が表示され 適切な証明書が表示される

■straceの抜粋

# strace -tt -s 1024 -p 21462
strace: Process 22157 attached
20:04:04.693471 epoll_wait(9,

[{EPOLLIN, {u32=2378772744, u64=139893758570760}}], 512, -1) = 1
20:04:08.085823 accept4(7, {sa_family=AF_INET, sin_port=htons(60941), sin_addr=inet_addr("<接続元アドレス>")}, [16], SOCK_NONBLOCK) = 3
20:04:08.085989 epoll_ctl(9, EPOLL_CTL_ADD, 3, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=2378773240, u64=139893758571256}}) = 0
20:04:08.086055 epoll_wait(9, [{EPOLLIN, {u32=2378772744, u64=139893758570760}}, {EPOLLIN, {u32=2378773240, u64=139893758571256}}], 512, 60000) = 2
20:04:08.086118 accept4(7, {sa_family=AF_INET, sin_port=htons(60942), sin_addr=inet_addr("<接続元アドレス>")}, [16], SOCK_NONBLOCK) = 12
20:04:08.086197 epoll_ctl(9, EPOLL_CTL_ADD, 12, {EPOLLIN|EPOLLRDHUP|EPOLLET, {u32=2378773488, u64=139893758571504}}) = 0
20:04:08.086266 recvfrom(3, "\26", 1, MSG_PEEK, NULL, NULL) = 1
20:04:08.086334 setsockopt(3, SOL_TCP, TCP_NODELAY, [1], 4) = 0
20:04:08.086609 brk(NULL) = 0x5621533bf000
20:04:08.086673 brk(0x5621533e4000) = 0x5621533e4000
20:04:08.086754 read(3, "\26\3\1\2\0\1\0\1\374\3\3", 11) = 11
20:04:08.086869 read(3, "\366~~~略~~~", 506) = 506

<!ここでOPEN!>
20:04:08.087083 open("/etc/nginx/cert/xxx.1773.work.crt", O_RDONLY) = 13
20:04:08.087182 fstat(13, {st_mode=S_IFREG|0666, st_size=1907, ...}) = 0
20:04:08.087263 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3b8dce4000
20:04:08.087334 read(13, "-----BEGIN CERTIFICATE-----\n<証明書の内容>"..., 4096) = 1907
20:04:08.087553 read(13, "", 4096) = 0
20:04:08.087622 close(13) = 0
20:04:08.087783 munmap(0x7f3b8dce4000, 4096) = 0

<!ここでOPEN!>
20:04:08.087887 open("/etc/nginx/cert/xxx.1773.work.key", O_RDONLY) = 13
20:04:08.088032 fstat(13, {st_mode=S_IFREG|0666, st_size=1704, ...}) = 0
20:04:08.088189 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3b8dce4000
20:04:08.088298 read(13, "-----BEGIN PRIVATE KEY-----\n<秘密鍵の内容>"..., 4096) = 1704
20:04:08.088525 close(13) = 0
20:04:08.088587 munmap(0x7f3b8dce4000, 4096) = 0
20:04:08.090924 write(3, "~~~略~~~","..., 1809) = 1809
20:04:08.091136 read(3, 0x5621533bbc83, 33093) = -1 EAGAIN (Resource temporarily unavailable)
20:04:08.091276 epoll_wait(9, [{EPOLLIN, {u32=2378773488, u64=139893758571504}}], 512, 59999) = 1
20:04:08.091407 recvfrom(12, "\26", 1, MSG_PEEK, NULL, NULL) = 1
20:04:08.091525 setsockopt(12, SOL_TCP, TCP_NODELAY, [1], 4) = 0
20:04:08.091667 read(12, "\26\3\1\2\0\1\0\1\374\3\3", 11) = 11
20:04:08.091799 read(12, "~~~略~~~", 506) = 506

<!ここでOPEN!>
20:04:08.091990 open("/etc/nginx/cert/xxx.1773.work.crt", O_RDONLY) = 13
20:04:08.092114 fstat(13, {st_mode=S_IFREG|0666, st_size=1907, ...}) = 0
20:04:08.092231 mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f3b8dce4000
20:04:08.092348 read(13, "-----BEGIN CERTIFICATE-----\n<証明書の内容>"..., 4096) = 1907
20:04:08.092557 read(13, "", 4096) = 0
20:04:08.092676 close(13) = 0
20:04:08.092787 munmap(0x7f3b8dce4000, 4096) = 0

<!ここでOPEN!>
20:04:08.092913 open("/etc/nginx/cert/xxx.1773.work.key", O_RDONLY) = 13
20:04:08.093055 fstat(13, {st_mode=S_IFREG|0666, st_size=1704, ...}) = 0
20:04:08.093288 read(13, "-----BEGIN PRIVATE KEY-----\n<秘密鍵の内容>"..., 4096) = 1704
20:04:08.093582 munmap(0x7f3b8dce4000, 4096) = 0
20:04:08.095721 write(12, "~~~略~~~"..., 1809) = 1809
20:04:08.096013 read(12, 0x5621533bbc83, 33093) = -1 EAGAIN (Resource temporarily unavailable)
20:04:08.096170 epoll_wait(9, [{EPOLLIN, {u32=2378773240, u64=139893758571256}}], 512, 59994) = 1
20:04:08.117737 read(3, "~~~略~~~", 33093) = 126
20:04:08.118427 write(3, "~~~略~~~", 258) = 258

~~~略~~~

20:04:08.130079 write(5, "time:2019-03-04T20:04:08+09:00\thost:xxx.1773.work\thttp_host:xxx.1773.work\tscheme:https\tremote_addr:<接続元アドレス>\tremote_user:-\ttime_local:04/Mar/2019:20:04:08 +0900\trequest:GET / HTTP/1.1\tstatus:200\tbody_bytes_sent:0\thttp_referer:-\thttp_user_agent:Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.119 Safari/537.36\thttp_x_forwarded_for:-\trequest_time:0.000\tupstream_response_time:\tmsec:1551697448.128\tupstream_http_content_type:-\tupstream_status:-\tupstream_cache_status:-\n", 524) = 524

~~~略~~~

 

② https://xxx.1773.work/ への2度目のアクセス(再ハンドシェイク)

挙動:https://xxx.1773.work/が表示され 適切な証明書が表示される

straceのログを見ていると①とフローが一致。

ドキュメント記述の通り、SSLハンドシェイク毎に 都度証明書(と鍵)がロードされる という動作を確認しました。

 

③ https://yyy.1773.work/ へのアクセス (証明書未配置のパス)

 挙動:SSLハンドシェイク失敗

■straceの抜粋

~~~略~~~

20:12:51.337382 open("/etc/nginx/cert/yyy.1773.work.crt", O_RDONLY) = -1 ENOENT (No such file or directory)
20:12:51.337677 gettid() = 22157
20:12:51.337840 write(4, "2019/03/04 20:12:51 [error] 22157#22157: *7 cannot load certificate \"/etc/nginx/cert/yyy.1773.work.crt\": BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/etc/nginx/cert/yyy.1773.work.crt','r') error:2006D080:BIO routines:BIO_new_file:no such file) while SSL handshaking, client: <接続元アドレス>, server: 0.0.0.0:443\n", 367) = 367
20:12:51.338061 write(13, "\25\3\3\0\2\2P", 7) = 7
20:12:51.338266 close(13) = 0

~~~略~~~
■error_logへの出力

2019/03/04 20:12:51 [error] 22157#22157: *7 cannot load certificate "/etc/nginx/cert/yyy.1773.work.crt": BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/etc/nginx/cert/yyy.1773.work.crt','r') error:2006D080:BIO routines:BIO_new_file:no such file) while SSL handshaking, client: <接続元アドレス>, server: 0.0.0.0:443
2019/03/04 20:12:51 [error] 22157#22157: *6 cannot load certificate "/etc/nginx/cert/yyy.1773.work.crt": BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/etc/nginx/cert/yyy.1773.work.crt','r') error:2006D080:BIO routines:BIO_new_file:no such file) while SSL handshaking, client: <接続元アドレス>, server: 0.0.0.0:443
2019/03/04 20:12:51 [error] 22157#22157: *8 cannot load certificate "/etc/nginx/cert/yyy.1773.work.crt": BIO_new_file() failed (SSL: error:02001002:system library:fopen:No such file or directory:fopen('/etc/nginx/cert/yyy.1773.work.crt','r') error:2006D080:BIO routines:BIO_new_file:no such file) while SSL handshaking, client: <接続元アドレス>, server: 0.0.0.0:443

誘導先の証明書(鍵)パスが存在していないとエラーに。 

 

④ https://yyy.1773.work/ へのアクセス (証明書の動的追加)

証明書追加を行います。

# mv old/yyy* ./

# ls -l /etc/nginx/cert/
total 20
drwxr-xr-x 2 root root 4096 Mar 4 12:18 old
-rw-r--r-- 1 root root 1907 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.crt
-rw-r----- 1 root root 1704 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.key
-rw-r--r-- 1 root root 1907 Mar 4 12:51 /etc/nginx/cert/yyy.1773.work.crt
-rw-r----- 1 root root 1704 Mar 4 12:51 /etc/nginx/cert/yyy.1773.work.key

 挙動:https://yyy.1773.work/ が表示され 適切な証明書が表示される

straceのログを見ていると①②とフローが一致。

reloadを伴わない動的追加が可能そうです。

 

⑤ https://yyy.1773.work/ へのアクセス (証明書の動的削除)

今度は、証明書を削除します。

# mv yyy.* old/

# ls -l /etc/nginx/cert/
total 12
drwxr-xr-x 2 root root 4096 Mar 4 12:18 old
-rw-r--r-- 1 root root 1907 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.crt
-rw-r----- 1 root root 1704 Mar 4 12:58 /etc/nginx/cert/xxx.1773.work.key

挙動:

(1) keepaliveが切れるまではhttps://yyy.1773.work/ が表示され 適切な証明書が表示される

(2) 再SSLハンドシェイク時より ハンドシェイク失敗する

 

今回は、これらのざっくりとした挙動を確認してみました。

 

Ansible で 動的リストをつかって 一部の処理をループ実行する

この記事の概要

Ansible の playbook を使って、

① 対象サーバからリスト(インターフェース一覧など)を取得する
② そのリストの項目毎に一部の処理をループ実行する

ということを、1回のplaybook実行でできる記述方法を模索していたところ、とあるアプローチに至ったので、その手法を書き留めておきます。

変数に定義するのはつらいリスト(流動的 or 膨大な量)に対して処理を実行したいときに使えそうな方法なのではないかと思っています。

 

実行バージョンによる留意事項

私が試した Ansible バージョンは 2.6.2です。

記事内の記述は、バージョン 2.4 以降の Action である import_tasksinclude_tasks をつかって記述しています。

バージョン 2.6.2だとまだ include でも記述可能ですが、下記のような include は 2.8で無くなる という旨の警告が出力されます。

[DEPRECATION WARNING]: 'include' for playbook includes. You should use 'import_playbook' instead. This
feature will be removed in version 2.8. Deprecation warnings can be disabled by setting
deprecation_warnings=False in ansible.cfg. 

バージョン 2.1以降~2.4未満で記述する場合は、逆に include を使わないとsyntax errorになる はずですので、

下記に記述する手法の import_tasksinclude_tasksinclude に置き換えて頂くと、同様の動作をすると思われます。

 

playbook の記述例と解説

さて、本題です。playbook の記述例 です。

<階層>

ansible.cfg
hosts
playbook.yml
roles
L test
L tasks
L main.yml
  L install.yml
  L config.yml
L setup.yml ← こちらがループ対象の処理

※ ansible.cfg、hosts、install.yml については今回の説明に内容の例示が不要ですので割愛させていただきます。

 

<playbook.yml>

---
- hosts: all
remote_user: root
roles:
- role: test

 

<main.yml>

---
- import_tasks: install.yml
tags:
- test
- test_install

- import_tasks: config.yml
tags:
- test
- test_config

 

<config.yml>

- name: config
command: echo "test config"

## ここで何かしらのリストを取得します
- name: get target interface name
shell: |
/sbin/ip -o link | awk -F': ' '{print $2}' | grep -v lo
register: interface_list
check_mode: no
changed_when: false

- include_tasks: setup.yml
with_items: "{{ interface_list.stdout_lines }}"

 わたしが例で示したリスト取得コマンドの実行結果の例はこのような内容です。

$ /sbin/ip -o link | awk -F': ' '{print $2}' | grep -v lo
eth0
eth1

これを、with_items: <レジスタ名>.stdout_lines で呼ぶと リストの各行を 1 item として setup.yml をループ実行させることができます。

 

<setup.yml>

- name: setup test
command: echo "{{ item }}"

 今回は、分かりやすいようにechoするだけにしてみます。

 

playbook の実行結果

さて、このplaybookを実行してみます。

ansible-playbook playbook.yml -v -D -t test

PLAY [all] **********************************************************************

(略)

TASK [test : install] ***********************************************************
changed: [(アドレス)] => {"changed": true, "cmd": ["echo", "install"], "delta": "0:00:00.002680", "end": "2019-02-22 23:58:21.466355", "rc": 0, "start": "2019-02-22 23:58:21.463675", "stderr": "", "stderr_lines": [], "stdout": "install", "stdout_lines": ["install"]}

TASK [test : config] ************************************************************
changed: [(アドレス)] => {"changed": true, "cmd": ["echo", "config"], "delta": "0:00:00.002671", "end": "2019-02-22 23:58:22.044150", "rc": 0, "start": "2019-02-22 23:58:22.041479", "stderr": "", "stderr_lines": [], "stdout": "config", "stdout_lines": ["config"]}

TASK [test : get target interface name] *****************************************
ok: [(アドレス)] => {"changed": false, "cmd": "/sbin/ip -o link | awk -F': ' '{print $2}'", "delta": "0:00:00.004626", "end": "2019-02-22 23:58:22.631947", "rc": 0, "start": "2019-02-22 23:58:22.627321", "stderr": "", "stderr_lines": [], "stdout": "eth0\neth1", "stdout_lines": ["eth0", "eth1"]}

TASK [test : include_tasks] *****************************************************
included: /home/nozomi/work/test-loop/roles/test/tasks/setup.yml for (アドレス)
included: /home/nozomi/work/test-loop/roles/test/tasks/setup.yml for (アドレス)

TASK [test : setup test] ********************************************************
changed: [(アドレス)] => {"changed": true, "cmd": ["echo", "lo"], "delta": "0:00:00.002642", "end": "2019-02-22 23:58:23.403662", "rc": 0, "start": "2019-02-22 23:58:23.401020", "stderr": "", "stderr_lines": [], "stdout": "eth0", "stdout_lines": ["eth0"]}

TASK [test : setup test] ********************************************************
changed: [(アドレス)] => {"changed": true, "cmd": ["echo", "eth0"], "delta": "0:00:00.002678", "end": "2019-02-22 23:58:24.035418", "rc": 0, "start": "2019-02-22 23:58:24.032740", "stderr": "", "stderr_lines": [], "stdout": "eth1", "stdout_lines": ["eth1"]}

PLAY RECAP **********************************************************************

(略)

 実行処理の順序は下記のようになりました。

main.yml
→ install.yml
task: install
→ config.yml
task: config
task: include_tasks
→ setup.yml
task: setup test (1回目/item=eth0)
task: setup test (2回目/item=eth1)

 このように 動的リストをつかって 一部の処理をループ実行させることができました。