Attempt #1
Windowsランナーでssh-agent

Windowsランナーを使う予定のない人は、以降まったく読む価値が無いことに注意。

別のプライベートリポジトリのクローン

あるリポジトリAについてGitHub Actionsのジョブを実行中に、Aと別のプライベートリポジトリBをチェックアウトまたはクローンするステップを記述したい。Windowsランナーに限らないが、簡単な方法はマーケットプレイスのactions/checkoutを次のように使うことだろう:

    steps:
    - name: Checkout private repository
      uses: actions/checkout@v4
      with:
        repository: maroontress-tomohisa/private-repository-example
        ssh-key: ${{secrets.PRIVATE_REPO_DEPLOY_KEY}}
        path: private-repository-example
    - name: Print README.md
      shell: bash
      run: |
        cat private-repository-example/README.md

GitHubで見る

「リポジトリB」にアクセスするために、デプロイキーを使用することにする。リポジトリBのURLはgit@github.com:maroontress-tomohisa/private-repository-example.gitとしよう。そして、リポジトリBには適切にデプロイキーのパブリックキーが設定され、リポジトリAのシークレットに適切にデプロイキーのプライベートキーが設定されていて、それをワークフロー内ではsecret.PRIVATE_REPO_DEPLOY_KEYで参照できるようになっている、とする。

最初のステップでリポジトリBをprivate-repository-exampleにチェックアウトする。次のステップで確認のため、リポジトリBのREADME.mdを出力する。

結果は次のようになった:

Run actions/checkout@v4
Syncing repository: maroontress-tomohisa/private-repository-example
Getting Git version info

Temporarily overriding HOME='D:\a\_temp\d3f120f5-cde6-43a2-b847-7146107be8b8' before making global git config changes
Adding repository directory to the temporary git global config as a safe directory
"C:\Program Files\Git\bin\git.exe" config --global --add safe.directory D:\a\try_out_github_actions\try_out_github_actions\private-repository-example
Initializing the repository
Disabling automatic garbage collection
Setting up auth
Determining the default branch
Fetching the repository
Determining the checkout info
Checking out the ref
"C:\Program Files\Git\bin\git.exe" log -1 --format='%H'
'3cf9492e7ffb20ea5246a8edf1bec8d3aab293a4'
⋮
# An Example of Private Repository

GitHub Actionsの結果

成功だ。最後の行はREADME.mdの中身そのものである。

このように、actions/checkoutを使えば、別のプライベートリポジトリをチェックアウトすることができる。しかし、ローカル環境でも同じようにリポジトリBをチェックアウトする必要がある場合、何らかのスクリプトを用意して、そのスクリプトでチェックアウトしたい。そうすると、ワークフローファイルに記述したものはGitHub Actionsでしか実行できないので、二重に管理することになる。ワークフローファイルをローカルで実行できるように変換する何かを作るのも良さそうだが、他の方法も調べてみよう。

ssh-agentで別のプライベートリポジトリのクローン

同じことを、今度はssh-agentを用いてやってみる。そうではなく、~/.ssh/configにエントリを追加してもできるが、それは後のセクションで試してみる。

まず、Windowsランナーにインストールされているssh-agent関連のコマンドがどのようになっているのかを確認する。

    - name: Check commands
      shell: bash
      run: |
        ls -l `which ssh`
        ls -l `which ssh-add`
        ls -l `which ssh-agent`
        ls -l `which git`

GitHubで見る

結果は次のようになった:

-rwxr-xr-x 1 runneradmin 197121 958822 Aug 30 09:46 /usr/bin/ssh
-rwxr-xr-x 1 runneradmin 197121 441485 Aug 30 09:46 /usr/bin/ssh-add
-rwxr-xr-x 1 runneradmin 197121 415546 Aug 30 09:46 /usr/bin/ssh-agent
-rwxr-xr-x 4 runneradmin 197121 3830264 Aug 30 09:49 /mingw64/bin/git

GitHub Actionsの結果

Windowsのssh-agentの実装はいくつかあるが、パスにあるのはGit for Windowsのものである。

ssh-agentを素直に実行すると次のようになる:

    - name: Start ssh-agent
      shell: bash
      run: |
        eval `ssh-agent`
        echo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" >> "$GITHUB_ENV"
        echo SSH_AGENT_PID="$SSH_AGENT_PID" >> "$GITHUB_ENV"

GitHubで見る

