最近会社で聞かれて答えたことをまとめてみました。
OpenSSHの仕組みについて説明していたところ、「ホスト鍵?パスワード認証なのに鍵?」のような疑問の問いかけを受けました。よくよく聞いてみると、"ホスト鍵"と"公開鍵認証で使う鍵"の理解が曖昧な様子でした。
そこで、ホスト鍵について説明したことを掘り下げてまとめてみました。
サーバー認証とユーザー認証
SSHの認証プロセスには、以下の2つの段階があります。
- サーバー認証:ログインしようとしているサーバーが、本当にログインしたいサーバーなのかを認証する
- ユーザー認証:ログインしようとしているユーザーが、サーバーにログインしてもよいユーザーなのかを認証する
"パスワード認証"とか"公開鍵認証"とかいった言葉は、ユーザー認証の話です。一方、"ホスト鍵"はサーバー認証の話です。この部分の理解がまずは必要です。
これが理解できると、"パスワード認証"でも"公開鍵認証"でも、サーバー認証が行われるということが理解できます。そうすると、"パスワード認証"でも"ホスト鍵"と呼ばれる鍵が使われることが理解できます。
それでは、サーバー認証の部分を少しだけ掘り下げていくことにします。
サーバー認証の役割と手順
サーバー認証は、ログインしようとしているサーバーが、本当にログインしたいサーバーなのかを認証するというものでした。言い換えると、なりすましサーバーなどではないか?という疑問を払拭するための認証です。
サーバー認証の大まかな手順は、以下のようになっています。
- SSHクライアントがSSHサーバーに接続する。
- SSHサーバーは、自身のホスト鍵を、SSHクライアントに送出する。
- SSHクライアントは、自身が保持している"このサーバーは、過去にホスト鍵~~~を送ってきたことがある"という情報を確認する。
- 手順2と手順3のホスト鍵を照合する。一致すれば、"今接続しようとしているサーバーは、いつものサーバーだ"ということになる。
上の手順は、かなり端折って書いています。さらに、上に"ホスト鍵"と書いたものは"ホスト公開鍵"と呼ばれるものだったりします。しかし、公開鍵暗号技術そのものは今回の本題ではないので、その部分の記載は省略します。
手順4に"いつものサーバー"とある部分がポイントです。"今までホスト鍵=ABCDEを送ってきていたサーバーが、今回も同じホスト鍵=ABCDEを送ってきたのだから、前と同じサーバー=正しいサーバーに接続しているのだ"という認証の仕方ということです。
2回目以降はこの仕組みでOKでしょう。初回接続はどうなるでしょうか?初回接続時、ホスト鍵=ABCDEを送ってきていたサーバーは、本当に正しいサーバーなのでしょうか?
初回接続時のホスト鍵が正しいかどうかは本来、より安全な他の方法で確認すべきものです。但し、"そのような確認をしている人はあまりいない"、というようなことが過去のUnixUserに書かれていたりします。
第3章 OpenSSHのしくみ 3.2.3. なりすましを防ぐしくみ
上記のサイト記事はうまくまとまっているので、一度見ておくとよいでしょう。基本や概念を理解するには十分だと思います。10年以上前の雑誌寄稿記事をWeb公開しているものなので、新しい仕組み(キーローテーションなど)が書かれていない点には要注意です。
では、"より安全な他の方法"で確認してみることにしましょう。
確認方法 (ssh-keygen,MD5形式)
ホスト鍵が作成されるところを見るには、以下のように実行するのが分かりやすいと思います。CentOS 6では、OpenSSHの初回起動時にホスト鍵を自動的に作成します。
lubuntul:~$ docker run -it --rm centos:6
[root@c32865355acb /]# yum -q -y install openssh-server
:
[root@c32865355acb /]# /etc/init.d/sshd start
Generating SSH2 RSA host key: [ OK ]
Generating SSH1 RSA host key: [ OK ]
Generating SSH2 DSA host key: [ OK ]
Starting sshd: [ OK ]
[root@c32865355acb /]#
CentOSを使用してopenssh-serverをインストールして、sshdを起動してみました。すると"Generating SSH~ ~ host key:"というメッセージが並んでいるのが分かります。これがホスト鍵作成の瞬間です。
作成される鍵の場所は、ディストリビューションによって違うかもしれません。CentOSは"/etc/ssh/"にあります。
[root@c32865355acb /]# ls -l /etc/ssh/*host*
-rw------- 1 root root 668 Mar 7 04:05 /etc/ssh/ssh_host_dsa_key
-rw-r--r-- 1 root root 590 Mar 7 04:05 /etc/ssh/ssh_host_dsa_key.pub
-rw------- 1 root root 963 Mar 7 04:05 /etc/ssh/ssh_host_key
-rw-r--r-- 1 root root 627 Mar 7 04:05 /etc/ssh/ssh_host_key.pub
-rw------- 1 root root 1675 Mar 7 04:05 /etc/ssh/ssh_host_rsa_key
-rw-r--r-- 1 root root 382 Mar 7 04:05 /etc/ssh/ssh_host_rsa_key.pub
[root@c32865355acb /]#
見ての通り、メッセージに表示されていた3種類、それぞれホスト秘密鍵(拡張子なし)とホスト公開鍵(拡張子=.pub)の計6種類が表示されているのが分かります。
では、このサーバーに接続してみます。これを試すならクライアントも入れて、自分自身に繋ぐのがお手軽でしょう。
[root@c32865355acb /]# yum -q -y install openssh-clients
:
[root@c32865355acb /]# ssh localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
RSA key fingerprint is 7b:2f:77:63:1b:91:82:18:29:72:9d:26:86:5e:bc:7f.
Are you sure you want to continue connecting (yes/no)?
サーバーから送られてきた"7b:2f:77:63:1b:91:82:18:29:72:9d:26:86:5e:bc:7f"が、前項の手順2のホスト鍵です。これと比較すべきは"ssh_host_rsa_key"です。
[root@c32865355acb /]# cat /etc/ssh/ssh_host_rsa_key.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEApdFuX08k/h1SAZScHxOs2t2+9DY6f3bD/XtztfmjXPapwuvAwO1mCjevHFtitnDMOjamVEb2R5xP21b6jQ4hH/XpQRmYcofZycgfBQZsRyDhEPXSkfImXUc18J8CNK7TcXwk3077/7OggMfFnUWgwKrFDCkpSHp4ajEc6lK/usjeafWTtsKGAq5iXyMqAzcZlnHVA+Zonu076om7bs5Mv1Fy42DsXN1Cc0ZXxJiW/635+R8VXtlGOKyx1e/8dAcXZHzooWtANW1rQWdXljTOY0pf1i5FkjnIB0qV19kOdp7x0AujTfW3/ISO8HCVYTWMoSrCU7FHz9mOq1kp1/bNUQ==
[root@c32865355acb /]#
おっと、全く違いました。
サーバーから送られてきたと上で書いた"7b:2f:77:63:~"は、"ホスト鍵"そのものではありません。サーバーから送られてきた"ホスト鍵"をもとに、クライアント側でfingerprintを算出したものです。表示されているメッセージをよく見ると"RSA key fingerprint"と書かれています。他方、"ssh_host_rsa_key"はホスト鍵そのものです。これは一致しなくて当然です。
では改めて確認してみます。
[root@c32865355acb /]# ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key.pub
2048 7b:2f:77:63:1b:91:82:18:29:72:9d:26:86:5e:bc:7f /etc/ssh/ssh_host_rsa_key.pub (RSA)
[root@c32865355acb /]#
この"7b:2f:77:63:1b:91:82:18:29:72:9d:26:86:5e:bc:7f"が、"より安全な他の方法"で確認したfingerprintです。そして、サーバーから送られてきたものと一致していることが確認できました。
確認方法 (ssh-keygen,SHA256形式)
前項に挙げたCentOS 6にインストールされるOpenSSHは5.3p1です。実はこのバージョンで採用されているfingerprintの形式(MD5/hex)は古いものです。OpenSSH 6.8からは新しい形式(SHA256/base64)が採用されています。
前項と同様のやり方で、CentOS 7を用いて確認してみます。CentOS 7を使うと、OpenSSH 7.4p1がインストールされることになります。
但し、サービス起動がうまくいかない事情があります。それに加えて、サーバープログラムsshdとホスト鍵生成プログラムsshd-keygenが分離しているため、ホスト鍵を手動で作成する必要があります。
それらを回避しつつ、前項と同様のことを確認してみます。
lubuntu:~$ docker run -it --rm centos:7
[root@80d383d2f059 /]# yum list | grep -F openssh.
openssh.x86_64 7.4p1-21.el7 base
[root@80d383d2f059 /]# yum -q -y install openssh-server openssh-clients
:
[root@80d383d2f059 /]# ssh-keygen -t rsa -N '' -f /etc/ssh/ssh_host_rsa_key
:
[root@80d383d2f059 /]# ssh-keygen -t ecdsa -N '' -f /etc/ssh/ssh_host_ecdsa_key
:
[root@80d383d2f059 /]# ssh-keygen -t ed25519 -N '' -f /etc/ssh/ssh_host_ed25519_key
:
[root@80d383d2f059 /]# /usr/sbin/sshd
[root@80d383d2f059 /]# ssh localhost
The authenticity of host 'localhost (127.0.0.1)' can't be established.
ECDSA key fingerprint is SHA256:HQsPnNw3C1uicBctaNtiWPm6m/TgUK+IcIDjymxiIrg.
ECDSA key fingerprint is MD5:4e:ec:be:b4:3c:76:fb:8c:53:7a:68:20:6e:c2:37:88.
Are you sure you want to continue connecting (yes/no)?
ssh接続すると、ECDSAのホスト鍵が2種類の形式で表示されました。一方はMD5、もう一方はSHA256です。これらがサーバーから送られてきたホスト鍵のfingerprintです。
では、前項同様にサーバー上のホスト鍵から生成したfingerprintを確認してみることにしましょう。
[root@80d383d2f059 /]# ssh-keygen -l -f /etc/ssh/ssh_host_ecdsa_key.pub
256 SHA256:HQsPnNw3C1uicBctaNtiWPm6m/TgUK+IcIDjymxiIrg root@80d383d2f059 (ECDSA)
[root@80d383d2f059 /]# ssh-keygen -l -E MD5 -f /etc/ssh/ssh_host_ecdsa_key.pub
256 MD5:4e:ec:be:b4:3c:76:fb:8c:53:7a:68:20:6e:c2:37:88 root@80d383d2f059 (ECDSA)
[root@80d383d2f059 /]#
それぞれ、サーバーから送られてきたものと同じだということが分かります。
確認方法 (本題)
fingerprintの表示は、クライアントプログラムsshのバージョンに依存します。5.3pは旧型式、6.8は新旧併記でした。
実は、6.8が新旧併記というのはCentOS 7に付属のOpenSSHの場合です。他のディストリビューションでは異なる可能性があります。実際、素のOpenSSHでは新形式のみで表示されます。
さて、Windows 10標準についているssh.exeは7.7p1です。このクライアントから旧形式バージョンのサーバーを見に行くとどう表示されるでしょうか?
サーバー側のセットアップ手順はこうです。
lubuntu:~$ docker run -it --rm -p 10022:22 centos:6
[root@8976e3f07656 /]# yum -q -y install openssh-server
:
[root@8976e3f07656 /]# /etc/init.d/sshd start
Generating SSH2 RSA host key: [ OK ]
Generating SSH1 RSA host key: [ OK ]
Generating SSH2 DSA host key: [ OK ]
Starting sshd: [ OK ]
[root@8976e3f07656 /]# ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key.pub
2048 8d:68:31:b5:00:78:28:42:cf:04:4a:0a:e7:d1:14:e2 /etc/ssh/ssh_host_rsa_key.pub (RSA)
[root@8976e3f07656 /]#
クライアントから接続すると、こうなります。
C:\> ssh -p 10022 192.168.99.104
The authenticity of host '[192.168.99.104]:10022 ([192.168.99.104]:10022)' can't be established.
RSA key fingerprint is SHA256:xosTKy/+ZnditceplyYI766wWWJ4W7y71yle7VYpKBg.
Are you sure you want to continue connecting (yes/no)?
どうやらWindows標準の7.7p1は新形式のfingerprintしか表示しないようです。しかし、サーバーのssh-keygenは古いため、表示形式を指定するオプションに対応していません。サーバー上でホスト鍵のfingerprintを確認しようにも、新形式では表示できないわけです。
そもそもそんな古いバージョンのsshdが入っていることが問題かもしれません。しかし、そのようなケースに遭遇した時に、どうすればいいでしょうか?
ホスト鍵本体で確認
ssh.exeの代わりにssh-keyscan.exeコマンドを実行してみます。
C:\>ssh-keyscan -p 10022 192.168.99.104
# 192.168.99.104:10022 SSH-2.0-OpenSSH_5.3
[192.168.99.104]:10022 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwuSC9W1cSyrS7CQE/X5xDXyOKBpdOV7Gclo3CuifhjGtUEZJAe8b6U0FaaedDaXKh99tG5twxcFi1krQue8y69a8YYQUvRYKbJtga2KupOZSmC7pEFF3ssH9bf7FUKMmroW7kgfStEd7w8fVue1qbsKTlraSffisNsy1hwtc7mMi0LLiKYpkCGPdz8GV/VL3oxcGvrWO31SkBWKNPRj5uwv94esIIlx/FVSFkFrRWdrpv6hqyYfSmeHCr33HiCh/k2hahLGsBufqw9S0r0UGtkBgPd7aNJJXwsr+N8tnbNjdpsujmukmoscDp6mfdnR9I/2Z7u8/1Oxl01oLIa303Q==
# 192.168.99.104:10022 SSH-2.0-OpenSSH_5.3
# 192.168.99.104:10022 SSH-2.0-OpenSSH_5.3
C:\>
fingerprintではなくホスト鍵本体を取り出すことができました。これが"/etc/ssh/ssh_host_rsa_key.pub"と一致していることを確認するという方法が使えます。
なお、ファイルにリダイレクトすると、2行目だけがファイルに書き込まれます。そして、Windowsに付属のssh-keygen.exeを使用することで、ホスト鍵のfingerprintを確認できます。但し、これはあくまで"接続したホストが返してきた情報"です。より安全な方法というわけではありません。
C:\>ssh-keyscan -p 10022 192.168.99.104 > D:\Tmp\test.txt
# 192.168.99.104:10022 SSH-2.0-OpenSSH_5.3
# 192.168.99.104:10022 SSH-2.0-OpenSSH_5.3
# 192.168.99.104:10022 SSH-2.0-OpenSSH_5.3
C:\>ssh-keygen -l -f D:\Tmp\test.txt
2048 SHA256:xosTKy/+ZnditceplyYI766wWWJ4W7y71yle7VYpKBg [192.168.99.104]:10022 (RSA)
C:\>ssh-keygen -l -E MD5 -f D:\Tmp\test.txt
2048 MD5:8d:68:31:b5:00:78:28:42:cf:04:4a:0a:e7:d1:14:e2 [192.168.99.104]:10022 (RSA)
C:\>
ssh.exeはSHA256しか出力しないのに、ssh-keygen.exeは両方に対応している模様です。
そして、送られてきたホスト鍵の旧形式のfingerprintを確認することができれば、サーバー上のssh-keygenが旧型式のみに対応している場合でも、比較確認することができます。
ホスト鍵からMD5/hexを算出
古い形式はMD5/hexという形式でした。このfingerprintの算出方法は、下のようになります。1つ目がssh-keygenコマンドと結果、2つ目が算出コマンドと結果です。
[root@8976e3f07656 /]# ssh-keygen -l -f /etc/ssh/ssh_host_rsa_key.pub | awk '{print $2}'
8d:68:31:b5:00:78:28:42:cf:04:4a:0a:e7:d1:14:e2
[root@8976e3f07656 /]# cat /etc/ssh/ssh_host_rsa_key.pub | awk '{print $2}' | base64 -d | md5sum | awk '{print $1}' | sed -E -e 's/(..)/\1:/g' | sed -e 's/:$//'
8d:68:31:b5:00:78:28:42:cf:04:4a:0a:e7:d1:14:e2
[root@8976e3f07656 /]#
解説すると、ホスト鍵の本体(第2フィールド)をBASE64でデコードして、MD5を算出したものが、古い形式のfingerprintです。
別解 FingerprintHash
ssh_configには、FingerprintHashというキーワードがあります。これは"Key Fingerprint"の表示アルゴリズムを指定するというものです。これに"md5"を指定することで、旧形式で表示することが可能です。
ssh_configのキーワードは、sshコマンドの引数"-o"で指定可能です。つまり、以下のようなコマンドを使えば、旧形式で表示することが可能です。これはWindows付属のOpenSSHでも可能です。但し、Dockerコンテナ無き今、現物を用いた結果を確認することはできません。
ssh -o FingerprintHash=md5 -p 10022 192.168.99.104
FingerprintHashを用いるほうが頑張らなくても旧形式で表示できます。
ホスト鍵からSHA256/base64を算出
新しい形式はSHA256/base64という形式でした。このfingerprintの算出方法は、下のようになります。ssh-keygenによる出力はできないので、ホスト鍵と算出結果を載せています。
[root@8976e3f07656 /]# cat /etc/ssh/ssh_host_rsa_key.pub
ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAwuSC9W1cSyrS7CQE/X5xDXyOKBpdOV7Gclo3CuifhjGtUEZJAe8b6U0FaaedDaXKh99tG5twxcFi1krQue8y69a8YYQUvRYKbJtga2KupOZSmC7pEFF3ssH9bf7FUKMmroW7kgfStEd7w8fVue1qbsKTlraSffisNsy1hwtc7mMi0LLiKYpkCGPdz8GV/VL3oxcGvrWO31SkBWKNPRj5uwv94esIIlx/FVSFkFrRWdrpv6hqyYfSmeHCr33HiCh/k2hahLGsBufqw9S0r0UGtkBgPd7aNJJXwsr+N8tnbNjdpsujmukmoscDp6mfdnR9I/2Z7u8/1Oxl01oLIa303Q==
[root@8976e3f07656 /]# cat /etc/ssh/ssh_host_rsa_key.pub | awk '{print $2}' | base64 -d | sha256sum | awk '{print $1}' | xargs echo -n | sed -E -e 's/(.{2})/\1\n/g' | while read -r hex; do printf "%b" "\x${hex}"; done | base64
xosTKy/+ZnditceplyYI766wWWJ4W7y71yle7VYpKBg=
[root@8976e3f07656 /]#
ホスト鍵の本体(第2フィールド)をBASE64でデコードして、SHA256を算出し、16進数⇒バイナリ変換し、BASE64エンコードしたものが、新しい形式のfingerprintです。
算出結果が、Windowsからの接続時に表示されていた文字列 "xosTKy/+ZnditceplyYI766wWWJ4W7y71yle7VYpKBg" と一致していることが分かります。末尾の"="の違いはBASE64でよく見られる末尾のパディング文字なので問題ありません。気になるなら"sed"で切り取っておくか。
補足:16進数⇒バイナリ変換
sed -E -e 's/(.{2})/\1\n/g' | while read -r hex; do printf "%b" "\x${hex}"; done
やっていることは、"2桁ごとに行分割"、"各行を16進数2桁⇒バイナリに変換"、"変換結果を改行せずに連結"です。テストしたところ、文字コード00も扱えるようでした。
Lubuntuに入っている"xxd"コマンドが使えるなら、"xxd -r -p"でも構いません。CentOS(6~latest)には"xxd"コマンドが標準では入っていませんでした。そのため、無理やり作りました。
最初は"perl -pe"と"pack()"でやろうとしたのですが、うまく動かず。もしかすると、うまく動いていないと思ったのは、単に改行LFを除去できていなかっただけなのかもしれません。ハッシュは1文字違うと大違いなので。"xargs echo -n"の部分が改行除去の部分です。
最後に
SSHサーバーは、古いCentOS 6のopenssh-serverを使って独立で建てるべきだったかも。同じホスト鍵を複数のバージョンのOpenSSHで比較確認する形の記事にするほうが分かりやすかったかもしれません。CentOS 7のdocker+systemd問題を気にする必要もなかっただろうし。