Skip to main content

Command Palette

Search for a command to run...

zsh に移行したら Obsidian Git が壊れた

SSH agent と Flatpak の格闘記

Published
5 min read
zsh に移行したら Obsidian Git が壊れた

はじめに

前の記事で .bashrc をやめ、bash と zsh を役割分担させた。ターミナル環境は整った。

翌日、Obsidian を開いたら Git の同期が止まった。

[email protected]: Permission denied (publickey).
fatal: Could not read from remote repository.

「zsh にしたら Obsidian が壊れた」ように見えた。しかし実際の原因はシェルではなかった。
ssh-agent のスコープが GUI アプリに届いていなかったのだ。

これは、その原因を特定し、Flatpak サンドボックスと格闘しながら解決した記録だ。


問題の構造

ターミナルで確認すると、SSH 鍵は見えている。

echo "$SSH_AUTH_SOCK"   # /run/user/1000/gcr/ssh
ssh-add -l              # work-github / personal-github

しかし Obsidian Git plugin では毎回パスフレーズを求められる。

Terminal zsh → OK
Obsidian GUI → NG

問題はこう分解できた。

シェル起動の ssh-agent
  → そのシェル配下だけ有効

GUI 起動の Obsidian
  → .zshrc を読まない
  → ssh-agent に届かない

つまり、認証はシェル設定の問題ではなく、ログインセッション全体の設計の問題だった。


なぜ .bashrc 時代は動いていたのか

ここで疑問が生じた。.bashrc に全部盛りしていた頃は、Obsidian Git が動いていた。なぜか。

調べると、GNOME Keyring(gcr)が関係していることが分かった。

gcr-ssh-agent は、GNOME のログインセッションと連携し、login keyring が自動アンロックされるときに SSH 鍵を自動で公開する。つまり、以前は Obsidian がシェルを経由せずとも gcr 経由で鍵を参照できていたのだ。

「パスフレーズが不要だった」のではなく、過去に入力・保存されたパスフレーズを GNOME Keyring が自動的に使っていたのだった。


gcr-ssh-agent 統一を目指した(そして失敗した)

この仕組みを活かし、すべての入口を gcr で統一しようとした。

gcr-ssh-agent
  → /run/user/1000/gcr/ssh
  → Terminal / Editor / Obsidian / Flatpak から共有

Flatpak の Obsidian に gcr socket を見せた。

flatpak override --user \
  --filesystem=xdg-run/gcr \
  --env=SSH_AUTH_SOCK=/run/user/1000/gcr/ssh \
  md.obsidian.Obsidian

確認すると、一時的に動いた。GitHub 認証が通った。

しかし、再起動後に問題が再発した

署名処理で固まる

ssh-add -l          # personal-github / work-github → 見えている
ssh -T github-personal  # → 固まる(返答が来ない)

詳細ログを見ると、GitHub は鍵を受け入れていた。

Offering public key: id_ed25519_personal
Server accepts key: id_ed25519_personal
signing using ssh-ed25519   ← ここで固まる

gcr-ssh-agent への署名依頼が返ってこない状態だった。

ssh-add -D で消しても鍵が復活する

ssh-add -D   # All identities removed.
ssh-add -l   # personal-github / work-github  ← 即座に復活

gcr が keyring に保存された情報から鍵を自動公開していた。その自動公開された状態が、署名処理で不安定になっていた。

Seahorse での掃除は危険だった

Seahorse(GNOME のキーリング管理 GUI)で gcr 側の SSH 鍵項目を削除しようとすると、秘密鍵ファイル(~/.ssh/id_ed25519_personal)ごと削除するリスクがあることが分かった。

ここで gcr-ssh-agent 統一を断念した。


未解明の点

gcr-ssh-agent が署名処理で固まる根本原因は、最終的に特定できなかった。

分かったことは以下だ。

鍵ファイルが壊れているわけではない
GitHub 側の公開鍵登録が間違っているわけでもない
Flatpak の socket 公開だけが原因でもない
~/.ssh/config の設定が間違っているわけでもない

→ gcr-ssh-agent 内部の自動復元状態が、署名処理で不安定になっていた
→ 原因は未解明

「gcr-ssh-agent が悪い」という断定ではない。この環境では、この問題が起きた。そのため、決定性を優先して OpenSSH ssh-agent に切り替えることにした。


OpenSSH ssh-agent を systemd user service として固定する

gcr に頼らず、固定した socket を持つ OpenSSH ssh-agent を systemd user で管理することにした。

mkdir -p ~/.config/systemd/user

cat > ~/.config/systemd/user/ssh-agent.service <<'EOF'
[Unit]
Description=OpenSSH key agent