最後の二行は、後に続くステップで環境変数SSH_AUTH_SOCKSSH_AGENT_PIDが有効になるように$GITHUB_ENVに追記している(詳細は環境変数の設定を参照)。

結果は次のようになった:

Agent pid 433

GitHub Actionsの結果

ssh-agentがバックグラウンドで動作したので、次にデプロイキーをssh-addコマンドでエージェントに登録する:

    - name: Add a deploy key
      shell: bash
      run: |
        mkdir -p $HOME/.ssh
        echo "${{secrets.PRIVATE_REPO_DEPLOY_KEY}}" > $HOME/.ssh/PRIVATE_REPO_DEPLOY_KEY
        ssh-add $HOME/.ssh/PRIVATE_REPO_DEPLOY_KEY

GitHubで見る

一旦、~/.sshディレクトリを作成して、そこにファイルとしてプライベートキーを保存した。

結果は次のようになった:

Identity added: /c/Users/runneradmin/.ssh/PRIVATE_REPO_DEPLOY_KEY (git@github.com:maroontress-tomohisa/private-repository-example.git)

GitHub Actionsの結果

無事、登録できた。カッコ内にSSHキーのコメントが表示されている。このデプロイキーは、コメントにリポジトリのURLを指定して作成した。これは現時点では意味が無い(後のセクションで、複数のプライベートリポジトリを読み出すときに用いる)ので、ここでは無視する。

念のため、次のようにssh-add -lで登録されたキーを確認する:

    - name: List fingerprints
      shell: bash
      run: |
        ssh-add -l

GitHubで見る

結果は次のようになった:

3072 SHA256:EHYsJhMvV2X03sbEYcAH3w7MNft1lra8M/ZSF0XMr5k git@github.com:maroontress-tomohisa/private-repository-example.git (RSA)

GitHub Actionsの結果

これでgit cloneできるはずである。次のようにクローンしてみよう:

    - name: Clone the private repository (which fails)
      continue-on-error: true
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-repository-example.git
        cat private-repository-example/README.md

GitHubで見る

最後の行は、クローンに成功した場合にREADME.mdを表示したい、という意図だが、成功しないので意味が無い。結果は次のようになった:

Cloning into 'private-repository-example'...
Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
Error: Process completed with exit code 128.

GitHub Actionsの結果

これは簡単で、github.comのSSHパブリックキーが~/.ssh/known_hostsに含まれてないのでエラーになる。そもそも~/.ssh/known_hostsが存在してないのだ。次のようにknown_hostsを作成しよう:

    - name: Perform workarounds (create ~/.ssh/known_hosts)
      shell: bash
      run: |
        rm -rf private-repository-example
        cat << EOF > $HOME/.ssh/known_hosts
        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
        EOF

GitHubで見る

もう一度、次のようにクローンしてみよう:

    - name: Clone a private repository
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-repository-example.git
        cat private-repository-example/README.md

GitHubで見る

結果は次のようになった:

Cloning into 'private-repository-example'...
# An Example of Private Repository

GitHub Actionsの結果

成功した。

少し前までは、この段階で環境変数GIT_SSHの設定が必要だったのだが、現在はその必要が無くなった。GitHub Actionsの世界は少しずつ良くなっているようだ。

別のプライベートLFSリポジトリのクローン

今度は、別のプライベートリポジトリCをクローンしてみよう。ただし、リポジトリCはGit LFSを用いている。そう、説明はなかったが、前のセクションのリポジトリBはLFSを使っていなかった。

リポジトリCのURLはgit@github.com:maroontress-tomohisa/private-lfs-repository-example.gitとする。今度も同様に、リポジトリCには適切にデプロイキーのパブリックキーが設定され、リポジトリAのシークレットに適切にデプロイキーのプライベートキーが設定されていて、それをワークフロー内ではsecret.PRIVATE_LFS_REPO_DEPLOY_KEYで参照できるようになっているとしよう。

素直に考えると、前のセクションと同じように、URLとシークレットの変数だけ変更すればできそうだ。次のようにステップを実行する:

    steps:
    - name: Start ssh-agent
      shell: bash
      run: |
        mkdir -p $HOME/.ssh
        cat << EOF > $HOME/.ssh/known_hosts
        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
        EOF
        eval `ssh-agent`
        echo SSH_AUTH_SOCK="$SSH_AUTH_SOCK" >> "$GITHUB_ENV"
        echo SSH_AGENT_PID="$SSH_AGENT_PID" >> "$GITHUB_ENV"
    - name: Add a deploy key
      shell: bash
      run: |
        echo "${{secrets.PRIVATE_LFS_REPO_DEPLOY_KEY}}" > $HOME/.ssh/PRIVATE_LFS_REPO_DEPLOY_KEY
        ssh-add $HOME/.ssh/PRIVATE_LFS_REPO_DEPLOY_KEY
    - name: List fingerprints
      shell: bash
      run: |
        ssh-add -l
    - name: Clone a private repository with LFS
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-lfs-repository-example.git
        cat private-lfs-repository-example/README.md
        unzip -v private-lfs-repository-example/empty.zip

