GitLab CIでパスフレーズ付き秘密鍵のSSH接続で困ったことと解決策

# 困ったこと
以前 [GitLab CIからEC2インスタンスへのセキュアなSSH接続方法](https://qiita.com/n11sh1/items/9f3794c02e185ca3abb3) を投稿しました。

https://qiita.com/n11sh1/items/9f3794c02e185ca3abb3

この時はパスフレーズ無し秘密鍵を使用したので簡単にセットアップできたのですが、
パスフレーズ付き秘密鍵を使用した場合にハマりどころがありました。

__困りポイント1 :sob: GitLab CI実行時にパスフレーズ入力するノウハウがネット上に少ない__
Dockerエグゼキューターを使用して、よくあるEC2インスタンスへのSSH接続を実現しようとすると以下のような .gitlab.yml になります。
`before_script`で`ssh-agent`を起動、CI/CD変数に設定した秘密鍵を登録して、後はいつも通り`ssh`を使う形式です。

```yaml:.gitlab-ci.yml
image: ubuntu

before_script:
  - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )'
  - eval $(ssh-agent -s)
  - mkdir -p ~/.ssh
  - chmod 700 ~/.ssh
  - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
  - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'

Test SSH:
  script:
    - ssh -v ec2-user@xxx.xxx.xxx "whoami"
```

パスフレーズ無し秘密鍵であれば難なくsshを使えるのですが、
パスフレーズ付き秘密鍵の場合ジョブに以下のようなログが表示されます。（sshコマンドに-vオプションを追加してデバッグメッセージを表示）

```
$ ssh -v ec2-user@xxx.xxx.xxx
OpenSSH_8.4p1 Debian-5, OpenSSL 1.1.1n  15 Mar 2022
--- （省略） ---
debug1: Next authentication method: publickey
debug1: read_passphrase: can't open /dev/tty: No such device or address
debug1: No more authentication methods to try.
ec2-user@xxx.xxx.xxx: Permission denied (publickey,gssapi-keyex,gssapi-with-mic).
```
> debug1: read_passphrase: can't open /dev/tty: No such device or address

ターミナルだと「Enter passphrase for key 'xxx.pem': 」とプロンプトが表示されてパスフレーズを入力できるのですが、
GitLab CI/CDの場合プロンプトが表示されず、ジョブ失敗となります。

「gitlab ci read_passphrase」「gitlab ci enter passphrase for key」でググったり、
sshコマンドのオプション、sshpassコマンド（sshコマンドのパスワードを手動ではなく、あらかじめ決めた方法で入力するコマンド）、
expectコマンド（スクリプトの指示に従って、対話的なプログラムと会話するプログラム）
など色々調べて設定してみたが、どれも上手くいきませんでした・・・ 

__困りポイント2 :sob: そもそもGitLab CI公式ドキュメントがパスフレーズを付けるなと言っている__

https://docs.gitlab.com/ee/ci/ssh_keys/

GitLab CI/CDでのSSH公開鍵認証の使い方の公式ドキュメントを読むと、そもそも秘密鍵にパスフレーズを付けるな、と記載がありました。
> Generate the SSH key pair as described in the instructions to generate an SSH key. __Do not__ add a passphrase to the SSH key, or the before_script will prompt for it.

詰みました・・・

（ここまでの所要時間：６時間）

---

諦めてパスフレーズ無し秘密鍵を作成しようと思った時、神Issueが舞い降りてきましたっ :angel:

https://gitlab.com/gitlab-examples/ssh-private-key/-/issues/7#note_558846462

>訳）
> 保護されていない ssh-key を使うという悪い習慣に人々が群がっているのを見るのは残念です。これは、このサンプルのメンテナの知識不足によるものでしょう。もし ssh-key にパスフレーズを使わないのなら、わざわざ ssh-agent をインストールする必要はないでしょう。
>
> 2つの変数、ひとつは秘密鍵、もうひとつはパスフレーズを使ってこれを行う方法を示す pull-request を作成します。
>
> ssh-add のマニュアルページの ENVIRONMENT セクションを読むと、解決への鍵が見つかります。


# 解決策

https://gitlab.com/gitlab-examples/ssh-private-key/-/merge_requests/21

> 訳）
> この pull-request は .gitlab-ci.yml に、暗号化された ssh-key を使えるようにする機能を追加します。SSH_PASSPHRASE という変数を作成する必要があります。これは ssh-agent に秘密鍵を追加するときに使用します。
>
> ssh-add のマニュアルページの ENVIRONMENT セクションに ssh-add をスクリプト化する方法が説明されています。これは、before_script: セクションで設定される2つの変数を使用して行われます。DISPLAY=None を設定すると、X11 の必要性がなくなります。SSH_ASKPASS は ssh-add が標準入力から読み込むように指示されたときに呼び出される 実行ファイルです。
>
> したがって、SSH_ASKPASS は単純なシェルスクリプトで、変数 SSH_PASSPHRASE の値を stdout に出力するだけなので、これを ssh-add にパイプすればいいわけです。

```diff_yaml:.gitlab-ci.yml
image: ubuntu

before_script:
  - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client git -y )'
  - eval $(ssh-agent -s)
  - mkdir -p ~/.ssh
  - chmod 700 ~/.ssh
- - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -
+ - echo 'echo $SSH_PASSPHRASE' > ~/.ssh/tmp && chmod 700 ~/.ssh/tmp
+ - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | DISPLAY=None SSH_ASKPASS=~/.ssh/tmp ssh-add -
  - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config'

Test SSH:
  script:
    - ssh -v ec2-user@xxx.xxx.xxx "whoami"
```

```
$ ssh -v ec2-user@xxx.xxx.xxx
OpenSSH_8.4p1 Debian-5, OpenSSL 1.1.1n  15 Mar 2022
--- （省略） ---
debug1: Next authentication method: publickey
debug1: Offering public key: (stdin) RSA SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx agent
debug1: Server accepts key: (stdin) RSA SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxx agent
debug1: Authentication succeeded (publickey).
```

CI/CD変数に秘密鍵とパスフレーズを追加すれば、パスフレーズ付き秘密鍵でSSH接続できるようになりました！

（完）