SSHでRSA鍵なのに接続できない原因と対処|「no matching host key type found. Their offer: ssh-rsa」の意味・ssh-rsa(SHA-1署名)とrsa-sha2-256/512の違い・+ssh-rsaの書き方

SSHでRSA鍵なのに接続できない原因と対処

こんにちは、おうどんです🍜

新人さん「古いサーバーに SSH でつながりません!」

おうどん「エラーはなんて?」

新人さん「Their offer: ssh-rsa って。だから RSA 鍵を作り直しました」

おうどん「……何で作り直したの?」

新人さん「RSA で」

(会話は説明用の架空の場面です)

同じ材料で、同じ打ち方をして、同じうどんができた。 そりゃ、同じ結果になります。

でもこれ、おうどんも迷ったことがある疑問なんです。 エラーに ssh-rsa と書いてある。手元の鍵も RSA。じゃあ鍵が悪いんだろう、と思うのが自然ですよね。

……ところが、ここでいう ssh-rsa は、鍵の種類ではなく、ハンコの押し方の名前なんです。

うどん屋の出前で言うと、こんな感じ。

受付さん「受け取りのハンコ、その朱肉は古いのでお受けできません」

出前さん「じゃあ新しいハンコを彫ってきます!」

受付さん「いえ、ハンコはそのままでいいんです。朱肉だけ替えてください」

……彫り直す前に、朱肉の話を聞いてあげてほしい。

今回は、RSA 鍵なのに SSH がつながらないときに、エラーのどこを読み、何を確かめ、どう直すかを、実際にコマンドを動かしながら整理します。

最初に、初心者向けのチェックリストです。

  • エラー文の Their offer の後ろを読む。ssh-rsa なら、鍵ではなく「署名の方式」の食い違い
  • 古いサーバーへの一時しのぎの +ssh-rsa は、~/.ssh/config の、そのサーバーの Host ブロックだけに書く
  • 根本対処は、古いサーバー側を rsa-sha2 に対応した版へ上げること(鍵の作り直しではない)
目次

RSA鍵とssh-rsaは、同じ名前の別物

ハンコ(鍵)と、押し方(署名方式)は別の話です。 同じ RSA のハンコでも、押し方は3通りあります。

まず用語を3つだけ。

  • 鍵:自分だけが持つ秘密鍵と、相手に渡す公開鍵のペア。ハンコそのもの
  • 署名:秘密鍵を使って「本人です」という証拠を作ること。ハンコを押すこと
  • ハッシュ:データを短い指紋にまとめる計算。SHA-1 や SHA-256 などの種類がある。署名はこの指紋に対して押す

今回の検証環境(Git for Windows に入っている OpenSSH 9.7p1、bash 5.2)で、RSA 鍵を作ってみます。

ssh -V
ssh-keygen -q -t rsa -b 3072 -N "" -C lab-key -f ./lab_rsa
cut -c1-20 lab_rsa.pub
ssh-keygen -l -f lab_rsa.pub
OpenSSH_9.7p1, OpenSSL 3.2.1 30 Jan 2024
ssh-rsa AAAAB3NzaC1y
3072 SHA256:jMtEdk9aFSQTnTLWiCBSCMYB3CaiCLslwtEyoGE7RAc lab-key (RSA)

(1つずつ実行した結果をまとめて並べています。指紋は鍵を作るたびに変わります)

公開鍵ファイルの先頭は ssh-rsa。 ここで多くの人が「あ、ssh-rsa って自分の鍵のことだ」と思うわけです。

新人さん「ほら、やっぱり自分の鍵が ssh-rsa じゃないですか」

おうどん「うん。表札にはそう書いてある」

……表札と、押し方の名前が、同姓同名なんです。

次に、この SSH が知っている「署名の方式」の中から、RSA のものだけを出してみます。

ssh -Q sig | grep rsa
ssh-rsa
rsa-sha2-256
rsa-sha2-512

RSA の押し方は3つ。

署名方式の名前使うハッシュ使う鍵
ssh-rsaSHA-1RSA 鍵
rsa-sha2-256SHA-256同じ RSA 鍵
rsa-sha2-512SHA-512同じ RSA 鍵

RFC 8332(rsa-sha2-256・rsa-sha2-512 を定めた規格)によると、新しい2つの方式は、既存の ssh-rsa の公開鍵の書式をそのまま使い、書式の中の ssh-rsa という文字列も変えません。だから、今ある RSA 鍵を作り直さずに使えて、信頼済みの鍵の指紋にも影響しない、と書かれています。

新人さん「じゃあ、表札を rsa-sha2-512 に書き換えれば……」

おうどん「表札は ssh-rsa のまま。それが規格の決まり」

……表札をいじると、むしろ誰の家か分からなくなる。

そして、ここが本題です。 OpenSSH 8.8 のリリースノートには、このリリースから SHA-1 を使う RSA 署名を既定で無効にした、と書かれています。理由は、SHA-1 が暗号として破られているから。RFC 8332 の RSA/SHA-256/512 署名には 7.2 から対応している、ともあります。