GitHubで見る

プライベートリポジトリCのルートにはempty.zipがあるので、最後の行でそれを確認できれば成功だ。結果は次のようになった:

Agent pid 56
⋮
Identity added: /c/Users/runneradmin/.ssh/PRIVATE_LFS_REPO_DEPLOY_KEY (git@github.com:maroontress-tomohisa/private-lfs-repository-example.git)
⋮
3072 SHA256:61EFfTJR56r9rX3u9EGG/HrvPcejWJTR0VLssfIpBzg git@github.com:maroontress-tomohisa/private-lfs-repository-example.git (RSA)
⋮
Cloning into 'private-lfs-repository-example'...
# An Example of Private Repository with LFS
Archive:  private-lfs-repository-example/empty.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2023-10-06 06:44 00000000  empty
--------          -------  ---                            -------
       0                0   0%                            1 file

GitHub Actionsの結果

成功した。

少し前までは、この段階でも環境変数GIT_SSHの設定が必要だったのだが、現在はその必要が無くなった。

なお、actions/checkoutを用いる場合は、次のようにオプションlfs: trueの追加が必要になることに注意:

    steps:
    - name: Checkout private LFS repository
      uses: actions/checkout@v4
      with:
        repository: maroontress-tomohisa/private-lfs-repository-example
        ssh-key: ${{secrets.PRIVATE_LFS_REPO_DEPLOY_KEY}}
        lfs: true
        path: private-lfs-repository-example

GitHubで見る

GitHub Actionsの結果

別の複数のプライベートリポジトリをクローンする

次に、二つのリポジトリBとCを、リポジトリAのジョブから読み出したい。actions/checkoutを使うなら二つのステップを並べれば良いが、ssh-agentを使ってもできるのだろうか。

ssh-agentssh-add同じホストのキーを複数追加することはできる。しかし、sshは、接続に失敗したら次のキーを試す、のように接続先に順番にキーを適用するだけである。マーケットプレイスのwebfactory/ssh-agentの説明を引用すると:

ただし、注意点が1つあります。SSHサーバーは、多数の一致しないキーが提示された後に接続試行を中断する可能性があります。そのため、たとえば、6つの異なるキーがssh-agentにロードされていて、5つの不明なキーの後にサーバーが中断した場合、最後のキー(正しいキーかもしれません)は決して試されません。

だから、ssh-agentを用いて同じホストの複数のリポジトリをチェックアウトするなら、次のように「デプロイキーの登録、チェックアウト、デプロイキーの削除」を繰り返す必要がある:

  • リポジトリBのキーをssh-addで登録
  • リポジトリBをクローン
  • リポジトリBのキーをssh-add -dで削除
  • リポジトリCのキーをssh-addで登録
  • リポジトリCをクローン
  • リポジトリCのキーをssh-add -dで削除

ところで、ローカルでビルドする際は、デプロイキーを用いることなく、二つのリポジトリをクローンできるのが望ましい。ワークフローファイルでもローカルと同じような手順でチェックアウトしたいので、ssh-agentを用いるのをやめて、別の方法を試してみよう。

