
iPhone の SSH クライアント Moshi から DGX Spark を触っていると、sudo のたびに管理者パスワードを打つのが地味に面倒です。Moshi は SSH 鍵を iOS のキーチェーンに保存して Face ID で解錠できるので、この鍵を sudo の認証にも使えないかと考えました。
結論から言うと、Moshi の SSH エージェント転送と pam_ssh_agent_auth を組み合わせれば実現できます。ただし Tailscale SSH 経由では鍵認証が通らない、tmux の中では環境変数が古いまま、という 2 つの落とし穴があったので、その回避策も含めて手順をまとめます。
この記事は、Claude と対話しながら進めた作業をもとに、Claude が下書きした記事を筆者が確認・修正したものです。手順はすべて筆者の環境で実行し、結果を確認しています。
前提と注意
- 向いているのは、自分専用の検証機のように sudo を使うユーザーが 1 人のホストです。複数人で使うホストや本番機には向きません
- 転送中は、そのユーザー権限で動く全プロセスが sudo を試せます。私の環境では Hermes Agent が admin で常駐しているので、エージェントからの sudo も iPhone に届きます。後述の「承認」を「毎回」にしておけば意図しない sudo はダイアログで拒否できますが、「自動」や「セッション」で自律エージェントや CI が動くホストに入れるのは避けてください
- Face ID が求められるのは接続時の 1 回だけです。Moshi 4.0.3 以降は「承認: 毎回」で署名ごとの許可ダイアログを出せるので、sudo のたびに iPhone で許可する運用ができます(ダイアログで Face ID の再認証はされません)
- 被害範囲を狭めたいなら、特定コマンドだけ
NOPASSWDにする方が安全です
仕組み
Moshi の「SSH エージェント転送」で iPhone 上の鍵をサーバーから使えるようにし、サーバー側では sudo の PAM に pam_ssh_agent_auth を追加して、転送された鍵で認証させます。秘密鍵は iPhone から出ず、sudo 側は署名の検証だけを行います。

sudo 実行時に pam_ssh_agent_auth が転送チャネル経由で Moshi に署名を求め、登録済みの公開鍵で検証できれば通します。
環境
- サーバー:NVIDIA DGX Spark(DGX OS、Ubuntu 24.04 ベース、arm64)
- 接続経路:Tailscale 1.104.1(サーバー側で Tailscale SSH を有効化済み)
- sshd:OpenSSH 9.6p1(ssh.socket によるソケット起動)、ufw は無効
- クライアント:iPhone の Moshi 4.0.3(SSH / Mosh クライアント)
- 多重化:tmux 3.4
Moshi からは Tailscale SSH の接続(パスワード空・接続タイプ自動)を普段使いしています。本記事では、この接続はそのまま残し、sudo 用の接続を別に追加します。
Tailscale SSH の壁と sshd のポート追加
最初に Moshi の既存接続を鍵認証に切り替えて試したところ、「Authentication failed」で接続できませんでした。Tailscale SSH は SSH の認証フェーズで鍵を検証せず、Tailscale の ID で認証する方式(認証タイプ none)のため、鍵認証を押し通すクライアントとは相性が悪いようです。
Tailscale SSH が奪うのは tailnet 側のポート 22 だけなので、素の sshd に別ポートを足して回避しました。既存の Tailscale SSH 接続には影響しません。
事前に sshd の状態を確認しておきます。
sudo sshd -T | grep -Ei '^(port|pubkeyauthentication|allowagentforwarding) '
(out) port 22
(out) pubkeyauthentication yes
(out) allowagentforwarding yes
sudo ss -tlnp | grep sshd
(out) LISTEN 0 4096 0.0.0.0:22 0.0.0.0:* users:(("sshd",pid=4779,fd=3),("systemd",pid=1,fd=248))systemd が同じソケットを持っているのでソケット起動(ssh.socket)です。この場合、sshd_config のポート設定はソケット側にも反映させる必要があります。
sudo tee /etc/ssh/sshd_config.d/50-moshi-port.conf <<'EOF'
Port 22
Port 2222
EOF
sudo sshd -t
sudo systemctl daemon-reload
sudo systemctl restart ssh.socket
sudo ss -tlnp | grep sshd22 と 2222 の両方が LISTEN していれば OK です。daemon-reload で ssh.socket の ListenStream が sshd_config から生成し直されるので、通常はソケットの再起動だけで反映されます。
なお Tailscale SSH 自体はエージェント転送に対応しています(過去に v1.68.0 で非 TTY 実行時だけ壊れる退行があり、1.70.0 で修正済み)。問題は転送ではなく、鍵認証の接続が通らない点です。
Moshi 側の設定