つまり、新しい OpenSSH は「RSA 鍵」は普通に使えるけれど、「ssh-rsa という押し方」は、言われない限りしません。

受付さん「SHA-1 の朱肉は、うちでは使っていません」

新人さん「シャ……シャチハタ不可みたいなもんですか」

……シャしか合っていない。でも、気持ちはだいたい合っています。

同じリリースノートには、ほとんどの人にとってこの変更は見えないし、ssh-rsa の鍵を置き換える必要もない、とも書かれています。既存の RSA 鍵は、できる場面では自動で強いほうの方式を使うからです。 困るのは、相手が古くて、新しい朱肉を知らないときだけ、というわけです。

エラー文の「Their offer」で、どこで止まったかを読む

エラー文は、「どの話し合いで、相手が何を出してきたか」を教えてくれる伝票です。 最後の Their offer を読めば、どこで止まったかが分かります。

本物の古いサーバーは用意できないので、今回は Python で「古いサーバーのふり」をするニセサーバーを書きました。最初のあいさつで OpenSSH_5.3 と名乗り、使える方式の一覧(KEXINIT というメッセージ)を返すだけのものです。ログインはできません。コードは後半の上級者向けの章に載せています。

まず、ホスト鍵の方式として ssh-rsa だけを出すサーバーにつないでみます。

python fake_old_server.py ssh-rsa

(別のターミナルで起動しておきます)

ssh -F none -o BatchMode=yes -o UserKnownHostsFile=/dev/null -p 2222 lab@127.0.0.1 true
Unable to negotiate with 127.0.0.1 port 2222: no matching host key type found. Their offer: ssh-rsa

(今回の検証環境で実行した結果。終了コードは 255 でした。-F none は自分の設定ファイルを読まない、UserKnownHostsFile=/dev/null は既知のホストの記録を汚さないための指定です)

出ました。 これが、検索でいちばん見かけるあのエラーです。

注目は no matching host key type。 ホスト鍵は、あなたの鍵ではありません。サーバーが「本物の店です」と名乗るために押すハンコです。

新人さん「つまり、断られたのは自分の鍵……」

おうどん「じゃなくて、店の看板のハンコ。しかも、断ったのはこっちの ssh」

新人さん「入店拒否したのは客のほう!?」

……そうなんです。古い店の看板のハンコの押し方を、新しいお客さんが「それじゃ本物か確かめられません」と断っている。この時点では、あなたの RSA 鍵はまだ一度も出番がありません。

同じニセサーバーで、鍵交換(暗号の合言葉を作る段階)の方式を古いものだけにすると、文面が変わります。

python fake_old_server.py ssh-rsa diffie-hellman-group1-sha1
ssh -F none -o BatchMode=yes -o UserKnownHostsFile=/dev/null -p 2222 lab@127.0.0.1 true
Unable to negotiate with 127.0.0.1 port 2222: no matching key exchange method found. Their offer: diffie-hellman-group1-sha1

(同じ環境で実行した結果。終了コードは 255)

まとめると、こうなります。

エラーの文面食い違っているものクライアント側の設定名今回の再現
no matching host key type found. Their offer: ssh-rsaサーバーのホスト鍵の署名方式HostKeyAlgorithms再現した
no matching key exchange method found. Their offer: …鍵交換の方式KexAlgorithms再現した
鍵交換は通ったのに、公開鍵の認証で RSA 鍵が使われずに断られるユーザー認証の署名方式PubkeyAcceptedAlgorithms再現していない

3行目は、今回のニセサーバーでは鍵交換の先まで進めないので、再現できていません。OpenSSH 8.8 のリリースノートが、ホスト鍵(HostkeyAlgorithms)とユーザー認証(PubkeyAcceptedAlgorithms)の両方で ssh-rsa を足す例を出しているのは、この2か所で食い違いが起こりうるからです。

新人さん「じゃあ、エラー文をそのままググればいいんですね」

おうどん「その前に、Their offer をメモして」

……伝票を捨ててから「何を注文したっけ」と言わないために。

今日つなぐための対処:Hostを限定して+ssh-rsa

今日どうしてもつなぎたいなら、そのサーバーにだけ、古い朱肉を使ってよいと伝えます。 店全体の朱肉を古くするのではなく、その受付に行くときだけ、です。

~/.ssh/config に、こう書きます(old-host と 192.0.2.10 は説明用の例です)。

Host old-host
    HostName 192.0.2.10
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa

これは、OpenSSH 8.8 のリリースノートにある一時しのぎの書き方と同じ形です。 先頭の + は、既定の一覧に足すという意味。+ を付けずに ssh-rsa だけを書くと、既定の一覧がまるごと置き換わります(ssh_config のマニュアル)。

新人さん「+ を付け忘れたら?」