リポジトリBとCは、同じホストgithub.comにあるので、ちょっとした工夫が必要になる(GitHub Actionsに限った話ではなく、良く知られた話なので、詳細はChatGPTにでも尋ねてほしい)。要約すると、git configurl.<base>.insteadOfの機能と、~/.ssh/configHost及びHostnameの機能を使って、Gitのレイヤーでリポジトリ毎にユニークな偽ホスト名を割り当て、SSHのレイヤーで偽ホスト名をgithub.comに変換し、同時にその偽ホストに対応するリポジトリのSSHキーを関連付ける、という技法である。具体的には次のようになる:

    steps:
    - name: Create ~/.ssh/known_hosts
      shell: bash
      run: |
        mkdir -p $HOME/.ssh
        cat << EOF > $HOME/.ssh/known_hosts
        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
        EOF
    - name: Add deploy keys
      shell: bash
      run: |
        add_key() {
          key="$HOME/.ssh/$1"
          win_key="$(cygpath -w $key)"
          echo "$2" > "$key"
          ssh-keygen -y -f $key > $key.pub
          read a b comment < $key.pub
          echo comment: $comment
          url="${comment%.*}"
          echo url: $url
          host_path="${url#*@}"
          new_host_path="$1.${host_path}"
          new_url="git@$new_host_path"
          echo git config --global url."${new_url}".insteadOf "${url}"
          git config --global url."${new_url}".insteadOf "${url}"
          cat << EOF >> $HOME/.ssh/config

        Host ${new_host_path%%:*}
          HostName github.com
          IdentityFile $win_key
          IdentitiesOnly yes
        EOF
        }
        add_key PRIVATE_REPO_DEPLOY_KEY "${{secrets.PRIVATE_REPO_DEPLOY_KEY}}"
        add_key PRIVATE_LFS_REPO_DEPLOY_KEY "${{secrets.PRIVATE_LFS_REPO_DEPLOY_KEY}}"
    - name: Print git config
      shell: bash
      run: git config --global --list
    - name: Print ssh config
      shell: bash
      run: cat $HOME/.ssh/config
    - name: Clone a private repository
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-repository-example.git
        cat private-repository-example/README.md
    - name: Clone another private repository with LFS
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-lfs-repository-example.git
        cat private-lfs-repository-example/README.md
        unzip -v private-lfs-repository-example/empty.zip

GitHubで見る

少し長いが、それほど難しくはない。Add deploy keysのステップで、git configによる設定変更と~/.ssh/configのエントリ作成を、デプロイキーごとに実施する。前のセクションで言及したSSHキーのコメントはここで使用する。鍵にコメントとしてURLを埋め込んでおくことで、「キーとURL」の組みを記述せずに済んでいる。結果は次のようになった:

comment: git@github.com:maroontress-tomohisa/private-repository-example.git
url: git@github.com:maroontress-tomohisa/private-repository-example
git config --global url.git@PRIVATE_REPO_DEPLOY_KEY.github.com:maroontress-tomohisa/private-repository-example.insteadOf git@github.com:maroontress-tomohisa/private-repository-example
comment: git@github.com:maroontress-tomohisa/private-lfs-repository-example.git
url: git@github.com:maroontress-tomohisa/private-lfs-repository-example
git config --global url.git@PRIVATE_LFS_REPO_DEPLOY_KEY.github.com:maroontress-tomohisa/private-lfs-repository-example.insteadOf git@github.com:maroontress-tomohisa/private-lfs-repository-example
⋮
url.git@PRIVATE_REPO_DEPLOY_KEY.github.com:maroontress-tomohisa/private-repository-example.insteadof=git@github.com:maroontress-tomohisa/private-repository-example
url.git@PRIVATE_LFS_REPO_DEPLOY_KEY.github.com:maroontress-tomohisa/private-lfs-repository-example.insteadof=git@github.com:maroontress-tomohisa/private-lfs-repository-example
⋮

Host PRIVATE_REPO_DEPLOY_KEY.github.com
  HostName github.com
  IdentityFile C:\Users\runneradmin\.ssh\PRIVATE_REPO_DEPLOY_KEY
  IdentitiesOnly yes

Host PRIVATE_LFS_REPO_DEPLOY_KEY.github.com
  HostName github.com
  IdentityFile C:\Users\runneradmin\.ssh\PRIVATE_LFS_REPO_DEPLOY_KEY
  IdentitiesOnly yes
⋮
Cloning into 'private-repository-example'...
# An Example of Private Repository
⋮
Cloning into 'private-lfs-repository-example'...
# An Example of Private Repository with LFS
Archive:  private-lfs-repository-example/empty.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2023-10-06 06:44 00000000  empty
--------          -------  ---                            -------
       0                0   0%                            1 file

GitHub Actionsの結果

git configの設定内容と、~/.ssh/configの中身を確認してから、リポジトリBとCをチェックアウトした。正しくチェックアウトできている。

GitHub公式ドキュメントでも“1つのサーバー上で複数のリポジトリを利用する”で複数のデプロイキーを扱う別の方法を紹介している。しかし、その方法だと、ローカルの~/.ssh/configも変更することになるし、git cloneの際にリポジトリのURLとして偽のURLを指定することになる。また別の面倒を引き寄せたくなければ、避けたほうが良い。

