blog

開発メモ

鍵は登録したのにPermission denied (publickey)——Windowsで詰まった6つの原因

ssh -v のログでOffering public keyとServer accepts keyのどちらで拒否されたかを見れば、サーバー側か手元側かが決まることを示すアイキャッチ

同じ週に2件の受託案件で、平文FTPからSSH公開鍵認証への移行を頼まれました。「鍵を作って登録するだけ」の作業のはずが、どちらの案件でも一筋縄ではいかない詰まりに当たっています。特に厄介だったのは、サーバー側が鍵を受理した直後に拒否されるという現象でした。ここでサーバー側を疑って管理画面を何度も往復するのは、たいてい無駄足になります。実際に踏んだ詰まりと、その切り分け方を記録します。

先に結論から

Permission denied (publickey) が出たとき、管理画面を開いて登録内容を見直すのは、たいてい順番が違います。先に ssh -v のログを1行読めば、サーバー側と手元側のどちらが壊れているかが機械的に決まります。

ssh -v <接続先>  # ログの中の1行だけを見る

Offering public key の直後に拒否  →  鍵がサーバーに入っていない(サーバー側を見る
Server accepts key の後に拒否   →  鍵は登録済み(手元の秘密鍵を見る

公開鍵認証は「鍵を受け付けるか」と「署名が作れるか」の2段階に分かれています。Server accepts key は1段目を通った証拠なので、この行が出ていればサーバー側は無罪です。以下は、この見分け方に辿り着くまでに踏んだ6つの原因の記録です。

PowerShellが空文字の引数を消し、ssh-keygenが「Too many arguments」で死ぬ

鍵生成の定番コマンドをそのままPowerShellに貼ると、いきなり躓きます。

ssh-keygen -t ed25519 -f ~/.ssh/keys/mykey -N ” -C ‘mykey’
# Too many arguments.

構文は完全に正しいのに落ちます。原因はPowerShellの構文ではなく、Windowsのプロセス起動時に引数がどう渡るかにあります。-N ''(パスフレーズなし)の空文字が引数リストから消えてしまい、ネイティブのssh-keygen.exeからは-N -C mykeyのように見えます。-Nが次の-Cをパスフレーズとして食い、余った値の行き場が無くなって「引数が多い」というエラーになります。""でも''でも結果は同じで、確実なのは空文字の引数をそもそも渡さないことです。オプションを省いて対話プロンプトに任せます。

ssh-keygen -t ed25519 -f “$env:USERPROFILE\.ssh\keys\mykey”
# Enter passphrase: → 空のままEnterを2回

ただし、この回避には後述の副作用があります。

「Server accepts key」と言われた直後にPermission denied

公開鍵をホスティング会社の管理画面(SSH設定・公開鍵登録)に登録し、接続を試すと弾かれました。

user@example.sakura.ne.jp: Permission denied (publickey,gssapi-keyex,gssapi-with-mic)

ここで「登録に失敗した」「ユーザー名が違う」と疑って管理画面を往復するのは、たいてい無駄足になります。-vを付けると答えはすでに出ています。

debug1: Offering public key: …\mykey ED25519 SHA256:hJ2A… explicit
debug1: Server accepts key: …\mykey ED25519 SHA256:hJ2A… explicit
user@example.sakura.ne.jp: Permission denied (publickey,…)

「Server accepts key」は決定的な情報です。SSHの公開鍵認証は2段階で進みます。RFC 4252(SSH認証プロトコル)は、まずクライアントが署名なしのクエリで鍵の受理可否だけを確認し、サーバーがSSH_MSG_USERAUTH_PK_OKを返した後に初めて、署名付きの本申請を送る手順を定めています(RFC 4252 Section 7)。

「Server accepts key」が出るのは、この1段目を通過した証拠です。つまりauthorized_keysに鍵は入っており、ユーザー名も正しく、SSHも有効化されています。サーバー側は無罪で、壊れているのは2段目、手元の署名の作成です。ログの出方で疑うべき場所は変わります。

ログ サーバー側 疑うべき場所
Offering public keyの直後に拒否 鍵がauthorized_keysに無い 登録内容・ユーザー名・SSH有効化
Server accepts keyの後に拒否 鍵は登録済み 手元の秘密鍵(暗号化・破損・.pubとの不一致)

この案件での真因は、罠1の回避で対話プロンプトに任せた際、空Enterのつもりでパスフレーズが入ってしまっていたことでした。パスフレーズ付きの秘密鍵は、自動実行のため対話を禁止する-o BatchMode=yesのような環境だと復号できず、署名を作れません。一方で公開鍵の提示は.pubファイルを読むだけで済むので1段目は通ります。だから「受理された直後に拒否」という奇妙な形になります。

鍵が暗号化されているか調べようとすると、その調査自体がハングする

暗号化の有無を確かめる素直な手は、非対話のセッションでは使えません。

ssh-keygen -y -f $key # パスフレーズを聞きに行ったまま無応答
ssh-keygen -y -P “” -f $key # Too many arguments

-y(秘密鍵から公開鍵を導出)は-P(パスフレーズ指定)と併用できません。確実なのは、ファイルの中身を直接見ることです。OpenSSH形式の秘密鍵は、base64を剥がすと先頭に暗号方式が平文で入っています。

$b64 = ((Get-Content $key) | Where-Object { $_ -notmatch ‘—–‘ }) -join ”
$bytes = [Convert]::FromBase64String($b64)
[Text.Encoding]::ASCII.GetString($bytes[0..70]) -replace ‘[^\x20-\x7E]’,’.’
# => openssh-key-v1…..aes256-ctr….bcrypt……..

aes256-ctr + bcryptならパスフレーズ付き、none....noneなら平文です。プロンプトを一切踏まないので、自動化された文脈でも安全に判定できます。判定さえつけば対処は単純で、ssh-addで一度エージェントに載せれば以降は無入力で繋がります。

パスフレーズを付けた瞬間、Git Bash側のsshだけ死ぬ

別案件では、鍵が平文のうちはGit BashからもPowerShellからもscpが通っていたのに、パスフレーズを付与した直後からGit Bash側だけ失敗するようになりました。

scp: Connection closed
user@example: Permission denied (publickey,password).

原因は認証エージェントの経路です。Windows純正のssh-agentサービス(PowerShellのssh.exeやWinSCPが使う)と、Git Bash同梱のMSYS版OpenSSHはエージェントのソケットが別物で、素の設定では橋渡しされません。鍵が平文のうちはエージェントを介さずファイルを直読みできるため通っていて、暗号化した瞬間に「エージェントに聞くしかない」状態になって失敗します。厄介なのは、これが移行の最終工程(仕上げにパスフレーズを付ける)で起きることです。それまで順調に通っていた確認コマンドが、仕上げた途端に全部落ちるので、本番側の異常を疑ってしまいますが、サーバーは何も変わっていません。対処は単純で、SSH関連の操作はWindows純正の経路(PowerShellのssh.exe)に統一することでした。

WinSCPのセッションURLは、パスワードに「/」が入ると壊れる

非対話でSFTPに入るためWinSCPのコマンドライン(WinSCP.com)を使い、セッションをURL形式sftp://user:pass@host/で渡したところ、初回の実行が謎のエラーで落ちました。

サーバを探索中…
ホスト “example” が見つかりません

ホスト名はexample.ne.jpと正しく書いているのに、WinSCPはexampleだけを探しに行っていました。原因はパスワードの末尾に「/」が含まれていたことです。URLとしてパースされるので「/」がパス区切りと解釈され、ホスト情報とパス部分が分断されていました。対処はURLエンコードです([uri]::EscapeDataString($pw)で「/」を%2Fに変換)。エラーメッセージが「ホストが見つからない」なので、DNSやネットワークの問題に見えてしまうのが厄介なところでした。ランダム生成パスワードには記号が入りやすいため、URL形式で認証情報を渡すツール全般で再発しうる罠です。

認証情報ファイルを読もうとしたら、例外メッセージで全案件のパスワードが出力された

接続情報を得るため、FTPクライアントの設定ファイル(サイト一覧をXMLで保存する形式のもの)を読みました。このファイルは登録パスワードをbase64で保存しているだけで暗号化ではなく、実質平文です。1レコードだけを取り出すつもりが、ファイル全体を一度[xml]にキャストしてから絞り込む書き方をしたところ、文字化けした値を含むサイト名がありXMLパースが例外を投げました。.NETの型変換エラーは変換に失敗した値そのものを例外メッセージに含めるため、パースできなかったXML全文——つまり全案件のFTPパスワードが標準エラー出力にそのまま出てしまいました。

教訓は2つです。認証情報ファイルは、まず必要な1レコードだけを正規表現などで抜き出してから使うこと(全体をパースしてから絞るのは、失敗時に全部こぼれます)。そして、例外メッセージは入力値をそのまま含みうるということです。機微データを扱う処理では、例外を握り潰して自前の短いメッセージに差し替える設計が要ります。根本的な対処としては、そのFTPクライアントにマスターパスワードを設定すれば平文保存自体がなくなります。

移行できるかは、ホスティング会社の「型」で決まる

今回確認できた範囲では、ホスティング会社によってSSH移行の前提条件がまったく違いました。管理画面が見られない案件でも、この型さえ分かれば詰まないケースがあります。

さくら型FTPと同じID/パスワードでSSHにもそのまま入れる→ 管理画面が無くても自分でauthorized_keysを書けるエックスサーバー型公開鍵登録は管理画面のSSH設定フォーム経由FTPパスワードがSSHで通るかとは無関係→ 管理画面が見えないと詰む

さらに2つの前提を潰す必要があります。プランがSSH対応か(下位プランはSSH非対応というケースがある)、そしてFTPユーザーが本アカウントか(サブアカウント@ドメイン形式はSSH不可というケースがある)です。到達性はTest-NetConnection ホスト -Port 22で即座に確認できます。ポートはホストによって違い(一般22番・エックスサーバーやスターサーバーは10022番・heteml/ロリポップは2222番など)、両方叩いて開いている方を見ると型の推定にもなります。「管理画面が見られないから無理です」と即答する前に、まずホスティング会社の型を確認するのが先でした。

FTPアスキーモードの名残で、本番との差分確認が壊れやすい

SSH移行後、本番とローカルのファイルを比較すると全ファイルでサイズや行数がわずかに違って見えることがあります。多くは長年のFTP運用で、FileZillaのようなクライアントが既定でテキストファイルの改行コードを転送時に自動変換していた名残です。改行を正規化してから比較すると、実質的な差分はごく一部に絞れます。ここは既存記事(wp-cli×PowerShell×SSHの記事、FTPS反映の記事)で扱った内容と同じ罠なので、本記事では深入りしません。ただしSSH移行の目的そのものが「本番との差分を自力で確認できるようになること」である以上、この型だけは知っておく価値があります。

受理と成功は別のログで語られる

今回の一連の詰まりに共通していたのは、「鍵は登録できている」ことと「鍵で入れる」ことは別の事実だという点でした。SSHの公開鍵認証は2段階に分かれているため、ログのどの行で拒否されたかを読めば、疑うべき場所がサーバー側か手元側かは機械的に決まります。管理画面を何度も往復する前に、まず-vのログを1行読む——それだけで無駄な切り分けの多くを省けます。なお、同じWindows+PowerShellからssh越しにサーバーを操作したときの詰まりはPowerShellからssh経由でwp-cliを叩くときの回避策に、「送ったはずが本番に反映されていない」型の確かめ方は確かめ方を間違えた3つの現場にまとめています。

この一件の後、なぜ移行したのかを本人に聞くと、答えはセキュリティではありませんでした。

「FTPクライアントソフトで従来やっていたのが面倒になってきたため。SSH接続でCLIから行うのが便利なため」

移行先の選び方(どの案件から着手するか)についても、優先順位付けは働いていなかったそうです。

「単に思い出した」

記事や解説では「平文FTPは盗聴されるから」というセキュリティの話から入るのが定石ですが、実際に人を動かしたのは手作業の面倒くささでした。FTPクライアントはGUIでファイルを探して掴んで放り込む操作が要りますが、SSHに移せばCLIから叩ける——つまりAIエージェントに代行させられます。セキュリティ向上は結果としてついてきた副産物で、動機ではなかった、というのが実際のところでした。

制作体制:一次体験・方針判断・最終確認は筆者が担い、構成・執筆・事実照合はAI(Claude)との協働です。
※本記事の内容は2026年7月時点のものです。

Web制作・改善のご相談は、
初回無料で承っています。

無料で相談する

いきなり相談はちょっと、という方へ

まずは無料で費用感を見積もる

オンライン・お電話・対面(福井近郊)対応/無理な勧誘はいたしません