おうどん「新しい朱肉を全部捨てて、古い朱肉1個で営業する店になる」

……一時しのぎのつもりが、全面改装。

書いたら、効いているかを ssh -G で確かめます。-G は、設定ファイルを全部評価した結果を表示して、接続せずに終わるオプションです。

ssh -G -F none -o HostKeyAlgorithms=+ssh-rsa old-host | grep '^hostkeyalgorithms' | tr ',' '\n' | tail -n 3
rsa-sha2-512
rsa-sha2-256
ssh-rsa

(今回の検証環境で実行した結果。標準入力が端末でない状態で実行したため出た Pseudo-terminal will not be allocated の1行は省いています。ここでは設定ファイルの代わりに -o で同じ指定をしています)

ssh-rsa が一覧の最後に足されました。 最後なので、相手が新しい方式を出してくれるなら、そちらが優先されます。

ニセサーバーにも、この指定でつないでみます。

ssh -F none -o BatchMode=yes -o UserKnownHostsFile=/dev/null -p 2222 -v -o HostKeyAlgorithms=+ssh-rsa lab@127.0.0.1 true
debug1: Remote protocol version 2.0, remote software version OpenSSH_5.3
debug1: SSH2_MSG_KEXINIT sent
debug1: SSH2_MSG_KEXINIT received
debug1: kex: algorithm: diffie-hellman-group14-sha256
debug1: kex: host key algorithm: ssh-rsa
debug1: kex: server->client cipher: aes128-ctr MAC: hmac-sha2-256 compression: none
debug1: kex: client->server cipher: aes128-ctr MAC: hmac-sha2-256 compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
Connection closed by 127.0.0.1 port 2222

(実行結果から関係する行だけ抜き出しています。最後の Connection closed は、ニセサーバーがここで黙って切っているためで、本物のサーバーなら鍵交換が続きます)

host key algorithm: ssh-rsa。 さっき (no match) だったところで、話がまとまりました。

ニセサーバー「OpenSSH_5.3 です。朱肉はこれしかありません」

ssh「今日だけ、その朱肉でいいですよ」

ニセサーバー「ありがとうございます。……では、ここで失礼します」

……ニセモノなので、合意したところで帰ってしまう。本物はちゃんと続きます。

ただし、これはあくまで一時しのぎです。 リリースノートも、RSA/SHA-1 を有効にするのは古い実装を更新するまでのつなぎにとどめるよう勧めています。全体の既定(Host *)には書かない。これだけは守ってください。

🔰 ここまで読めば今日から困らない

  • Their offer: ssh-rsa は、鍵の種類ではなく署名方式(SHA-1 の押し方)の話。鍵の作り直しでは直らない
  • host key type のエラーは、サーバーのホスト鍵の話。自分の鍵はまだ使われていない
  • 今日つなぐなら、そのサーバーの Host ブロックに HostKeyAlgorithms +ssh-rsa(ユーザー認証でも食い違うなら PubkeyAcceptedAlgorithms +ssh-rsa)。+ を忘れない
  • 根本対処は、古いサーバーを rsa-sha2 に対応した版へ上げること

新人さん「4行だけなら、ハンコの裏に彫っておけますね」

おうどん「彫るたびに鍵が変わる人の発想だね」

……メモは、紙に書いてください。

新人さん「じゃあ、+ssh-rsa を全部のサーバーに書いておけば、もう悩まないのでは」

おうどん「悩まなくなるのは、悪い人のほうもね」

……全部に書くと、暗号として破られている SHA-1 の押し方を、どの相手にも認めることになる。だから、そのサーバーだけです。

ここから先は中級者向け。読み飛ばしてもOKです。

「入っている」と「使う」は別:-Qと-Gで確かめる

ssh -Q は「この ssh が知っている方式」、ssh -G は「今回の接続に向けて設定された許可一覧」です。実際に選ばれる方式は、接続時に相手と照らし合わせて決まります。 道具箱に入っていることと、今日の出前に持っていくことは、別なんです。

ssh -Q PubkeyAcceptedAlgorithms | grep -c .
ssh -G -F none old-host | grep '^pubkeyacceptedalgorithms' | tr ',' '\n' | grep rsa
21
rsa-sha2-512-cert-v01@openssh.com
rsa-sha2-256-cert-v01@openssh.com
rsa-sha2-512
rsa-sha2-256

(1行目は1つ目のコマンド、2行目以降は2つ目のコマンドの結果です。2つ目では Pseudo-terminal の1行を省いています)

-Q では 21 種類。その中には ssh-rsa も入っています(ssh -Q sig でも出ていましたね)。 なのに -G で見ると、既定で使う一覧の RSA は rsa-sha2 の2系統だけ。ssh-rsa はいません。

新人さん「入ってるなら、使ってくれればいいのに」

ssh「持ってます。でも、言われないと出しません」

……冷蔵庫に賞味期限切れの朱肉が入っている。捨ててはいないけど、出さない。