webfactory/ssh-agentで別の複数のプライベートリポジトリをクローンする

しかし、このような「工夫」をワークフローファイルに記述するのは煩雑だ。同じことをマーケットプレイスのwebfactory/ssh-agentを次のように使って実現してみよう:

    steps:
    - name: webfactory/ssh-agent
      uses: webfactory/ssh-agent@v0.8.0
      with:
        ssh-private-key: |
          ${{secrets.PRIVATE_REPO_DEPLOY_KEY}}
          ${{secrets.PRIVATE_LFS_REPO_DEPLOY_KEY}}

GitHubで見る

結果は次のようになった:

Starting ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-QrHWdhFo8ceQ/agent.1095
SSH_AGENT_PID=1096
Adding private key(s) to agent
Identity added: (stdin) (git@github.com:maroontress-tomohisa/private-repository-example.git)
Identity added: (stdin) (git@github.com:maroontress-tomohisa/private-lfs-repository-example.git)
Key(s) added:
3072 SHA256:EHYsJhMvV2X03sbEYcAH3w7MNft1lra8M/ZSF0XMr5k git@github.com:maroontress-tomohisa/private-repository-example.git (RSA)
3072 SHA256:61EFfTJR56r9rX3u9EGG/HrvPcejWJTR0VLssfIpBzg git@github.com:maroontress-tomohisa/private-lfs-repository-example.git (RSA)
Configuring deployment key(s)
Added deploy-key mapping: Use identity 'C:\Users\runneradmin/.ssh/key-43749e3def49002289a25278b6d8d5a0b8fed7f2c33f26750fe6233c114a1c39' for GitHub repository maroontress-tomohisa/private-repository-example
Added deploy-key mapping: Use identity 'C:\Users\runneradmin/.ssh/key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9' for GitHub repository maroontress-tomohisa/private-lfs-repository-example

GitHub Actionsの結果

簡単になったが、副作用としてssh-agentがバックグラウンドで開始する。次のステップで確認してみる:

    - name: List fingerprints
      shell: bash
      run: ssh-add -l

GitHubで見る

結果は次のようになった:

3072 SHA256:EHYsJhMvV2X03sbEYcAH3w7MNft1lra8M/ZSF0XMr5k git@github.com:maroontress-tomohisa/private-repository-example.git (RSA)
3072 SHA256:61EFfTJR56r9rX3u9EGG/HrvPcejWJTR0VLssfIpBzg git@github.com:maroontress-tomohisa/private-lfs-repository-example.git (RSA)

GitHub Actionsの結果

git configの設定内容と、~/.ssh/configの中身も確認してみよう:

    - name: Print git config
      shell: bash
      run: git config --global --list
    - name: Print ssh config
      shell: bash
      run: cat $HOME/.ssh/config

GitHubで見る

結果は次のようになった:

url.git@key-43749e3def49002289a25278b6d8d5a0b8fed7f2c33f26750fe6233c114a1c39.github.com:maroontress-tomohisa/private-repository-example.insteadof=https://github.com/maroontress-tomohisa/private-repository-example
url.git@key-43749e3def49002289a25278b6d8d5a0b8fed7f2c33f26750fe6233c114a1c39.github.com:maroontress-tomohisa/private-repository-example.insteadof=git@github.com:maroontress-tomohisa/private-repository-example
url.git@key-43749e3def49002289a25278b6d8d5a0b8fed7f2c33f26750fe6233c114a1c39.github.com:maroontress-tomohisa/private-repository-example.insteadof=ssh://git@github.com/maroontress-tomohisa/private-repository-example
url.git@key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9.github.com:maroontress-tomohisa/private-lfs-repository-example.insteadof=https://github.com/maroontress-tomohisa/private-lfs-repository-example
url.git@key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9.github.com:maroontress-tomohisa/private-lfs-repository-example.insteadof=git@github.com:maroontress-tomohisa/private-lfs-repository-example
url.git@key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9.github.com:maroontress-tomohisa/private-lfs-repository-example.insteadof=ssh://git@github.com/maroontress-tomohisa/private-lfs-repository-example
⋮

Host key-43749e3def49002289a25278b6d8d5a0b8fed7f2c33f26750fe6233c114a1c39.github.com
    HostName github.com
    IdentityFile C:\Users\runneradmin/.ssh/key-43749e3def49002289a25278b6d8d5a0b8fed7f2c33f26750fe6233c114a1c39
    IdentitiesOnly yes

