前回に引き続き怠惰Humanシリーズです。
さて、いっぱいVMやらLXCがある弊PVEクラスタですが、ホストごとにパスワード認証だったり公開鍵認証だったりバラバラでやってられないので、認証基盤を統一しました。
すでにk8sにpocket IDが乗っているので、これに統合して1度パスキーログインをすればその後はssh user@hostで入れるようにしました。
やりたいこと
- どんなクライアントでも認証を1回通すだけで以降は自由につなげるようにしたい
- わざわざパスワードログインしたくない
- でもクライアントごとに全ホストに公開鍵を登録しにいくのもやだ
- ラッパーコマンドや長いオプションは打ちたくない。ただの
ssh [email protected]で入りたいし、~/.ssh/configも汚したくない - 毎日・毎回のログインは絶対にやりたくない
- どことは言いませんがお前のことやぞGCP
- 既存の公開鍵・パスワード認証・Proxmoxコンソールは復旧経路としてそのまま残す
ボツったもの
| 方式 | 判断 | 理由 |
|---|---|---|
| 公開鍵をAnsible等で全台配布 | ボツ | 新端末を増やすたびにmanifest更新+全台へpushしないといけない |
| Smallstep等でSSH CA構築 | ボツ | 標準のOpenSSH証明書だけで完結しサーバー側にバイナリ不要なのは綺麗だけどCAサーバーの常駐運用・CA秘密鍵の保護が増える。自宅1人運用には重い |
| Tailscale SSH / NetBird SSH | ボツ | 全ゲストにメッシュVPNエージェントを入れる前提にしたくない |
採用したもの
opkssh + pocket ID
pocket IDは皆さまおなじみOIDC IdPです。
opksshは、OpenPubkey SSHというらしいです。
OIDCのIDトークンに、クライアントが生成した一時SSH鍵のハッシュを埋め込んで署名します。
別途SSH CAサーバーを建てなくてよくて、Pocket IDが署名したIDトークンそのものが公開鍵の証明書として機能するのが良いところです。
- クライアント側
opkssh login→ ブラウザでPocket IDのパスキー認証 →~/.ssh/id_ecdsaと IDトークンを埋め込んだ~/.ssh/id_ecdsa-cert.pubが生成される。~/.ssh/id_ecdsaに置かれるのでOpenSSHクライアントが何の設定もなしに自動で提示してくれる。
- サーバー側:
sshdのAuthorizedKeysCommand /usr/bin/opkssh verify %u %k %tが呼ばれる。opkssh verifyがPocket IDの.well-known/jwks.jsonでIDトークンの署名を検証し、/etc/opk/providersと/etc/opk/auth_idを照合してOKなら公開鍵を返す。
設計ポイント
トークンの有効期限ポリシー
これは期間指定をせず、neverにしました。
期間での自動失効が行われません。
あぶなくね?と思う方もいるかもしれませんが、
- デフォルトの
24h:- 毎日ブラウザが開いてパスキーを要求される。ありえなさすぎる。
oidc_refreshed+ クライアント常駐opkssh login --auto-refresh:- 一見これが本命に見えたが、opkssh側の実装を読むとリフレッシュトークンをディスクに一切保存せずプロセスのメモリ上にしか持っていない。
- つまりPCを再起動したり、スリープ中にオフラインで更新に1回失敗してプロセスが落ちるたびに結局ブラウザが開いてパスキー認証を求められる。むりむりカタツムリ。
- ついでにサーバー側の
/etc/opk/providersに書ける有効期限もコード上12h/24h/48h/1week/oidc/oidc_refreshed/neverの7種類しかなく、30日みたいな任意の日数指定もできない。
never:- 結局認証しないなら普通のSSH公開鍵認証と何が違うの?
- opksshの証明書だけ漏れてもクライアントの秘密鍵がなければログインできない。
- クライアントの秘密鍵ごと抜かれる状況だったらもう何抜かれても一緒。
- そもそも22番はインターネットに公開ないので外からはNetBirdの認証も抜かないとSSHアクセスしようがない。
- 新しい端末をセットアップするとき、鍵のコピペではなくパスキー認証で本人確認できることこそが目的で、発行後の寿命を無期限にしてもリスクは今までと一緒じゃん。ってことで採用。
- もし端末を紛失したらどうするの?
neverだとPocket ID側でログアウトしても証明書は失効しないが、Pocket IDでOIDC Clientを作り直して全ホストの/etc/opk/providersのClient IDを書き換えれば、旧端末の証明書はaud不一致で一斉に無効化できる。
- 結局認証しないなら普通のSSH公開鍵認証と何が違うの?
パッケージ管理
最初は公式のinstall-linux.shを試したのですが、野良バイナリをバラまくと後から管理してらんなくなります。
かといってOS標準リポジトリを見ると、
- Debian 13公式apt:
opkssh 0.4.0とかいう遺産バージョンが降ってきて使い物にならない - EPEL 10:
0.17.0があるけど全ホストにEPELを入れないといけない
てことでGitHuで配られている公式のrpm / .debを直接入れる方式にしました。
これならOSやArchが混ざっていても全部揃うし、rpm / dpkgの管理下に収まります。
パッケージの中身は/usr/bin/opksshバイナリだけなので、opksshuserの作成や/etc/opk/の生成、SELinuxモジュール導入、/etc/ssh/sshd_config.d/60-opk-ssh.confの配置、sshd -Tの差分チェックまでやるスクリプトを書いて全台に流しました。
SELinux対応
AlmaLinux 9 / 10のホストはSELinux Enforcingで動かしています。(最近は脳死で無効化をやめた、えらい)
sshdから呼ばれるopkssh verifyは検証時にPocket IDへHTTPS通信しにいくので、素のままだとSELinuxにブロックされます。
Permissiveに落とさずEnforcingのまま通るようにしました。
Device Flowは使えなかった
ブラウザがないcode-serverのコンテナでもクライアントとしてログインさせようと、opksshのdevice_flow: trueを試してみました。
Pocket ID側にもエンドポイントがあるのでブラウザの承認画面までは進んだのですが、最後のトークン検証で以下のエラーが出て弾かれました。
error verifying PK Token: missing commitment claim nonce
(the OpenID Provider https://id.fugafuga does not return the nonce in device flow, so device_flow cannot be used with it)
Pocket IDはDevice Flowのときにnonceを返さない実装になっているので原理的に使えないようです。
まぁヘッドレス環境がクライアントになることはほぼないのでヨシ。
実際の設定
Pocket ID側
OIDC Clientを1個作ります。
- Name:
hogehoge-ssh - Public Client: ON
- Callback URLs:
http://localhost:3000/login-callback http://localhost:10001/login-callback http://localhost:11110/login-callback
サーバー側
/etc/opk/providershttps://id.fugafuga <Client_ID> never/etc/opk/auth_id
メールアドレスではなくPocket IDのsubで紐付けます。ホストに応じてrootと、wheel/sudo持ちの一般ユーザーを登録しました。root <User_Sub_UUID> https://id.fugafuga user <User_Sub_UUID> https://id.fugafuga/etc/ssh/sshd_config.d/60-opk-ssh.confAuthorizedKeysCommand /usr/bin/opkssh verify %u %k %t AuthorizedKeysCommandUser opksshuser
クライアント側
~/.opk/config.yml--- default_provider: pocket-id providers: - alias: pocket-id issuer: https://id.fugafuga client_id: <Client_ID> scopes: openid email redirect_uris: - http://localhost:3000/login-callback - http://localhost:10001/login-callback - http://localhost:11110/login-callback
初回だけopkssh login pocket-idを叩いてブラウザでパスキー認証すれば、あとは普通にssh [email protected]で入れます。
おわりに
毎回パスワードを手打ちしていたVMも、手元のMacの鍵を入れ忘れていたホストも、全部そのままsshで入れるようになりました。
もっと怠惰になっていけ。