ここで -Q だけ見て「ssh-rsa に対応しているから大丈夫」と判断すると、外れます。 判断に使うのは -G の結果です。

もう1つ、スクリプトで SSH を使うときの落とし穴。-q(静かモード)を付けると、さっきのエラーが出ません。

ssh -F none -o BatchMode=yes -o UserKnownHostsFile=/dev/null -p 2222 -q lab@127.0.0.1 true

今回の検証環境で、ssh-rsa だけを出すニセサーバーにつないだ結果は、画面に何も表示されず、終了コードが 255 になっただけでした。

ジョブ「SSH、終わりました」

おうどん「なんて言ってた?」

ジョブ「何も」

……静かすぎて、つながらなかったことすら伝わらない。

-q を付けているジョブが急に失敗し始めたら、まず -q を外して1回手で動かしてみましょう。今回の検証では、つながらなかったときの終了コードは、-q の有無にかかわらず 255 でした。

書いたのに効かない:最初に決まった値が勝つ

設定ファイルに +ssh-rsa を書いたのに、つながらない。 そんなときは、同じ項目が、もっと前で決まっていないかを疑います。ssh の設定は、早い者勝ちなんです。

ssh_config のマニュアルには、こう書かれています。

  • 設定は、コマンドラインのオプション → ユーザーの ~/.ssh/config → システムの /etc/ssh/ssh_config の順に読まれる
  • 特に断りがない限り、各項目は最初に指定された値が使われる
  • だから、ホストごとの設定はファイルの先頭近くに、全体の既定は最後に書く

試しに、Host * を先頭に置いた設定ファイルを作りました。

Host *
    PubkeyAcceptedAlgorithms rsa-sha2-512

Host old-host
    HostName 192.0.2.10
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa

(config_bad という名前で保存しました)

ssh -G -F ./config_bad old-host | grep '^pubkeyacceptedalgorithms'
pubkeyacceptedalgorithms rsa-sha2-512

(Pseudo-terminal の1行は省いています)

old-host に書いた +ssh-rsa は、無視されました。 先に Host * で PubkeyAcceptedAlgorithms が決まってしまったからです。

old-host のブロック「ssh-rsa を足してください!」

ssh「すみません、その項目はさっき Host * さんで締め切りました」

……受付は、先着1名さま。

一方、HostKeyAlgorithms は Host * で決めていないので、old-host の +ssh-rsa がちゃんと効いていました(ssh -G の hostkeyalgorithms の行の最後に ssh-rsa が入っていました)。 同じブロックに書いた2行のうち、片方だけ効く。これ、知らないとかなり悩みます。

順番を入れ替えた config_good(old-host を先、Host * を最後)ではこうなります。

ssh -G -F ./config_good old-host | grep '^pubkeyacceptedalgorithms' | tr ',' '\n' | tail -n 3
ssh -G -F ./config_good new-host | grep '^pubkeyacceptedalgorithms'
rsa-sha2-512
rsa-sha2-256
ssh-rsa
pubkeyacceptedalgorithms rsa-sha2-512

(1つ目のコマンドの結果が最初の3行、2つ目が最後の1行です。Pseudo-terminal の行は省いています)

old-host には ssh-rsa が足され、それ以外のホスト(new-host)には Host * の設定が効きました。

新人さん「じゃあ、old-host のブロックを3回書いておけば、どれか効きますね」

おうどん「最初の1回しか効かないよ」

……おみくじは、何回引いても最初の1枚が本番です。

/etc/ssh/ssh_config やそこから読み込まれるファイルにも同じ項目があるかもしれません。でも順番では ~/.ssh/config のほうが先なので、自分のファイルに書いたほうが勝ちます。それでも効かないときは、ssh -G ホスト名 の結果が「最終的な答え」です。推理するより、まず -G を見ましょう。

逆向き:新しいサーバーに古いクライアントが入れないとき

ここまでは「新しい ssh から古いサーバーへ」でした。 逆に、新しい OS のサーバーへ、古いクライアントや古い機器から入れない、ということも起きます。こちらは店の受付が新しくなったパターンです。

Red Hat の RHEL 9 への移行に関する考慮事項には、次のようなことが書かれています。

  • RHEL 9 では、DEFAULT のシステム全体の暗号化ポリシーで、署名への SHA-1 の使用が制限されている。HMAC を除き、SSH でも SHA-1 は許可されない
  • 既定では、SSH で RHEL 9 から RHEL 6 などの古いシステムへ、また古いシステムから RHEL 9 へ接続できない
  • どうしても SHA-1 の署名が必要なら update-crypto-policies --set DEFAULT:SHA1 で再有効化できる。LEGACY ポリシーへの切り替えもできるが、LEGACY はセキュアではない他の多くのアルゴリズムも有効にする

この方針が sshd 向けに書き出されているのは、次のファイルです。

/etc/crypto-policies/back-ends/opensshserver.config