[Service]
Type=simple
Environment=SSH_AUTH_SOCK=%t/ssh-agent.socket
ExecStart=/usr/bin/ssh-agent -D -a %t/ssh-agent.socket

[Install]
WantedBy=default.target
EOF

systemctl --user daemon-reload
systemctl --user enable --now ssh-agent.service

socket は /run/user/1000/ssh-agent.socket に固定される。

~/.ssh/config を更新する

IdentityAgent を明示し、SSH_AUTH_SOCK の揺れに依存しない設定にした。

Host github-personal
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_personal
    IdentitiesOnly yes
    AddKeysToAgent yes
    IdentityAgent /run/user/1000/ssh-agent.socket

Host github-work
    HostName github.com
    User git
    IdentityFile ~/.ssh/id_ed25519_work
    IdentitiesOnly yes
    AddKeysToAgent yes
    IdentityAgent /run/user/1000/ssh-agent.socket

bash / zsh で socket を正規化する

.bashrc.zshrc 両方に入れる。

# OpenSSH ssh-agent(.bashrc / .zshrc 共通)
if [ -S "$XDG_RUNTIME_DIR/ssh-agent.socket" ]; then
  export SSH_AUTH_SOCK="$XDG_RUNTIME_DIR/ssh-agent.socket"
fi

Flatpak 対応

Flatpak の Obsidian は独自のサンドボックス内で動く。ホスト側の socket を見せる必要がある。

flatpak override --user \
  --filesystem=xdg-run/ssh-agent.socket \
  --env=SSH_AUTH_SOCK=/run/user/1000/ssh-agent.socket \
  md.obsidian.Obsidian

# gcr は外す
flatpak override --user \
  --nofilesystem=xdg-run/gcr \
  md.obsidian.Obsidian

Obsidian Git wrapper

Obsidian Git plugin が Flatpak 内の /app/bin/git を使うため、git コマンドを wrapper で包んだ。

cat > ~/.local/bin/obsidian-git <<'EOF'
#!/usr/bin/env bash

SOCK="/run/user/1000/ssh-agent.socket"

export SSH_AUTH_SOCK="$SOCK"
export GIT_SSH_COMMAND="ssh \
  -F /home/rbcn2000/.ssh/config \
  -o IdentityAgent=$SOCK \
  -o BatchMode=yes \
  -o ConnectTimeout=10 \
  -o ServerAliveInterval=5 \
  -o ServerAliveCountMax=1"

exec /app/bin/git "$@"
EOF

chmod +x ~/.local/bin/obsidian-git

BatchMode=yes が重要だ。自動コミット・自動同期の裏側でパスフレーズ入力待ちになって固まるのを防ぐ。

Obsidian Git plugin の設定で Custom Git binary path/home/rbcn2000/.local/bin/obsidian-git を指定する。


再起動後の鍵投入を自動化する

OpenSSH ssh-agent は gcr と違い、鍵を自動復元しない。再起動後は空の状態から始まる。

keychain を戻す選択肢もあったが、agent の二重化リスクがあるため採用しなかった。代わりに「鍵投入だけ」を担う最小のスクリプトを作った。

cat > ~/.local/bin/ssh-load-github <<'EOF'
#!/usr/bin/env bash

set -u

SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="$SOCK"

if [ ! -S "$SOCK" ]; then
  systemctl --user start ssh-agent.service 2>/dev/null || true
fi

if [ ! -S "$SOCK" ]; then
  echo "ssh-agent socket not found: $SOCK" >&2
  exit 1
fi

# 非対話環境ではパスフレーズ入力待ちにしない
if [ ! -t 0 ]; then
  exit 0
fi

CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"

if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'personal-github'; then
  ssh-add "$HOME/.ssh/id_ed25519_personal" || exit 1
fi

CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"

if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'work-github'; then
  ssh-add "$HOME/.ssh/id_ed25519_work" || exit 1
fi
EOF

chmod +x ~/.local/bin/ssh-load-github

.zshrc.bashrc で interactive shell 起動時に呼び出す。

