「Are you sure you want to continue connecting に、今日も yes と打った……」
初めてのサーバーに接続したときに出る、あの長い警告です。読まずに yes と打つ。2回目からは出ないので、そのまま忘れる。実はあの警告は、本編の第3回から第5回まで引きずった「この公開鍵は本当に本人のものか」という問題が、そのままの形で目の前に出てきたものでした。番外編の2本目は、毎日使っている SSH と SFTP の中を覗きます。
OpenSSL で手を動かして学ぶ暗号の基礎、今回は番外編です。本編を読んでいなくても進められますが、署名の仕組みは第4回、公開鍵の身元の問題は第5回が下敷きになっています。
SSH には、鍵ペアが2組ある
先に地図を描いておきます。SSH の接続には鍵ペアが2組出てきます。1組はサーバーの鍵(ホスト鍵)で、「このサーバーは名乗りどおりか」をあなたが確かめるためのもの。もう1組はあなたの鍵(ユーザー鍵)で、「この接続は本人か」をサーバーが確かめるためのもの。どちらも第3回で作った鍵ペアと同じ種類の道具で、どちらも第4回の署名で働きます。向きが逆なだけです。
上の段はユーザー鍵、下の段はホスト鍵。向きが逆なだけで、道具は同じ。秘密鍵はそれぞれの手元から動かない。
今日はユーザー鍵から入って、ホスト鍵で終わります。
ssh-keygen で鍵ペアを作り、中を開く
第3回は openssl genpkey で作りましたが、SSH には専用のコマンドがあります。手元の鍵を上書きしないように、練習用の名前で作ります。
ssh-keygen -t ed25519 -C "lab@example" -f ~/.ssh/lab_ed25519
パスフレーズを聞かれます。練習用なら空で構いません(本番の鍵には付けます。注意点で触れます)。lab_ed25519(秘密鍵)と lab_ed25519.pub(公開鍵)ができました。-t ed25519 は鍵の種類で、第3回の注意点に書いた楕円曲線の系統です。公開鍵を見ます。
cat ~/.ssh/lab_ed25519.pub
# ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIQNSsl1ZZ4sVW7lgCaE4DAWw6AkucaC/VodNNhJsUc4 lab@example
1行です。第3回の alice.pub は BEGIN PUBLIC KEY で始まる数行のファイルでしたが、SSH は鍵の種類・本体・コメントを空白で区切った1行形式を使います。真ん中の塊を base64 から戻すと、中身が見えます。
awk '{print $2}' ~/.ssh/lab_ed25519.pub | base64 -d | xxd
# 00000000: 0000 000b 7373 682d 6564 3235 3531 3900 ....ssh-ed25519.
# 00000010: 0000 2084 0d4a c975 659e 2c55 6ee5 8026 .. ..J.ue.,Un..&
# 00000020: 84e0 3016 c3a0 24b9 c682 fd5a 1d34 d849 ..0...$....Z.4.I
# 00000030: b147 38 .G8
鍵の種類名と、32バイトの鍵本体。Ed25519 の公開鍵は本当にこれだけです。openssl で読むには、第3回の形式(PEM/DER)の封筒に入れ直す必要があります。Ed25519 の封筒は「これは Ed25519 の公開鍵です」と書いた決まりきった12バイトなので、手で足せます。
( printf '\x30\x2a\x30\x05\x06\x03\x2b\x65\x70\x03\x21\x00'
awk '{print $2}' ~/.ssh/lab_ed25519.pub | base64 -d | tail -c 32 ) \
| openssl pkey -pubin -inform DER -text -noout
# ED25519 Public-Key:
# pub:
# 84:0d:4a:c9:75:65:9e:2c:55:6e:e5:80:26:84:e0:
# 30:16:c3:a0:24:b9:c6:82:fd:5a:1d:34:d8:49:b1:
# 47:38
xxd の後半と同じ数字が、第3回と同じ体裁で出てきました。SSH の鍵は、封筒が違うだけで中身は本編の鍵です。
サーバーに置くのは、公開鍵だけ
このユーザー鍵でサーバーにログインできるようにするには、公開鍵をサーバーの ~/.ssh/authorized_keys に1行追記します。ssh-copy-id がその作業をやってくれます。
ssh-copy-id -i ~/.ssh/lab_ed25519.pub user@host
GitHub のようなサービスなら、設定画面に .pub の中身を貼ります。どちらも、運ぶのは公開鍵だけです。
サーバーの手元に何があるかを確かめます。当ブログのデプロイ先(レンタルサーバー)に入って、置いてある鍵のフィンガープリント(公開鍵を短く縮めた識別値)をサーバー側で取ります。xserver は ~/.ssh/config に書いた接続先の名前です。
ssh xserver 'ssh-keygen -lf ~/.ssh/authorized_keys | cut -d" " -f1,2'
# 4096 SHA256:wYziAa8txTFF/oncHJ4/18VxvC3k7WCfioS4fZ+WVWQ
ssh-keygen -lf ~/.ssh/id_rsa.pub | cut -d' ' -f1,2
# 4096 SHA256:wYziAa8txTFF/oncHJ4/18VxvC3k7WCfioS4fZ+WVWQ
サーバー側と手元で同じフィンガープリントです。サーバーが持っているのは公開鍵の控えだけで、秘密鍵は手元に置いたままです。第3回の「錠前は配ってよく、鍵だけ隠す」が、そのまま運用になっています。
公開鍵がどれほど公開かは、GitHub が分かりやすい。登録した公開鍵は、誰でも URL で見られます。
curl -s https://github.com/kiyotd.keys
# ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDT...
ログインは、パスワードではなく署名で通る
接続の中身を見ます。-v を付けると認証の経過が読めます。相手は GitHub です(-T は端末を開かない指定で、GitHub は接続確認用にこの形を受け付けています)。
ssh -v -T git@github.com 2>&1 | grep -E "Authentications|Offering|accepts|Authenticated|Hi "
# debug1: Authentications that can continue: publickey
# debug1: Offering public key: /Users/kiyotd/.ssh/id_rsa RSA SHA256:wYziAa8txTFF/oncHJ4/18VxvC3k7WCfioS4fZ+WVWQ
# debug1: Server accepts key: /Users/kiyotd/.ssh/id_rsa RSA SHA256:wYziAa8txTFF/oncHJ4/18VxvC3k7WCfioS4fZ+WVWQ
# Authenticated to github.com ([20.27.177.113]:22) using "publickey".
# Hi kiyotd! You've successfully authenticated, but GitHub does not provide shell access.
認証の流れが4行で読めます。使える認証方式は publickey だけ(パスワードは選択肢にすらない)。手元の公開鍵を差し出す。サーバーが「その鍵なら登録がある」と受け入れる。認証が publickey で通る。最後の行は GitHub からの挨拶です。
「受け入れる」と「通る」の間で起きているのが、第4回の署名です。サーバーは公開鍵を持っているだけなので、差し出した相手が鍵の持ち主かどうかは、秘密鍵を使えることで証明してもらうしかありません。クライアントは、この接続でしか意味を持たないデータ(接続ごとに変わるセッション ID を含む)に秘密鍵で署名して送り、サーバーは authorized_keys の公開鍵で検証します。dgst -sign と dgst -verify が、ログインのたびに一往復している。パスワードは最初から最後まで通信に流れません。
dgst -sign と dgst -verify が、ログインのたびに一往復する。パスワードは最初から最後まで流れない。
レンタルサーバーも同じです。デプロイ先で同じ行を拾うと、こうなりました。
debug1: Authentications that can continue: publickey,gssapi-keyex,gssapi-with-mic
ここにもパスワードの文字はありません。公開鍵認証に絞ったサーバーでは、パスワードの総当たりはそもそも相手にされない、というのがこの一行の意味です。
フィンガープリントは、公開鍵のハッシュ
ログに何度も出てきた SHA256:wYzi... を片付けます。名前のとおり、第1回のハッシュです。公開鍵の塊(さきほど base64 から戻したもの)を SHA-256 にかけ、base64 で包み直したものがフィンガープリントです。
ssh-keygen -lf ~/.ssh/lab_ed25519.pub
# 256 SHA256:hIMs/GB60Kaetvc5Xy5xQDsy1dxmFHrV1+VxVDMlqR0 lab@example (ED25519)
awk '{print $2}' ~/.ssh/lab_ed25519.pub | base64 -d | openssl dgst -sha256 -binary | base64
# hIMs/GB60Kaetvc5Xy5xQDsy1dxmFHrV1+VxVDMlqR0=
末尾の = を除けば一致します(SSH のフィンガープリントは base64 の詰め物を省く流儀です)。公開鍵そのものは長くて目で比べられないので、第1回の「入力がどれだけ大きくても、出力は64文字」を使って、見比べられる長さに縮めたもの。何と見比べるのかは、次の節です。
第1回の「入力がどれだけ大きくても、出力は固定長」を使って、見比べられる長さに縮めたもの。
初回接続の警告は、ホスト鍵が本物かという第5回の問題
ここからホスト鍵の話です。冒頭の警告を出してみます。手元の記録を一時的に別ファイルに向けると、何度でも「初回」を再現できます。
ssh -o UserKnownHostsFile=/tmp/kh -T git@github.com
The authenticity of host 'github.com (20.27.177.113)' can't be established.
ED25519 key fingerprint is: SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU
This key is not known by any other names.
Are you sure you want to continue connecting (yes/no/[fingerprint])?
読むと、言っていることは第5回の冒頭のつぶやきそのものです。「この公開鍵、本当に github.com のものなのか」。サーバーは接続の最初に自分のホスト鍵(公開鍵)を差し出し、対になる秘密鍵で署名して名乗りを証明します。署名の検証は完璧にできる。でも、差し出された公開鍵が本当に GitHub のものかは、署名では分からない。第4回の注意点に「検証は完璧に成功し、結論だけが間違う」と書いた、あの穴です。
第5回はこの穴を認証局の名簿で埋め、番外編のメールでは DNS が名簿の役でした。SSH の答えは、どちらとも違います。あなたの目で確かめて、あなたの記録に残す。警告はお願いではなく宿題で、このフィンガープリントを、信頼できる別の経路で公開されている値と見比べてください、と言っています。
保証人は認証局でも DNS でもなく、あなた自身。一致したら、yes の代わりにフィンガープリントを貼って記録する。
GitHub はフィンガープリントを公式に公開していて、API で引けます。
curl -s https://api.github.com/meta | grep -A3 ssh_key_fingerprints
# "ssh_key_fingerprints": {
# "SHA256_ECDSA": "p2QAMXNIC1TJYWeIOttrVc98/R1BUFWu3/LiyKgUfQM",
# "SHA256_ED25519": "+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU",
# "SHA256_RSA": "uNiVztksCsDhcc0u9e8BujQXVUpKZIDTMczCvj3tD2s"
警告の ED25519 のフィンガープリントと、API の SHA256_ED25519 が一致しました。一致を確かめたら、yes と打つ代わりにフィンガープリントそのものを貼れます。ssh が照合して、違っていれば拒みます。
Are you sure you want to continue connecting (yes/no/[fingerprint])? SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU
Warning: Permanently added 'github.com' (ED25519) to the list of known hosts.
Hi kiyotd! You've successfully authenticated, but GitHub does not provide shell access.
Permanently added。記録先が ~/.ssh/known_hosts(今回は /tmp/kh)で、中身を見ると見覚えのある形をしています。
cat /tmp/kh
# github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLs...
authorized_keys と同じ1行形式の公開鍵が、向きを変えて置かれています。サーバーがあなたの公開鍵を控えるのが authorized_keys、あなたがサーバーの公開鍵を控えるのが known_hosts。2回目以降に警告が出ないのは、この控えと突き合わせて一致するからです。そして一致しなかったときの警告が、SSH でいちばん派手な画面になります。
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
控えと違う公開鍵を差し出す相手がいる。サーバーが鍵を作り直したのか、途中に誰かが割り込んでいるのか、ssh には区別がつかないので、接続を止めて人間に判断を返します。第5回でブラウザが全画面の警告を出したのと同じ立場です。違いは、名簿の管理人があなた自身であること。サーバーの移転や再構築で鍵が変わったと分かっているなら、ssh-keygen -R ホスト名 で古い行を消して初回をやり直します。心当たりがなければ、消してはいけない場面です。
サーバーが鍵を作り直したのか、途中に誰かが割り込んだのか、ssh には区別がつかない。接続を止めて、人間に判断を返す。
SFTP は、同じ接続の中を通る
SFTP は名前が FTP に似ていますが、別物です。FTP を SSH で包んだものでもなく、SSH の接続の中でファイルをやり取りする、SSH 側の機能です。なので鍵も警告も known_hosts も、ここまでの話がそのまま使えます。
echo exit | sftp -v xserver 2>&1 | grep -E "Offering|Authenticated"
# debug1: Offering public key: /Users/kiyotd/.ssh/id_rsa RSA SHA256:wYziAa8txTFF/oncHJ4/18VxvC3k7WCfioS4fZ+WVWQ explicit
# Authenticated to torigoe.xsrv.jp ([162.43.122.131]:10022) using "publickey".
ssh と同じ鍵が、同じ手順で通りました。GUI の FTP クライアントで SFTP 接続するときに「秘密鍵ファイル」を指定する欄があるのは、同じ鍵を同じ用途で使っているからです。当ブログのデプロイも、rsync が SSH の中を通る形で、この鍵1本で動いています。
ついでに整理しておくと、FTPS は FTP を第6回の TLS で包んだもので、証明書の世界の住人です。SFTP は SSH の世界の住人で、今日の鍵と known_hosts の世界。名前は一字違いで、土台がまるごと違います。
FTP を SSH で包んだものではなく、SSH 側の機能。鍵も警告も known_hosts もそのまま。名前が似ている FTPS は、土台がまるごと違う。
注意点
openssl は OpenSSH の秘密鍵を読めない
openssl pkey -in ~/.ssh/lab_ed25519 と打つと、unsupported で断られます。OpenSSH の秘密鍵は BEGIN OPENSSH PRIVATE KEY で始まる独自形式で、openssl の知らない封筒です。壊れているのではなく、そういう仕様です。読めなくて困る場面もありません。秘密鍵を openssl に渡す必要が出てきたら、第3回の「送らない・見せない」を思い出して一度立ち止まってください。
RSA の公開鍵なら、ssh-keygen -e -m PKCS8 -f ~/.ssh/id_rsa.pub で第3回と同じ PEM に変換でき、そのまま openssl pkey -pubin で読めます。手元の OpenSSH 10.2 では、この変換が Ed25519 に未対応でした。本文で封筒を手で足したのはそのためです。
yes と打つ前に、フィンガープリントを引く先を持っておく
GitHub のようにフィンガープリントを公開しているサービスは、API かドキュメントで照合できます。自分で立てたサーバーなら、サーバー側で ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub を打てばフィンガープリントが出るので、初回接続の前に控えておきます。レンタルサーバーは管理画面や案内に載っていることがあります。どこにも見つからなければ、せめて初回接続は自宅や社内の信頼できる回線から行い、公共の Wi-Fi で控えを作らないようにします。
秘密鍵にはパスフレーズを付け、agent に預ける
第3回の -aes256 と同じ役割を、ssh-keygen のパスフレーズが担います。ファイルが盗まれたときの最後の砦です。毎回打つのが面倒なら ssh-agent に預けます。macOS では ssh-add --apple-use-keychain ~/.ssh/lab_ed25519 でパスフレーズをキーチェーンに保存できます。ファイルの権限も第3回と同じで、他人が読める秘密鍵を ssh は使ってくれません。chmod 600 です。
鍵の種類は Ed25519 で作る
新しく作るなら -t ed25519 です。第3回の注意点に書いた楕円曲線の系統で、鍵が短く、速く、OpenSSH 9.5 以降は ssh-keygen の既定の種類にもなっています。古い手順書の -t rsa も動きますが、新規で選ぶ理由は減っています。当ブログの鍵が RSA 4096 なのは、数年前に作った名残です。
サーバーに秘密鍵を作らせない
レンタルサーバーの管理画面には、サーバー側で鍵ペアを作って秘密鍵をダウンロードさせる機能があることがあります。動きはしますが、秘密鍵が手元の外で生まれ、回線を通って届いている。第3回の原則から言えば、手元で作って公開鍵だけを登録する今日の手順が筋です。
SSH にも認証局方式はある
OpenSSH には第5回と同じ認証局の方式(SSH 証明書)も用意されていて、サーバーの数が多い組織で使われます。個人や小さなチームではまず出てこないので、今日は known_hosts の話に絞りました。
鍵ペアは2組、署名は2方向。サーバーがあなたを確かめるのが第4回、あなたがサーバーを確かめるのが第5回で、SSH は後者の保証人をあなた自身にしました。初回接続の警告は、その保証人に回ってきた宿題です。フィンガープリントを引いて、見比べて、それから yes。本編で手作りした部品は、ログインのたびに、あなたのターミナルで一往復しています。
本編は第0回からどうぞ。