RHEL 9 の sshd は、/etc/ssh/sshd_config.d/50-redhat.conf の中の Include で、このファイルを読み込んでいます。中身は HostKeyAlgorithms や PubkeyAcceptedAlgorithms などの一覧です。crypto-policies の開発元が公開している生成結果の見本では、DEFAULT の一覧に ssh-rsa は入っておらず、DEFAULT:SHA1 と LEGACY では入っています。

このファイルに ssh-rsa があるかだけで、接続できるかは決まりません。まず、現在のポリシーと、sshd が設定を読み込んだときの許可一覧を確認します。

update-crypto-policies --show
sudo /usr/sbin/sshd -T | grep -i -e '^hostkeyalgorithms' -e '^pubkeyacceptedalgorithms'

sshd -T は、設定ファイルを読み込んで妥当性を確認し、実効設定を表示して終了するコマンドです。クライアントの ssh -G に近い確認方法ですね。

ただし、稼働中の sshd に問い合わせるわけではありません。起動時の -f(別の設定ファイル)や -o(設定値の指定)があれば、確認にも同じ指定が必要です。ユーザーや接続元で設定を変える Match がある場合は、-C user=ユーザー名,addr=接続元IP,host=接続元ホスト名 で条件を指定して確認します。ファイルを変更しただけで、稼働中の sshd にも反映されたとは判断しないでください。

また、一覧は「設定上、許可する方式」です。実際に選ばれる方式や認証の成否は、相手の対応、利用できるホスト鍵、ユーザーの公開鍵などにも左右されます。ssh -vvv とサーバー側のログを合わせて、ホスト鍵の段階かユーザー認証の段階かを切り分けましょう。

新人さん「じゃあ、このファイルに ssh-rsa を書き足せば……」

おうどん「それは本社の通達に、手書きで追記するやつ」

……次の通達が来たら、手書きの分は消えます。

opensshserver.config は update-crypto-policies が管理するファイルなので、恒久対策として直接は書き換えません。ポリシーの再適用やシステム更新で、直接編集した内容が上書きされる可能性があります。また、50-redhat.conf の冒頭には、暗号まわりの設定(Ciphers や MACs など)はこの Include より後に書いても効かない、という注意書きがあります。RHEL 9 でポリシー側の指定を上書きする方法として、Red Hat は /etc/ssh/sshd_config.d/ に 49-crypto-policy-override.conf など、50-redhat.conf より前に読まれるドロップインファイルを用意する方法を説明しています。公式の解説はこちら。

この読み込み方は RHEL 9 の説明です。RHEL 8 や Oracle Linux、ベンダー製品では、起動時の指定や独自ポリシーなど構成が異なる場合があります。同じファイル名があっても、RHEL 9 の手順をそのまま適用せず、対象環境の資料と sshd の起動方法を確認してください。

(RHEL は今回の検証環境にないため、このパートは未実行です。上の内容は Red Hat のドキュメントと、RHEL 9 系の openssh・crypto-policies パッケージのソースで確認した範囲です)

受付さん「本社の方針で、SHA-1 の朱肉はお断りになりました」

古いお客さん「じゃあ、本社の方針ごと昔に戻してください」

……1人のために、全店の鍵を古くするのは、やめておきましょう。

新人さん「LEGACY って、伝統の味みたいでいい名前ですね」

おうどん「秘伝のだしじゃなくて、昔の鍵穴だよ」

……老舗っぽい名前ほど、中身は「古い方式もいろいろ通します」。

システム全体の暗号化ポリシーは、そのポリシーに従う SSH・TLS などのアプリケーションにまとめて効きます。古いクライアント1台のためにサーバー全体を緩めるより、まずはクライアント側を新しくできないかを考えるのが順番です。どうしても緩めるなら、何に効くのかを Red Hat のセキュリティ強化のドキュメントで確認し、元に戻す手順も一緒に用意してからにしてください。

切り分けチェックリストと、完成形の設定

迷ったら、この順番で見ていきます。 伝票(エラー文)→ 相手の名乗り(-v)→ 自分の手持ち(-G)→ 直す、の4段です。

  1. エラー文を全部メモする。Their offer の後ろに何が書いてあるか
  2. ssh -v で1回つなぐ。見る行は次の3つ
    • remote software version:相手の名乗り(例: OpenSSH_5.3)
    • kex: host key algorithm::ホスト鍵の方式が決まったか、(no match) か
    • 鍵交換のあとに、公開鍵の認証でどの鍵をどう出したか
  3. ssh -G ホスト名 で、実際に使う一覧を見る。+ssh-rsa を書いたのに入っていなければ、もっと前で決まっている
  4. 直し方を選ぶ
    • 相手を新しくできる → 相手を rsa-sha2 対応の版へ。鍵はそのままでよい
    • 今日だけつなぎたい → そのホストの Host ブロックに +ssh-rsa
    • 相手が古いまま長く残る → 移行の計画を立てる。+ssh-rsa を恒久対策にしない