Host key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9.github.com
    HostName github.com
    IdentityFile C:\Users\runneradmin/.ssh/key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9
    IdentitiesOnly yes

GitHub Actionsの結果

git configに関してはエントリが前のセクションの三倍に増えているが、git@github.com:で始めるSSH形式のURLに加えて、HTTPS形式のURL、ssh://で始まるSSH+GIT形式のURLも変換の対象になっている。あとは、偽ホストの名前にハッシュ値が使われてることを除いて、前のセクションの説明と違いは無い。

ではクローンしてみよう:

    - name: Clone the private repository (which fails)
      shell: bash
      continue-on-error: true
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-repository-example.git

GitHubで見る

結果は次のようになった:

Cloning into 'private-repository-example'...
Host key verification failed.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
Error: Process completed with exit code 128.

GitHub Actionsの結果

webfactory/ssh-agentの説明によると、次のように~/.ssh/known_hostsを生成するという記述がある:

This action

  • configures known_hosts for GitHub.com.

しかし、Windowsランナーではどうやらうまくできないようだ。自分で作成してもう一度試してみよう:

    - name: Perform workarounds
      shell: bash
      run: |
        mkdir -p $HOME/.ssh
        cat << EOF > $HOME/.ssh/known_hosts
        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
        EOF
    - name: Clone a private repository
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-repository-example.git
        cat private-repository-example/README.md
    - name: Clone another private repository with LFS
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-lfs-repository-example.git
        cat private-lfs-repository-example/README.md
        unzip -v private-lfs-repository-example/empty.zip

GitHubで見る

結果は次のようになった:

Cloning into 'private-repository-example'...
# An Example of Private Repository
⋮
Cloning into 'private-lfs-repository-example'...
# An Example of Private Repository with LFS
Archive:  private-lfs-repository-example/empty.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2023-10-06 06:44 00000000  empty
--------          -------  ---                            -------
       0                0   0%                            1 file

GitHub Actionsの結果

成功した。そして、やっと本題に入れる。

🚧

webfactory/ssh-agentを本格的に使用する場合、リポジトリをそれぞれのオーガナイゼーションにコピー(フォーク)してから使うべきである。攻撃者がwebfactoryのアカウントを乗っ取り、マーケットプレイスのアクションを悪意をもって書き換え、プライベートキーとそのプライベートリポジトリのURLを抜いてしまう、というリスクを排除するためである。これはwebfactoryに限らず、GitHub公式のアクション以外の全てのサードパーティ製アクションに当てはまることだ。

参考までに、アクションのリポジトリをプライベートにコピーして使う設定例を次に示す:

コピーしたアクションを使用するプライベートリポジトリの設定

Settings ➜ Actions ➜ General ➜ Actions permissions で “Allow your-organization, and select non-your-organization, actions and reusable workflows” を選択して保存する。

このようにプライベートリポジトリを設定すると、自分のオーガナイゼーションのアクション(コピーしたサードパーティアクション)、GitHub公式のアクション、マーケットプレイスの「verified creator」のアクションだけが利用可能になる。それ以外のサードパーティアクションを指定するとワークフロー実行時にエラーになる。

もしくは、自分のオーガナイゼーションのリポジトリにコピーする代わりに、バージョンをSHA-1ハッシュで指定しても良い。ただし、その場合でも根本的なビジネスのリスクとして、依存している公開されているアクションが突然消失する可能性(例: 開発元の作成者がリポジトリを消してしまう、など)を認識しておくべきである。さらに、プライベートなコピーにしろSHA-1ハッシュ指定にしろ、使うアクションがその依存関係により参照する別の「何か」が問題にならないかについて注意する必要もある。セキュリティ強化に関する詳細は、公式の「Using third-party actions — Security hardening for GitHub Actions」を参照。

混ぜるな危険

最後に、マーケットプレイスのactions/checkoutとwebfactory/ssh-agentを組み合わせて、プライベートリポジトリをチェックアウトしてみよう。最初に、webfactory/ssh-agentでリポジトリCをチェックアウトするための準備として、デプロイキーを次のように登録する:

    steps:
    - name: webfactory/ssh-agent
      uses: webfactory/ssh-agent@v0.8.0
      with:
        ssh-private-key: |
          ${{secrets.PRIVATE_LFS_REPO_DEPLOY_KEY}}
    ⋮
    - name: Perform workarounds
      shell: bash
      run: |
        mkdir -p $HOME/.ssh
        cat << EOF > $HOME/.ssh/known_hosts
        github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=
        EOF