ホーム画面の「接続」から「自分の SSH 設定を使う」を選び、sudo 用の接続を新規に作ります。QR でのかんたんペアリングは別の鍵を自動登録する流れなので、今回は使いません。
- ホスト名:既存接続と同じ Tailscale のホスト名
- ポート:2222
- ユーザー名:admin
- 認証:鍵ファイル → Moshi 内で ED25519 鍵を新規生成(パスフレーズは空のまま)
- 接続:SSH(自動・Mosh・ET ではエージェント転送のスイッチが表示されない)
- SSH エージェント転送:ON → キー: このキー、承認: 毎回(4.0.3 以降)

エージェント転送は SSH チャネルの機能なので Mosh では使えません。回線が不安定なときのために、Mosh の接続は別に残しておきます。

鍵の生成画面で「公開鍵」をコピーしておき、Tailscale SSH の既存接続から DGX Spark にログインして ~/.ssh/authorized_keys に追記します。
mkdir -p ~/.ssh && chmod 700 ~/.ssh
echo 'ssh-ed25519 AAAA...' >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys設定 → セキュリティの「鍵に生体認証」(Biometric for keys)も ON にしておきます。これが OFF だと iPhone のロック解除だけで鍵が使えてしまいます。
Moshi 4.0.3 で、接続の編集画面の「SSH エージェント転送」の下に 2 つの項目が増えました。「キー」は転送する鍵の範囲(このキー/すべて)、「承認」は署名要求の確認方法(自動/セッション/毎回)です。sudo 用の接続では「このキー」「毎回」にしておきます。「毎回」にすると、署名要求のたびに iPhone にホスト名と鍵の指紋つきの許可ダイアログが出ます。
2222 の接続でログインできたら、転送が届いているか確認します。
echo "$SSH_AUTH_SOCK"
(out) /tmp/ssh-kycZoCXjD6/agent.150340
ssh-add -L
(out) ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA...Moshi の公開鍵が 1 行出れば成功です。SSH_AUTH_SOCK が空の場合、Moshi は転送要求の応答を待たないため、サーバー側で拒否されてもエラーは出ません。sshd -T で allowagentforwarding yes になっているかを確認してください。
pam_ssh_agent_auth の導入と公開鍵の登録
sudo の認証を SSH エージェントに委ねる PAM モジュールを入れます。Ubuntu 24.04(noble)の universe に arm64 版があります。
sudo apt install libpam-ssh-agent-auth
(out) 取得:1 http://ports.ubuntu.com/ubuntu-ports noble/universe arm64 libpam-ssh-agent-auth arm64 0.10.3-8 [123 kB]次に、sudo の認証に使う公開鍵を /etc/security/authorized_keys に登録します。先ほどの 2222 接続(転送が効いているシェル)で ssh-add -L の出力をそのまま流し込むのが確実です。
ssh-add -L | sudo tee /etc/security/authorized_keys
sudo chown root:root /etc/security/authorized_keys
sudo chmod 644 /etc/security/authorized_keystee は標準出力にも同じ行を出すので、鍵が 2 行表示されたように見えますが、ファイルには 1 行だけ入っています。pam_ssh_agent_auth はこのファイルが root 所有で他者から書き込み不可でないと拒否するため、所有者と権限は必ず確認してください。
~/.ssh/authorized_keys とは別ファイルです。ログイン用の鍵と sudo 用の鍵を分けて管理できるようになっています。
sudoers と PAM の変更
SSH_AUTH_SOCK を sudo に引き継ぐ
sudo は環境変数を落とすので、SSH_AUTH_SOCK だけ残すように sudoers に追記します。
echo 'Defaults env_keep += "SSH_AUTH_SOCK"' | sudo tee /etc/sudoers.d/ssh-agent
sudo chmod 440 /etc/sudoers.d/ssh-agent
sudo visudo -c
(out) /etc/sudoers: 正しく構文解析されました
(out) /etc/sudoers.d/ssh-agent: 正しく構文解析されましたここまでは認証方式に触っていないので、sudo が壊れることはありません。
PAM に pam_ssh_agent_auth を追加
PAM を触る前に、別のシェルで sudo -i して root シェルを開いたままにしておきます。失敗したときの戻し先です。
/etc/pam.d/sudo の @include common-auth の直前に 1 行入れます。sufficient なので、鍵で通らなければ従来どおりパスワード認証に落ちます。
sudo cp /etc/pam.d/sudo /etc/pam.d/sudo.bak
sudo sed -i '/^@include common-auth/i auth sufficient pam_ssh_agent_auth.so file=/etc/security/authorized_keys' /etc/pam.d/sudo
cat /etc/pam.d/sudo#%PAM-1.0
# Set up user limits from /etc/security/limits.conf.
session required pam_limits.so
session required pam_env.so readenv=1 user_readenv=0
session required pam_env.so readenv=1 envfile=/etc/default/locale user_readenv=0
auth sufficient pam_ssh_agent_auth.so file=/etc/security/authorized_keys
@include common-auth
@include common-account
@include common-session-noninteractive動作確認
sudo -k
sudo true && echo OK
(out) OKパスワードを聞かれずに OK が出れば成功です。うまくいかない場合は root シェルから cp /etc/pam.d/sudo.bak /etc/pam.d/sudo で戻し、journalctl -t sudo -n 5 でログを確認します。
「承認」を「毎回」にしていると、sudo のたびに iPhone に「DGX Spark が SSH キーの使用を求めています」という許可ダイアログが出て、許可すると sudo が通ります。拒否すると鍵認証が失敗し、従来どおりパスワード入力に落ちます。Face ID が求められるのは接続時の鍵の解錠だけで、ダイアログでは再認証されません。「自動」や「セッション」の場合は、接続後の sudo は無確認で通ります。