新人さん「4番の『移行の計画』って、誰が立てるんですか」

おうどん「……見つけた人が、まず1行メモを書く」

……計画は、たいてい伝票の裏のメモから始まる。

完成形の ~/.ssh/config は、こうなります(ホスト名・IP・ユーザー名は説明用の例です)。

# 古いサーバーだけの例外。先頭に置く(最初に決まった値が勝つため)
# 理由: OpenSSH 5.x 系で rsa-sha2 に未対応。更新までの一時しのぎ
Host old-host
    HostName 192.0.2.10
    User opsuser
    HostKeyAlgorithms +ssh-rsa
    PubkeyAcceptedAlgorithms +ssh-rsa

# 全体の既定は最後に
Host *
    IdentitiesOnly yes

(この設定で実在のサーバーへ接続はしていません。old-host のブロックの効き方は、上の ssh -G で確認したとおりです)

例外の理由をコメントで残しておくと、未来のおうどんが「これ、まだ要る?」と聞けます。

未来のおうどん「この +ssh-rsa、何のために書いたの?」

過去のおうどん「(コメントなし)」

……コメントのない例外は、だいたい誰にも消されずに、何年も生き残ります。。。

ここから先は上級者向け。読み飛ばしてもOKです。

SSHの話し合いの中身:KEXINITとext-info-c

SSH の最初の話し合いは、お互いが「使える方式の一覧」を一斉に見せ合うところから始まります。 その一覧を運ぶメッセージが KEXINIT です。今回のエラーは、この一覧の照らし合わせで「重なりがなかった」ということなんです。

今回使ったニセサーバーのコードです。Python 3.13 の標準ライブラリだけで書いています。

"""あいさつ(バージョン文字列)と KEXINIT だけを返す、検証用のニセSSHサーバー。
使い方: python fake_old_server.py <ホスト鍵の方式> [鍵交換の方式]
ログインはできません。鍵交換の「最初の提案」だけを再現します。"""
import os, socket, struct, sys

hostkey = sys.argv[1] if len(sys.argv) > 1 else "ssh-rsa"
kex = sys.argv[2] if len(sys.argv) > 2 else "diffie-hellman-group14-sha256,diffie-hellman-group14-sha1"

def name_list(s):
    b = s.encode("ascii")
    return struct.pack(">I", len(b)) + b

def kexinit():
    payload = bytes([20]) + os.urandom(16)              # 20 = SSH_MSG_KEXINIT、16バイトのcookie
    for s in (kex, hostkey, "aes128-ctr", "aes128-ctr", "hmac-sha2-256", "hmac-sha2-256",
              "none", "none", "", ""):
        payload += name_list(s)
    payload += b"\x00" + struct.pack(">I", 0)          # first_kex_follows=0、予約=0
    pad = 8 - (5 + len(payload)) % 8
    if pad < 4:
        pad += 8
    return struct.pack(">IB", 1 + len(payload) + pad, pad) + payload + b"\x00" * pad

with socket.create_server(("127.0.0.1", 2222)) as srv:
    print("listening 127.0.0.1:2222", flush=True)
    conn, _ = srv.accept()
    with conn:
        conn.settimeout(10)
        conn.sendall(b"SSH-2.0-OpenSSH_5.3\r\n")       # 古いサーバーのふり
        data = b""
        while b"\n" not in data:
            chunk = conn.recv(1024)
            if not chunk:
                break
            data += chunk
        print("client:", data.split(b"\r\n")[0].decode(), flush=True)
        conn.sendall(kexinit())
        try:
            while conn.recv(4096):
                pass
        except OSError:
            pass
listening 127.0.0.1:2222
client: SSH-2.0-OpenSSH_9.7

(python fake_old_server.py ssh-rsa で起動し、ssh からつないだときのニセサーバー側の表示です)

本物の OpenSSH 5.3 ではありません。名乗りと一覧を返すだけの、看板だけのお店です。 だから、ここで確かめられるのは「クライアントが一覧をどう照らし合わせるか」までです。

ニセサーバー「OpenSSH_5.3 です」

ssh「お若いですね。……あ、いや、お年を召してますね」

……名乗っただけで年齢がバレる。

-vv を付けると、お互いの一覧がそのまま見えます。