if [[ \(- == *i* ]] && [ -t 0 ] && [ "\){SSH_LOAD_GITHUB_ON_SHELL:-1}" = "1" ]; then
  "$HOME/.local/bin/ssh-load-github" 2>/dev/null || true
fi

これで、ターミナルを最初に開いたとき、鍵が未ロードであればパスフレーズを1回入力するだけでよくなった。


.desktop 起動の落とし穴と GUI ランチャー

これで解決したと思ったが、もうひとつ問題があった。

再起動後、Obsidian を最初に起動すると Git が動かない。

原因は ssh-load-github の TTY チェックだ。

if [ ! -t 0 ]; then
  exit 0   # TTY がなければ何もしない
fi

.desktop ファイルから起動する場合、TTY がない。そのため ssh-load-github が即座に終了し、鍵が未ロードのまま Obsidian が起動してしまう。

解決策:SSH_ASKPASS でGUI ダイアログを使う

TTY なし環境でパスフレーズ入力を受け付けるための仕組みが OpenSSH に用意されている。SSH_ASKPASS だ。

sudo apt install ssh-askpass-gnome

これをラップする Obsidian 専用のランチャーを作った。

cat > ~/.local/bin/open-obsidian <<'EOF'
#!/usr/bin/env bash

SOCK="/run/user/1000/ssh-agent.socket"
export SSH_AUTH_SOCK="$SOCK"

if [ ! -S "$SOCK" ]; then
  systemctl --user start ssh-agent.service 2>/dev/null || true
fi

if [ ! -S "$SOCK" ]; then
  echo "ssh-agent socket not found: $SOCK" >&2
  exit 1
fi

# TTY がなくても GTK ダイアログでパスフレーズ入力を受け付ける
export SSH_ASKPASS="/usr/lib/openssh/gnome-ssh-askpass"
export SSH_ASKPASS_REQUIRE=force

CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"

if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'personal-github'; then
  ssh-add "$HOME/.ssh/id_ed25519_personal" || exit 1
fi

CURRENT_KEYS="$(ssh-add -l 2>/dev/null || true)"

if ! printf '%s\n' "$CURRENT_KEYS" | grep -q 'work-github'; then
  ssh-add "$HOME/.ssh/id_ed25519_work" || exit 1
fi

exec flatpak run md.obsidian.Obsidian "$@"
EOF

chmod +x ~/.local/bin/open-obsidian

.desktop ファイルをオーバーライドする

Flatpak のシステム側 .desktop は直接編集できない(更新で上書きされる)。ユーザー領域にコピーして差し替える。

cp /var/lib/flatpak/exports/share/applications/md.obsidian.Obsidian.desktop \
   ~/.local/share/applications/md.obsidian.Obsidian.desktop

sed -i 's|^Exec=.*|Exec=/home/rbcn2000/.local/bin/open-obsidian %U|' \
   ~/.local/share/applications/md.obsidian.Obsidian.desktop

update-desktop-database ~/.local/share/applications/

これで、アプリランチャーから Obsidian を開くと、まず GTK のパスフレーズダイアログが表示され、入力後に Obsidian が起動する。


最終的な構造

systemd user
  ↓
OpenSSH ssh-agent.service
  ↓
/run/user/1000/ssh-agent.socket
  ├─ bash / zsh(起動時に ssh-load-github)
  ├─ ~/.ssh/config(IdentityAgent で明示)
  └─ ~/.local/bin/open-obsidian  ← .desktop から起動
       └─ SSH_ASKPASS=gnome-ssh-askpass(GTK ダイアログ)
            ↓
          Flatpak Obsidian
               ↓
             ~/.local/bin/obsidian-git(BatchMode=yes)
               ↓
             /app/bin/git

再起動後の挙動はこうなった。

ターミナルを最初に開く場合:
  → ssh-load-github 起動
  → パスフレーズを1回入力
  → 以降、bash / zsh / Obsidian すべてで共有

Obsidian を最初に開く場合:
  → open-obsidian 起動
  → GTK ダイアログでパスフレーズ入力
  → 以降、bash / zsh / Obsidian すべてで共有

責務の分離

最終的に、ssh-agent まわりの責務がこう整理できた。

役割 担当
agent 起動 systemd user ssh-agent.service
agent 選択 SSH_AUTH_SOCK / IdentityAgent
鍵投入(ターミナル) ssh-load-github(TTY あり)
鍵投入(GUI 起動) open-obsidian + SSH_ASKPASS(TTY なし)
自動 Git 処理 BatchMode=yes

keychain のような「まとめてやってくれるツール」を使うと便利だが、agent 二重化のリスクや GUI アプリへの不到達が起きやすい。責務を分けることで、どこで何が起きているかを把握できるようになった。


教訓

環境変数は「設定」ではなく「プロセス継承の結果」である。

SSH_AUTH_SOCK が正しく設定されていても、それを持つプロセスから起動されなければ子プロセスには届かない。シェルの .zshrc に書いた設定は、GUI アプリには届かない。これがすべての根本だった。

そして追加の教訓として:

便利な GUI 統合より、固定された socket と単純な agent の方が決定性が高い場合がある。

gcr-ssh-agent はデスクトップとの統合が美しい。しかし今回の環境では、その自動復元の仕組みが署名処理で不安定になった。シンプルな OpenSSH ssh-agent を固定 socket で起動する方が、ずっと予測可能だった。

More from this blog