GitHubで見る

結果は次のようになった:

Starting ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-bGHWNwHc6WoG/agent.1649
SSH_AGENT_PID=1650
Adding private key(s) to agent
Identity added: (stdin) (git@github.com:maroontress-tomohisa/private-lfs-repository-example.git)
Key(s) added:
3072 SHA256:61EFfTJR56r9rX3u9EGG/HrvPcejWJTR0VLssfIpBzg git@github.com:maroontress-tomohisa/private-lfs-repository-example.git (RSA)
Configuring deployment key(s)
Added deploy-key mapping: Use identity 'C:\Users\runneradmin/.ssh/key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9' for GitHub repository maroontress-tomohisa/private-lfs-repository-example
⋮

GitHub Actionsの結果

あとはリポジトリCをチェックアウトするだけだが、その前にactions/checkoutでリポジトリBをチェックアウトする:

    - name: Checkout a private repository with actions/checkout
      uses: actions/checkout@v4
      with:
        repository: maroontress-tomohisa/private-repository-example
        ssh-key: ${{secrets.PRIVATE_REPO_DEPLOY_KEY}}
        path: private-repository

GitHubで見る

結果は次のようになった:

Syncing repository: maroontress-tomohisa/private-repository-example
Getting Git version info
Copying 'C:\Users\runneradmin\.gitconfig' to 'D:\a\_temp\d52f99d9-b40f-4405-a929-b1a18384d9db\.gitconfig'
Temporarily overriding HOME='D:\a\_temp\d52f99d9-b40f-4405-a929-b1a18384d9db' before making global git config changes
Adding repository directory to the temporary git global config as a safe directory
"C:\Program Files\Git\bin\git.exe" config --global --add safe.directory D:\a\try_out_github_actions\try_out_github_actions\private-repository
Initializing the repository
Disabling automatic garbage collection
Setting up auth
Determining the default branch
Fetching the repository
Determining the checkout info
Checking out the ref
"C:\Program Files\Git\bin\git.exe" log -1 --format='%H'
'3cf9492e7ffb20ea5246a8edf1bec8d3aab293a4'

GitHub Actionsの結果

成功した。続いて、リポジトリCをクローンしてみよう:

    - name: List fingerprints after actions/checkout (which fails)
      continue-on-error: true
      shell: bash
      run: ssh-add -l
    - name: Clone another private repository with LFS (which fails)
      continue-on-error: true
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-lfs-repository-example.git
        cat private-lfs-repository-example/README.md
        unzip -v private-lfs-repository-example/empty.zip

GitHubで見る

本来は不要だが、git cloneの前のステップではssh-addを用いてssh-agentの状態を確認している。結果は次のようになった:

Error connecting to agent: Bad file descriptor
Error: Process completed with exit code 2.
⋮
Cloning into 'private-lfs-repository-example'...
Load key "C:\\Users\\runneradmin/.ssh/key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9": error in libcrypto
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
Error: Process completed with exit code 128.

GitHub Actionsの結果

git cloneは失敗した。その前のgit-add -lも失敗している。actions/checkoutを実行した結果、バックグラウンドで実行中のssh-agentの状態が壊れた(プロセスは存在しているが、プロセス間通信のステートがおかしくなった)のだろう。

とりあえず、次のようにssh-agentを再起動してから、再度クローンしてみよう:

    - name: Perform more workarounds (kill ssh-agent to restart)
      shell: bash
      run: |
        eval `ssh-agent -k`
        # The following lines are placebos (because we can't unset env.*):
        echo SSH_AUTH_SOCK= >> "$GITHUB_ENV"
        echo SSH_AGENT_PID= >> "$GITHUB_ENV"
        # See https://github.com/actions/runner/issues/1126
    - name: webfactory/ssh-agent
      uses: webfactory/ssh-agent@v0.8.0
      with:
        ssh-private-key: |
          ${{secrets.PRIVATE_LFS_REPO_DEPLOY_KEY}}
    - name: List fingerprints (after restarting ssh-agent)
      shell: bash
      run: ssh-add -l
    - name: Clone another private repository with LFS
      shell: bash
      run: |
        git clone --depth 1 git@github.com:maroontress-tomohisa/private-lfs-repository-example.git
        cat private-lfs-repository-example/README.md
        unzip -v private-lfs-repository-example/empty.zip

GitHubで見る