ssh -F none -o BatchMode=yes -o UserKnownHostsFile=/dev/null -p 2222 -vv lab@127.0.0.1 true
debug1: Remote protocol version 2.0, remote software version OpenSSH_5.3
debug1: compat_banner: match: OpenSSH_5.3 pat OpenSSH_5* compat 0x0c000002
debug2: local client KEXINIT proposal
debug2: KEX algorithms: sntrup761x25519-sha512@openssh.com,curve25519-sha256,curve25519-sha256@libssh.org,ecdh-sha2-nistp256,ecdh-sha2-nistp384,ecdh-sha2-nistp521,diffie-hellman-group-exchange-sha256,diffie-hellman-group16-sha512,diffie-hellman-group18-sha512,diffie-hellman-group14-sha256,ext-info-c,kex-strict-c-v00@openssh.com
debug2: host key algorithms: ssh-ed25519-cert-v01@openssh.com,ecdsa-sha2-nistp256-cert-v01@openssh.com,ecdsa-sha2-nistp384-cert-v01@openssh.com,ecdsa-sha2-nistp521-cert-v01@openssh.com,sk-ssh-ed25519-cert-v01@openssh.com,sk-ecdsa-sha2-nistp256-cert-v01@openssh.com,rsa-sha2-512-cert-v01@openssh.com,rsa-sha2-256-cert-v01@openssh.com,ssh-ed25519,ecdsa-sha2-nistp256,ecdsa-sha2-nistp384,ecdsa-sha2-nistp521,sk-ssh-ed25519@openssh.com,sk-ecdsa-sha2-nistp256@openssh.com,rsa-sha2-512,rsa-sha2-256
debug2: peer server KEXINIT proposal
debug2: KEX algorithms: diffie-hellman-group14-sha256,diffie-hellman-group14-sha1
debug2: host key algorithms: ssh-rsa
debug1: kex: algorithm: diffie-hellman-group14-sha256
debug1: kex: host key algorithm: (no match)
Unable to negotiate with 127.0.0.1 port 2222: no matching host key type found. Their offer: ssh-rsa

(今回の検証環境での実行結果から、関係する行だけ抜き出しています。暗号・MAC・圧縮の行などは省略)

読みどころは3つです。

1つ目。自分(local client)の host key algorithms には rsa-sha2-512 と rsa-sha2-256 があり、ssh-rsa がない。相手(peer server)は ssh-rsa だけ。重なりゼロなので (no match) です。鍵交換の方式(diffie-hellman-group14-sha256)は重なったので、そこは決まっています。

2つ目。自分の KEX algorithms の中に、ext-info-c という、鍵交換の方式ではない名前が混ざっています。 RFC 8308(SSH の拡張ネゴシエーションの規格)によると、これは「拡張情報(SSH_MSG_EXT_INFO)を受け取れます」という合図です。クライアントは ext-info-c、サーバーは ext-info-s と、別の名前を kex_algorithms に入れるので、鍵交換の方式としては決して一致せず、選ばれる方式にも影響しません。

ssh「一覧の最後に、名刺を1枚はさんでおきました」

ニセサーバー「名刺……読めません。5.3 なので」

……せっかくの名刺が、うどんの汁で読めなかったことにしておきます。

3つ目。compat_banner の行は、クライアントが相手の名乗り(OpenSSH_5.3)を見て、古い版向けの扱いをするパターンに当てはめた、という記録です。どの扱いが入るかの中身は、今回は調べていません。

server-sig-algs:ユーザー認証で「どの押し方なら受け付けるか」

ext-info-c の合図を受けたサーバーは、server-sig-algs という拡張で「ユーザー認証で処理できる公開鍵の方式の一覧」を返せます。 受付が「うちはこの朱肉なら受け付けます」と、先に貼り紙を出してくれる仕組みです。

RFC 8308 には、こう書かれています。

  • server-sig-algs は、サーバーが処理できる公開鍵アルゴリズムの一覧で、サーバーが送る
  • これがないと、クライアントはサーバーが受け付ける方式を試行錯誤で探すしかなく、サーバー側で認証の失敗として数えられ、IP の制限や警告につながることがある。RFC 8332 の作業でこの問題がはっきりした
  • server-sig-algs が来ないとき、クライアントはサーバーの対応について何も仮定してはいけない(試行錯誤で進めてよい)

RFC 8332 も、rsa-sha2 を受け付けるサーバーは RFC 8308 の server-sig-algs を実装するべきだとしています。そして、この拡張がないサーバーに対しては、クライアントが ssh-rsa に戻すことがある、とも書いています。

受付さん「貼り紙はありません。何でも押してみてください」

お客さん「じゃあ新しい朱肉で」

受付さん「ブー。失敗1回目です。3回で出禁です」

……貼り紙を出してくれない受付ほど、出禁が厳しい。

ここから言えるのは、ホスト鍵の照らし合わせ(KEXINIT)と、ユーザー認証の署名方式の決め方(server-sig-algs など)は、別の仕組みだということです。 だから、HostKeyAlgorithms と PubkeyAcceptedAlgorithms は別々に設定する項目になっていて、片方だけ足しても、もう片方で止まることがあります。

なお、server-sig-algs がない古いサーバーに対して、OpenSSH 9.7 のクライアントが実際にどの順番で何を試すかは、今回は実行して確かめられていません(ニセサーバーが鍵交換の先に進めないため)。ここは断定しないでおきます。

新人さん「じゃあ、そこは推測なんですね」

おうどん「うん。推測には『推測』のハンコを押しておく」