tmux の中で効かない問題
Moshi のセッションピッカーから既存の tmux セッションに入ると、sudo がまたパスワードを聞いてきました。
echo "$SSH_AUTH_SOCK"
(out) /run/user/1000/gnupg/S.gpg-agent.ssh
ssh-add -L
(out) The agent has no identities.tmux のセッションは作成時点の環境変数を引き継ぐため、SSH_AUTH_SOCK が古いまま(この例では gpg-agent のソケット)になっています。転送自体は届いていて、/tmp/ssh-*/agent.* にソケットはあります。
再接続のたびにソケットのパスが変わるので、固定パスのシンボリックリンクを経由させる定番の方法で対処します。
~/.ssh/rc を作成します。SSH ログインのたびに実行され、その接続の転送ソケットへリンクを張り直します。
cat > ~/.ssh/rc <<'EOF'
if [ -S "$SSH_AUTH_SOCK" ]; then
ln -sf "$SSH_AUTH_SOCK" ~/.ssh/ssh_auth_sock
fi
EOF
chmod 600 ~/.ssh/rc~/.tmux.conf に追記して、tmux 側は常に固定パスを使うようにします。1 行目はデフォルトの update-environment から SSH_AUTH_SOCK を外したものです。
cat >> ~/.tmux.conf <<'EOF'
set -g update-environment "DISPLAY SSH_ASKPASS SSH_AGENT_PID SSH_CONNECTION WINDOWID XAUTHORITY"
setenv -g SSH_AUTH_SOCK $HOME/.ssh/ssh_auth_sock
EOF
tmux source-file ~/.tmux.conf
tmux set-environment -g SSH_AUTH_SOCK ~/.ssh/ssh_auth_sockすでに開いている tmux 内のシェルだけは、1 回 export SSH_AUTH_SOCK=~/.ssh/ssh_auth_sock が必要です。新しく作るセッションは最初から固定パスを使います。
Moshi を切断して 2222 で再接続し、tmux に入って確認します。
echo "$SSH_AUTH_SOCK"
(out) /home/admin/.ssh/ssh_auth_sock
ls -la ~/.ssh/ssh_auth_sock
(out) lrwxrwxrwx 1 admin admin 32 10月 14 23:30 /home/admin/.ssh/ssh_auth_sock -> /tmp/ssh-U5Nue46U7u/agent.159014
sudo -k; sudo true && echo OK
(out) OKなお ~/.ssh/rc があると sshd が X11 転送時の xauth 処理をスキップしますが、サーバー用途では影響ありません。
tmux を systemd で自動起動している場合
私の環境では、Spark の起動時に tmux のセッション main を systemd user ユニット(tmux-main.service + loginctl enable-linger)で自動生成しています。ユニットは次のとおりです。
mkdir -p ~/.config/systemd/user
cat > ~/.config/systemd/user/tmux-main.service <<'EOF'
[Unit]
Description=tmux session "main"
[Service]
Type=forking
ExecStart=/bin/sh -c 'tmux has-session -t main 2>/dev/null || exec tmux new-session -d -s main'
ExecStop=/usr/bin/tmux kill-session -t main
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
systemctl --user enable --now tmux-main.service
loginctl enable-linger $USER
loginctl show-user $USER -p Linger
(out) Linger=yesenable-linger がないと user ユニットはログイン後にしか起動しません。ExecStart を has-session || new-session にしてあるのは、手で先に main を作ってしまった場合に duplicate session で失敗しないための保険です。
この方法で起動した tmux サーバーは環境変数が最小限で、SSH_AUTH_SOCK は最初から存在しません。
この場合も ~/.tmux.conf の setenv -g SSH_AUTH_SOCK $HOME/.ssh/ssh_auth_sock が効くので、上の手順をそのまま適用すれば問題ありません。~/.ssh/rc がログインのたびにリンクを張り直し、tmux 側は固定パスしか見ないためです。
なお、tmux の中のシェルで exit を打つとウィンドウが閉じ、セッションが空になって tmux サーバーごと終了します。Moshi を離れるときはアプリを閉じるだけ(デタッチ)にして、セッションを作り直すときは手で tmux new せず systemctl --user restart tmux-main.service を使うと、systemd との管理が揃います。
運用上の注意とまとめ
- 2222 の接続を切ると転送も消えるので、その後の sudo はパスワード認証に戻ります。
sufficientにしてあるので従来の認証はそのまま使えます - 転送中は admin 権限で動く全プロセスがその鍵で sudo を試せます。「承認: 毎回」で止められますが、使わないときは 2222 の接続を切り、鍵は Moshi 専用にし、
/etc/security/authorized_keysを他のホストに広げないようにします - Tailscale SSH 経由の既存接続では転送されないので、そちらの sudo は従来どおりパスワードです
- Face ID が出るのは接続時だけです。sudo のたびの確認は Moshi 4.0.3 以降の「承認: 毎回」で行います
普段の作業は Mosh や Tailscale SSH の接続、管理作業は 2222 の接続から、という使い分けになります。Spark を再構築するときに戻す設定は次の 9 点です。
libpam-ssh-agent-authのインストール~/.ssh/authorized_keys(ログイン用の公開鍵)/etc/security/authorized_keys/etc/sudoers.d/ssh-agent/etc/pam.d/sudoの 1 行/etc/ssh/sshd_config.d/50-moshi-port.conf~/.ssh/rc~/.tmux.confの 2 行~/.config/systemd/user/tmux-main.serviceとloginctl enable-linger
参考
- Connections and authentication – Moshi Docs
- Security and sync – Moshi Docs
- Tailscale SSH – Tailscale Docs
- Tailscale SSH failing to forward SSH Agent on 1.68.0 – tailscale/tailscale #12467

コメント