結果は次のようになった:

unset SSH_AUTH_SOCK;
unset SSH_AGENT_PID;
echo Agent pid 1650 killed;
⋮
Starting ssh-agent
SSH_AUTH_SOCK=/tmp/ssh-nR7bVfNdAwqI/agent.766
SSH_AGENT_PID=767
Adding private key(s) to agent
Identity added: (stdin) (git@github.com:maroontress-tomohisa/private-lfs-repository-example.git)
Key(s) added:
3072 SHA256:61EFfTJR56r9rX3u9EGG/HrvPcejWJTR0VLssfIpBzg git@github.com:maroontress-tomohisa/private-lfs-repository-example.git (RSA)
Configuring deployment key(s)
Added deploy-key mapping: Use identity 'C:\Users\runneradmin/.ssh/key-aabbaf196b10644a69c23360df933575c9e9d496cfc251dabf9b22ee13d7bea9' for GitHub repository maroontress-tomohisa/private-lfs-repository-example
⋮
3072 SHA256:61EFfTJR56r9rX3u9EGG/HrvPcejWJTR0VLssfIpBzg git@github.com:maroontress-tomohisa/private-lfs-repository-example.git (RSA)
⋮
Cloning into 'private-lfs-repository-example'...
# An Example of Private Repository with LFS
Archive:  private-lfs-repository-example/empty.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2023-10-06 06:44 00000000  empty
--------          -------  ---                            -------
       0                0   0%                            1 file

GitHub Actionsの結果

ssh-agentを再起動すると、確かに全てのエラーは消えた。さて、なぜこのようなことが起きるのか調べてみよう。

actions/checkoutが何故起動済みのssh-agentを「壊す」のだろうか。actions/checkoutのログを見てみると、次の行に気づく:

▾Setting up auth
  ⋮
  "C:\Program Files\Git\bin\git.exe" config --local core.sshCommand "\"C:\Windows\System32\OpenSSH\ssh.exe\" -i \"$RUNNER_TEMP/455b31bf-8bfa-4d87-bb2e-3d92b700ba9a\" -o StrictHostKeyChecking=yes -o CheckHostIP=no -o \"UserKnownHostsFile=$RUNNER_TEMP/455b31bf-8bfa-4d87-bb2e-3d92b700ba9a_known_hosts\""

GitHub Actionsの結果

なんと、actions/checkoutは、デプロイキーを指定した場合、SSHにC:\Windows\System32\OpenSSH\ssh.exeを使用するようだ。Windowsにはsshの実装がいくつかあるが、OpenSSH版の実装とGit for Windows版の実装は互換性が無い。OpenSSHのsshとGit for Windowsのssh-agentを一緒に使うことはできない。互換性が無い理由はちゃんと調べてないが、おそらくプロセス間通信で使うプロトコルが違うのだろう(だとすると、何故同じ環境変数を使っているのだろう? よく経緯はわからないが、まともではないことは間違いない)。

まとめると、Windowsランナーの場合:

  • bashからssh-agentを起動すると、Git for Windowsの実装の実行ファイルがバックグラウンドで動作する(ように環境変数PATHが設定されている)。
  • actions/checkoutはオプションでデプロイキーを指定すると、gitがSSHの実装としてC:\Windows\System32\OpenSSH\ssh.exeを使用するようになる。
  • OpenSSHのssh.exeは環境変数SSH_AUTH_SOCKSSH_AGENT_PIDの値を見て、OpenSSH版ssh-agentが実行中と判断して、プロセス間通信を開始する。しかし、起動しているのはGit for Windows版のエージェントなので、状態が壊れる(多分)。

次のような回避方法がある:

  • 先にactions/checkout、その後にssh-agent(またはwebfactory/ssh-agent)という順番になるように徹底する。
  • ssh-agentはOpenSSH版(C:\Windows\System32\OpenSSH\ssh-agent.exe)を使う。
  • 複数のデプロイキーを使うだけなら、ssh-agentを起動しないようにする。

最初の二つの選択肢は、別の混乱を招く可能性が高いので、やめておいた方が良い。そもそも、webfactory/ssh-agentは、その名前が表すように、ssh-agentを使うためのものだ。だから、複数のデプロイキーを扱うためだけに使うのは悪用なのではないだろうか。パッと探しただけだと、マーケットプレイスには複数のデプロイキーを扱うだけのアクションは見つからなかった。だとすると、アクションを自作(して、公開)するのも良さそうだ。