……今日いちばん正しいハンコの使い方かもしれない。

同じRSAのホスト鍵でも、rsa-sha2で押せば通る

最後に、根本対処の話です。 サーバーが同じ RSA のホスト鍵のまま、押し方として rsa-sha2-256 も出せるなら、新しい ssh は既定のままでつながります。

ニセサーバーに、rsa-sha2-256 と ssh-rsa の両方を出させてみます。

python fake_old_server.py rsa-sha2-256,ssh-rsa
ssh -F none -o BatchMode=yes -o UserKnownHostsFile=/dev/null -p 2222 -v lab@127.0.0.1 true
debug1: kex: algorithm: diffie-hellman-group14-sha256
debug1: kex: host key algorithm: rsa-sha2-256
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
Connection closed by 127.0.0.1 port 2222

(実行結果から関係する行だけ抜き出しています。+ssh-rsa は付けていません。Connection closed はニセサーバーが切っているためです)

+ssh-rsa なしで、host key algorithm: rsa-sha2-256。 ハンコ(RSA 鍵)はそのまま、朱肉(署名方式)だけ新しくすれば通る、ということです。

しかも RFC 8332 によると、rsa-sha2 は ssh-rsa と同じ公開鍵の書式を使うので、信頼済みの鍵の指紋には影響しません。サーバーを新しくしても、ホスト鍵そのものを変えなければ、指紋の確認をやり直す理由にはなりません。

新人さん「じゃあ、新しい鍵を彫った僕は……」

おうどん「予備のハンコができた、と思っておこう」

……ムダではない。ただ、今日の問題とは関係がなかっただけ。

古いサーバー「朱肉を替えるには、アップデートが必要で……」

新しい ssh「待ちます。でも、+ssh-rsa で待つのは今日だけです」

……優しいけど、期限はきっちり切ってくる。

バージョンの目安を、OpenSSH のリリースノートで確認できた範囲でまとめます。

OpenSSH の版RSA の署名
7.2 より前rsa-sha2-256/512 に未対応(RFC 8332 の署名への対応は 7.2 から)
7.2〜8.7rsa-sha2 に対応。ssh-rsa(SHA-1)も既定で使える
8.8 以降ssh-rsa(SHA-1)の RSA 署名を既定で無効化。RSA 鍵は rsa-sha2 で使う

(この表は OpenSSH 本家の版を基準にした目安です。OS ベンダーによる機能のバックポートや暗号ポリシー、個別設定で挙動は変わります。OpenSSH 以外の SSH 実装・機器も、それぞれのベンダーの資料で確認が必要です)

リリースノートは、ssh-rsa を使い続けるより、鍵を ECDSA や Ed25519 に替えることも勧めています。 ただ、今日のエラーの直接の原因は鍵の種類ではなく相手の古さなので、まずは相手の更新から考えましょう。

まとめ:彫り直す前に、朱肉を見る

冒頭の新人さんは、RSA 鍵を RSA 鍵で作り直していました。

新人さん「作り直した鍵、どうしましょう」

おうどん「ハンコ入れに2本並べておいて」

新人さん「どっちが本物か分からなくなりました」

……ハンコの管理が、一番セキュリティに効く。使わない鍵は、サーバーの authorized_keys からも外しておきましょう。

今回分かったことを並べると、

  • Their offer: ssh-rsa の ssh-rsa は、署名方式の名前(SHA-1 の押し方)。公開鍵の書式名と同姓同名
  • host key type のエラーは、サーバーのホスト鍵の話。断っているのは自分の ssh で、自分の鍵はまだ出番がない
  • ssh -Q は持っている方式、ssh -G は設定上の許可一覧。実際に選ばれた方式は接続ログで確認
  • +ssh-rsa はそのサーバーの Host ブロックだけに。最初に決まった値が勝つので、Host * は最後に
  • -q を付けると、このエラーすら出ない
  • ホスト鍵の照らし合わせとユーザー認証の署名方式は別の仕組み
  • 相手が rsa-sha2 を出せれば、同じ RSA 鍵のまま既定でつながる

新人さん「つまり、鍵は悪くなかったんですね」

おうどん「うん。悪いのは、朱肉の賞味期限」

新人さん「……朱肉に賞味期限ってあるんですか」

……ないかもしれない。でも SHA-1 には、ありました。

SSH の ssh-rsa エラーは、鍵の問題ではなく押し方の問題。彫り直す前に、朱肉を見る。

まずは、つながっている平和な日に、自分のよく使うサーバーで ssh -G ホスト名 を1回だけ見てみてください。 自分がどんな朱肉を持ち歩いているか知っておくと、受付で慌てずにすみます🍜

参考資料

確認日:2026-10-11。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

おうどん|癒しと創作をたのしむ雑食クリエイター
写経アプリやクレイセラピー、優しい和風デザインがすき。
ZARDと刀剣と文字に癒されて、今はアプリ作ってます。

コメント

コメントする

目次