CircleCIで同じHostnameのSSH鍵を複数追加したい

Add multiple SSH deploy keys with same hostname on CircleCI

# tl;dr

対象の現象は **CircleCIとGithubに登録するSSH鍵は間違っていないのに権限系のエラーが出る** です。

```console:error
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```

このエラーのパターンの1つとして、以下のことを確認しました。

- CircleCIで同じドメインのSSH鍵を複数追加すると`.ssh/config`中の優先順位で後から追加された鍵が利用できない
- 同じドメインの複数のprivate repositoryにアクセスする場合はMachine Userを作成することが推奨されている
- Machine Userを作りたくない場合のワークアラウンドを調査

# CircleCI

CIを実行してくれるサービスです。
ここではCircleCIの概要について詳しくは述べませんが、github等と連携して読み込んだソースコードの自動テストやbuildをする基盤を提供しています。
https://circleci.com/

# SSH Permission

CircleCI + GithubにおけるSSH Permissionのおさらいです。
bitbucketや他のgit registryでもおよそ同じことが言えると思いますが、確認はしていません。
知ってる人は読み飛ばしてOK

> CircleCI に SSH 公開鍵を登録する必要があるケースは、以下の 2 パターンです。

> 1. バージョン管理システムからコードをチェックアウトする
> 2. 実行中のプロセスが他のサービスにアクセスできるようにする

[公式サイト](https://circleci.com/docs/en/2.0/add-ssh-key/)に記載されているように、privateなsubmoduleや依存先repositoryをビルド時にひっぱるために鍵が必要です。
鍵には2種類あり、CircleCIのプロジェクトごとに設定することができます。
1. Deploy Key
2. User Key

## Deploy Key

Deploy KeyはGithubの概念です。
Githubの**repositoryごと**に作成し、repository単位でRead/Writeアクセスを制御します。

例えば以下の画像では、あるrepositoryに２つのDeploy Keyが追加されています。
CircleCIという名前の鍵はCircleCIでこのrepositoryを追加したときに自動で作成されたものです。checkout keyとも呼ばれます。
[公式ドキュメント](https://github.blog/2015-06-16-read-only-deploy-keys/)
![スクリーンショット 2019-08-15 10.51.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/8df8addd-0a8a-aa9a-44c8-fab3a870c918.png)

あるrepositoryのDeploy Keyの秘密鍵をCircleCIのProjectに追加することによって、ProjectはそのrepositoryのRead/Write権限を得ることができます。
Deploy Keyを登録するためには、Projectの設定画面のSSH Permissionsで追加します。その際、Hostname欄でどのドメインで利用する鍵かを指定することができます。Hostnameを空欄にすると全てのドメインで利用されます。
![スクリーンショット 2019-08-15 11.31.49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/d7fa8377-17ee-1534-c727-e283c6b1eac7.png)


## User Key

User KeyもGithubの概念です。
User Keyは**ユーザごと**に作成し、Githubのユーザと同等のRead/Write権限を持つため、User Keyを使用すると様々なrepositoryへのアクセスが可能です。
![スクリーンショット 2019-08-15 11.01.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/b097995b-f72e-998d-e6f7-35b6f624c153.png)

CircleCIでUser Keyを利用するためには、Projectの設定画面のCheckout SSH keysを選択し、Create and add [Githubユーザ名] user keyを実行します。するとGithubのUser Keyが作成されその秘密鍵がProjectに追加されます。
User Keyはユーザと同じ権限を持つ便利な鍵です。一方CircleCIでは追加された秘密鍵をProject内から見ることができてしまうため、チームのProjectやconfigを把握していないProjectに自身のUser Keyを追加する事には慎重になる必要があります。
![スクリーンショット 2019-08-15 10.57.00.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/b5298eb9-e42f-f835-5396-95b06679add6.png)

## CircleCIでのSSH鍵の使用

追加した鍵はDeploy keyもUser keyも次のようにして利用できます。
https://circleci.com/docs/2.0/add-ssh-key/

```yaml:.circleci/config.yml

version: 2
jobs:
  deploy-job:
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "SO:ME:FIN:G:ER:PR:IN:T"
      - run:
          name: pull submodule
          command: git submodule update --init
```

# 問題

1つのProjectで複数のprivate repositoryを扱う場合はどのようにしたら良いでしょうか?

例えば次のようなケースを考えます。
Github上にある2つのprivate repository(`submodule_1`, `submodule_2`)をsubmoduleとして持つrepository(`circleci_test`)があったときに、CircleCIでsubmoduleを含めてcircleci_testをbuildするためにはどのような設定したら良いでしょうか?

```console:こういうやつ
$ cat .gitmodules
[submodule "submodule_1"]
        path = submodule_1
        url = git@github.com:ymaki/submodule_1.git
[submodule "submodule_2"]
        path = submodule_2
        url = git@github.com:ymaki/submodule_2.git
```

## それぞれのDeploy KeyをCircleCIに追加しても機能しない

まずは次の画像のように2つのrepositoryのDeploy KeyをそれぞれCircleCIに追加するということを考えると思います。
![スクリーンショット 2019-08-15 12.03.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/56ecb413-6084-5aed-b084-931fb27a735d.png)

上記画像のように別々の秘密鍵をCircleCIの`circleci_test`Projectに追加した上で
- `7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b`の公開鍵をGithubの`submodule_1`のDeploy Keyに設定
- `29:ff:6c:d6:d5:5f:cc:92:b9:33:90:d9:63:33:c6:f7`の公開鍵をGithubの`submodule_2`のDeploy Keyに設定
します。

そして以下のようなconfig.ymlを実行します。
imageに`python:stretch`を利用していますが、Debianであればそう変わらないと思うので何でもいいです。

```yaml:.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: python:stretch
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"
            - "29:ff:6c:d6:d5:5f:cc:92:b9:33:90:d9:63:33:c6:f7"
      - run:
          name: pull submodule
          command: git submodule update --init
```

すると、下記の画像のようにadd_ssh_keysは成功するのですがsubmoduleを読み込む部分で **何故か** 失敗してしまいます。

![スクリーンショット 2019-08-15 13.33.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/1fd33d50-20f8-dbab-86f9-787716660925.png)

```console:エラー内容
#!/bin/bash -eo pipefail
git submodule update --init
Submodule 'submodule_1' (git@github.com:ymaki/submodule_1.git) registered for path 'submodule_1'
Submodule 'submodule_2' (git@github.com:ymaki/submodule_2.git) registered for path 'submodule_2'
Cloning into '/home/circleci/project/submodule_1'...
Cloning into '/home/circleci/project/submodule_2'...
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
fatal: clone of 'git@github.com:ymaki/submodule_2.git' into submodule path '/home/circleci/project/submodule_2' failed
Failed to clone 'submodule_2'.
```

よく見るとsubmodule_1へのアクセスは成功していますがsubmodule_2へのアクセスは失敗していることを心に留めておきます。

## 現象の調査

CircleCIはdebugのために実行中のcontainerにSSH接続できる機能を提供しています。
便利なので活用していきましょう。
https://circleci.com/docs/2.0/ssh-access-jobs/

![スクリーンショット 2019-08-15 13.58.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/2517173c-4958-892a-6456-7a42e2cf0cce.png)

先程失敗したjobのcontainerに入ってみます。

```console
circleci@a6bbcb66dd92:~# ls
project
circleci@a6bbcb66dd92:~# cd project/
circleci@a6bbcb66dd92:~/project# ls
submodule_1
```

先程失敗したところで止まっているため、`submodule_1`は引けてますが、`submodule_2`がありません。
まずは失敗したjobと同じコマンドを実行してみます。

```console

circleci@a6bbcb66dd92:~/project# git submodule update --init
Cloning into '/home/circleci/project/submodule_2'...
Warning: Permanently added the RSA host key for IP address '140.82.114.4' to the list of known hosts.
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
fatal: clone of 'git@github.com:ymaki/submodule_2.git' into submodule path '/home/circleci/project/submodule_2' failed
Failed to clone 'submodule_2'. Retry scheduled
Cloning into '/home/circleci/project/submodule_2'...
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
fatal: clone of 'git@github.com:ymaki/submodule_2.git' into submodule path '/home/circleci/project/submodule_2' failed
Failed to clone 'submodule_2' a second time, aborting
```

jobの結果と同じように失敗しました。

[上記のgitコマンドは`submodule_2`を引く際、sshプロトコルをを利用しています。](https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_ssh_protocol)
その際に利用するconfigや鍵はsshコマンドと同じく `~/.ssh` のものを利用しているため、ここを調べます。

```console
circleci@10e8c95f8f07:~# ls .ssh/
config  id_rsa  id_rsa_29ff6cd6d55fcc92b93390d96333c6f7  id_rsa_7a50914d0c1a98e17cc4784d873a397b  known_hosts
```

鍵が3本とconfig, known_hostsがあります。
鍵のファイル名のsuffixがfingerprintと一致するため、これがそれぞれのDeploy Keyだと想像できます。ちなみに`id_rsa`は`circleci_test`のcheckout keyです。

`~/.ssh/config`を見ましょう。

```
circleci@10e8c95f8f07:~# cat .ssh/config
Host github.com
  IdentitiesOnly yes
  IdentityFile /home/circleci/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b
Host github.com
  IdentitiesOnly yes
  IdentityFile /home/circleci/.ssh/id_rsa_29ff6cd6d55fcc92b93390d96333c6f7
```

`~/.ssh/config` の中に `Host github.com` 行が2行あることがわかりました。
これはマズいです。

OpenSSHのssh_config(5)によると、次のように書いてあります。

> For each parameter, the first obtained value will be used.

https://man.openbsd.org/ssh_config
https://linux.die.net/man/5/ssh_config

つまり各パラメータについて先に出てきた値が使われあとの値は無視されます。
今回の場合はgithub.comへのsshアクセスはどのrepositoryへのアクセスかによらず最初のブロックの
`IdentityFile /root/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b` が使われることになります。
ちなみに、次のようにssh-agentに鍵が登録されています。
1行目が`circleci_test`の読み込み権限を持つcheckout key、2行目3行目がそれぞれsubmoduleのDeploy Keyと思われますが、`Host github.com`ブロックで`IdentitiesOnly yes`が設定されているためこれらの鍵は使われま**せん**。

```console

circleci@26b1a44d8d1b:~$ ssh-add -l
2048 SHA256:Iv1F93xHKVfdpsngi5Zo6PuaQ+8aYTlxTIb1DbBp+M4  (RSA)
2048 SHA256:R+dN1DTPqR6CCIfFzDrWOAKUvRhDZ0/SkMeoYihNIrc  (RSA)
2048 SHA256:SZX2IepedP70lftv2gN8bhgMe/6TOkJqaQLVlniXWzg  (RSA)
```

上記より、`submodule_2`へのアクセスに`submodule_1`のdeploy keyを利用しようとして `Permission Denied`となってしまった。というのが今回起きた現象だとわかりました。

つまり、CircleCIの`add_ssh_keys`の仕様として、**同じドメイン用の秘密鍵を複数追加して同じcontainerの中で利用しようとしても、最初の鍵しか読まれずあとは失敗します。**

## CircleCIにおける鍵の扱い調査

対策を考える前にもう少し調査する必要があります。
`~/.ssh/config` へ意図しない設定がされていることが原因でしたが次の3点の状態を確認しておきます。

- containerが立ち上がった直後
- checkout後
- add_ssh_keys後

### containerが立ち上がった直後

次のconfigを実行してcontainerが立ち上がった直後の状態を確認します。

```yaml:add_ssh_keys前
version: 2
jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - run:
          name: fail
          command: "false"
```

```console
circleci@26b1a44d8d1b:~$ ls .ssh
ls: cannot access '.ssh': No such file or directory
circleci@26b1a44d8d1b:~$ ssh-add -l
2048 SHA256:Iv1F93xHKVfdpsngi5Zo6PuaQ+8aYTlxTIb1DbBp+M4  (RSA)
2048 SHA256:R+dN1DTPqR6CCIfFzDrWOAKUvRhDZ0/SkMeoYihNIrc  (RSA)
2048 SHA256:SZX2IepedP70lftv2gN8bhgMe/6TOkJqaQLVlniXWzg  (RSA)
```

初期状態では.sshフォルダすらありませんが、ssh-agentにDeploy Keyは登録されているようです。
1行目がcheckout key, 2行目と3行目はおそらくsubmoduleのDeploy Keyです。
Projectの設定でcheckout keyをUser Keyにしておくと、1行目がUser Keyになります。

次のようにssh-agentに追加されている秘密鍵では`circleci_test`にはアクセスできるが`submodule_1`と`submodule_2`にはアクセスできないという結果になりました。

```
circleci@184428427e0a:~$ git clone git@github.com:ymaki/circleci_test.git
Cloning into 'circleci_test'...
remote: Enumerating objects: 51, done.
remote: Counting objects: 100% (51/51), done.
remote: Compressing objects: 100% (25/25), done.
remote: Total 51 (delta 11), reused 50 (delta 10), pack-reused 0
Receiving objects: 100% (51/51), 5.20 KiB | 0 bytes/s, done.
Resolving deltas: 100% (11/11), done.
circleci@184428427e0a:~$ git clone git@github.com:ymaki/submodule_1.git
Cloning into 'submodule_1'...
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
circleci@184428427e0a:~$ git clone git@github.com:ymaki/submodule_2.git
Cloning into 'submodule_2'...
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
```

これはssh-agentの1行目のcheckout keyが`git@github.com`に対してsshプロトコルでは認証成功する一方でsubmoduleのRead権限は持っていないため、`ERROR: Repository not found.` となっています。もしsshでdenyされていれば次の鍵が試されるのですが、sshプロトコルとしては認証成功しているため２本目3本目の鍵は試行されない、ということだと推測しています。

ちなみに自身のUser Keyを追加してCircleCI上でpreferred keyに設定するとssh-agentの1行目の秘密鍵がUser Keyになり、`submodule_1`, `submodule_2`の読み出しが可能になります。

| repository | アクセス |
|:---:|:---:|
| circleci_test | 可 |
| submodule_1 | 不可 |
| submodule_2 | 不可 |

### checkout後

checkout stepで実行されているシェルスクリプトは実行結果に乗っています。
`Checkout code` を展開するとシェルスクリプトが見えますが、関係しているのは次の行です。

```sh

mkdir -p ~/.ssh

echo 'github.com ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkccKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81eFzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpIoaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==
bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAubiN81eDcafrgMeLzaFPsw2kNvEcqTKl/VqLat/MaB33pZy0y3rJZtnqwR2qOOvbwKZYKiEO1O6VqNEBxKvJJelCq0dTXWT5pbO2gDXC6h6QDXCaHo6pOHGPUy+YBaGQRGuSusMEASYiWunYN0vCAI8QaXnWMXNMdFP3jHAJH0eDsoiGnLPBlBp4TNm6rYI74nMzgz3B9IikW4WVK+dc8KZJZWYjAuORU3jc1c/NPskD2ASinf8v3xnfXeukU0sJ5N6m5E8VLjObPEO+mN2t/FZTMZLiFqPWc/ALSqnMnnhwrNi2rbfg/rd/IpL8Le3pSBne8+seeFVBoGqzHM9yXw==
' >> ~/.ssh/known_hosts

(umask 077; touch ~/.ssh/id_rsa)
chmod 0600 ~/.ssh/id_rsa
(cat <<EOF > ~/.ssh/id_rsa
$CHECKOUT_KEY
EOF
)
```

- .sshフォルダの作成
- known_hostsの設定
- checkout keyの挿入

が行われています。 `$CHECKOUT_KEY`については [ここ](https://circleci.com/docs/2.0/env-vars/#built-in-environment-variables)には記載がないのですが、ほぼ間違いなくcheckout key(=秘密鍵)の文字列だと推測できます。

> IdentityFile
> Specifies a file from which the user's RSA or DSA authentication identity is read. The default is ~/.ssh/identity for protocol version 1, and ~/.ssh/id_rsa and ~/.ssh/id_dsa for protocol version 2. 

https://linux.die.net/man/5/ssh_config

`~/.ssh/id_rsa`というファイル名で秘密鍵を保存しているので、ssh-agentに秘密鍵が追加されていない場合でも `circleci_test` repositoryからソースコードを読み込むことが可能になります。

ちなみにcheckout stepで追加した `~/.ssh/id_rsa` ファイルが無くても先程確認したとおりssh-agentに既に鍵が追加されているため`circleci_test`にアクセスできるはずです。以下でその確認を行いました。

以下のconfigでcheckout後の状態を作り、ssh接続します。

```yaml:.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - checkout
      - run:
          name: fail
          command: "false"
```

checkout stepによって`~/.ssh/id_rsa`, `~/.ssh/known_hosts`ファイルが作成されています。
`ssh-add -l`の結果は変わりません。

```
circleci@c9932af704b4:~$ ls ~/.ssh/
id_rsa  known_hosts
circleci@c9932af704b4:~$ ssh-add -l
2048 SHA256:Iv1F93xHKVfdpsngi5Zo6PuaQ+8aYTlxTIb1DbBp+M4  (RSA)
2048 SHA256:R+dN1DTPqR6CCIfFzDrWOAKUvRhDZ0/SkMeoYihNIrc  (RSA)
2048 SHA256:SZX2IepedP70lftv2gN8bhgMe/6TOkJqaQLVlniXWzg  (RSA)
```

checkout stepで追加された`~/.ssh/id_rsa`を削除して`git@github.com:ymaki/circleci_test.git`を読み出せることを確認します

```
circleci@c9932af704b4:~$ rm ~/.ssh/id_rsa ~/.ssh/known_hosts
circleci@c9932af704b4:~$ git clone git@github.com:ymaki/circleci_test.git
Cloning into 'circleci_test'...
The authenticity of host 'github.com (140.82.114.4)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,140.82.114.4' (RSA) to the list of known hosts.
remote: Enumerating objects: 49, done.
remote: Counting objects: 100% (49/49), done.
remote: Compressing objects: 100% (23/23), done.
remote: Total 49 (delta 10), reused 49 (delta 10), pack-reused 0
Receiving objects: 100% (49/49), 4.97 KiB | 0 bytes/s, done.
Resolving deltas: 100% (10/10), done.
```

成功しました。
checkout stepで追加した`~/.ssh/id_rsa`がなくても `circleci_test` にアクセスできました。
(試しに消してみたのですがCIでは非対話環境なのでknown_hostsは必要です)

上記より、checkout stepではcheckout keyの追加を行っていますが今回の現象にはあまり関係がなさそうです。

| repository | アクセス |
|:---:|:---:|
| circleci_test | 可 |
| submodule_1 | 不可 |
| submodule_2 | 不可 |

### add_ssh_keys後

最後にadd_ssh_keys stepの確認をします。
残念ながらadd_ssh_keys stepについては内部の処理が不明です。
https://circleci.com/docs/2.0/configuration-reference/#add_ssh_keys

そのためあまり良くないのですが外側から観測するしかなさそうです。
次の`.circleci/config.yml`を実行して`add_ssh_keys`前後の変化を確認してみます。

```yaml:.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - add_ssh_keys:
          fingerprints:
            - "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"    
      - run:
          name: fail
          command: "false"
```

jobがfailしたらsshで接続し、ssh関連の確認をします。

```
circleci@253954aa16fb:~$ ssh-add -l
2048 SHA256:Iv1F93xHKVfdpsngi5Zo6PuaQ+8aYTlxTIb1DbBp+M4  (RSA)
2048 SHA256:R+dN1DTPqR6CCIfFzDrWOAKUvRhDZ0/SkMeoYihNIrc  (RSA)
2048 SHA256:SZX2IepedP70lftv2gN8bhgMe/6TOkJqaQLVlniXWzg  (RSA)
```

ssh-agentに追加された鍵は変わりません。

```
circleci@253954aa16fb:~$ ls .ssh/
config  id_rsa_7a50914d0c1a98e17cc4784d873a397b
```

`~/.ssh/config`が追加されています。
また、add_ssh_keys stepでは、checkout keyではなく指定したfingerprintのDeploy Keyがsuffix付きで挿入されています。

```
circleci@253954aa16fb:~$ cat ~/.ssh/config
Host github.com
  IdentitiesOnly yes
  IdentityFile /home/circleci/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b
```

`~/.ssh/config`では、指定したHostnameに対して次の設定をしています。
1. ssh-agentに追加された鍵を使わず、configで指定したファイルの鍵を利用する
2. 秘密鍵ファイルの指定

ssh-agentに鍵が登録されていますが、`github.com`について`IdentitiesOnly yes`が設定されていますのでssh-agentの鍵は**利用しません**。
この状態ではgithub.comへの全てのsshアクセスに`/home/circleci/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b(submodule_1の秘密鍵)`を利用します。

`github.com`へのアクセスが`submodule_1`で成功、`submodule_2`, `circleci_test`で失敗することを確認します。

```console

circleci@253954aa16fb:~$ git clone git@github.com:ymaki/submodule_1.git
Cloning into 'submodule_1'...
The authenticity of host 'github.com (140.82.113.4)' can't be established.
RSA key fingerprint is SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8.
Are you sure you want to continue connecting (yes/no)? yes
Warning: Permanently added 'github.com,140.82.113.4' (RSA) to the list of known hosts.
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Total 3 (delta 0), reused 3 (delta 0), pack-reused 0
Receiving objects: 100% (3/3), done.
circleci@253954aa16fb:~$ git clone git@github.com:ymaki/submodule_2.git
Cloning into 'submodule_2'...
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
circleci@253954aa16fb:~$ git clone git@github.com:ymaki/circleci_test.git
Cloning into 'circleci_test'...
ERROR: Repository not found.
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.
```

予想通りの結果になりました。

| repository | アクセス |
|:---:|:---:|
| circleci_test | 不可 |
| submodule_1 | 可 |
| submodule_2 | 不可 |

#### add_ssh_keysを複数回

それではadd_ssh_keys stepを複数回実行した場合はどうなるかというと、先述の通り単純に`~/.ssh/config`に追記されます。

```
circleci@10e8c95f8f07:~# ls .ssh/
config  id_rsa  id_rsa_29ff6cd6d55fcc92b93390d96333c6f7  id_rsa_7a50914d0c1a98e17cc4784d873a397b  known_hosts
circleci@10e8c95f8f07:~# cat .ssh/config
Host github.com
  IdentitiesOnly yes
  IdentityFile /home/circleci/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b
Host github.com
  IdentitiesOnly yes
  IdentityFile /home/circleci/.ssh/id_rsa_29ff6cd6d55fcc92b93390d96333c6f7
```

繰り返しになりますが、`.ssh/config`は先に記述したものが優先される & `IdentitiesOnly yes`のため、２つ目のブロックの秘密鍵は使用されず、1つ目のブロックの秘密鍵`/home/circleci/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b`のみが利用されます。

| repository | アクセス |
|:---:|:---:|
| circleci_test | 不可 |
| submodule_1 | 可 |
| submodule_2 | 不可 |

### (おまけ)add_ssh_keysの後にcheckout

これまでの話から、add_ssh_keys stepの後にcheckout stepを実施した場合が気になります。
add_ssh_keys stepで`submodule_1`の秘密鍵を使用する設定になってしまうので、checkout stepでcheckout keyを挿入しても`circleci_test`の読み出しが失敗する気がします。
試してみましょう。

```
version: 2
jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - add_ssh_keys:
          fingerprints:
            - "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"    
      - checkout
      - run:
          name: fail
          command: "false"
```

無事失敗しました。

![スクリーンショット 2019-08-15 17.46.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/9fdd01de-e975-10db-20de-ea1ad19d00b9.png)

複数の鍵でなくてもgithub.com(またはbitbucket.org)ドメインの鍵を追加する際には注意が必要そうです。

# 対策

挙動が見えてきたので対策を考えます。

## (x) 1つのDeploy Keyを複数のGithub repositoryに追加する

CircleCIの`~/.ssh/config`に同じドメインの設定が複数記述されるのが問題なので、Deploy Keyを１つにしてGithubの複数repositoryにそのDeploy Keyを登録しようという発想です。

この案は、Githubでは複数repositoryに単一のDeploy Keyを登録できないという制約により棄却されます。
登録しようとしても`Error: Key already in use` とエラーが出て登録できませんでした。
https://help.github.com/ja/articles/error-key-already-in-use

## (x) `IdentitiesOnly yes`を削除して秘密鍵をssh-addする

add_ssh_keysでは`IdentitiesOnly` パラメータによってssh-agentが持つ鍵を利用不可にしていますが、これをnoに変更することでssh-agentの秘密鍵を利用できるようにしようという作戦です。
こんな感じでしょうか。

```yaml

version: 2

jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"
            - "29:ff:6c:d6:d5:5f:cc:92:b9:33:90:d9:63:33:c6:f7"
      - run:
          name: use ssh-agent
          command: |
            sed -i -e 's/IdentitiesOnly yes/IdentitiesOnly no/g' ~/.ssh/config
            for filename in $(grep 'id_rsa_' ~/.ssh/config | cut -d' ' -f4); do ssh-add ${filename}; done
      - run:
          name: pull submodule
          command: git submodule update --init
```

と、やってみたのですが、失敗してしまいました。

<img width="897" alt="スクリーンショット 2019-08-15 23.23.47.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/28c2f00d-6a3b-dfb8-b73b-5bbe5f38346f.png">

先述しましたが、ssh接続する際にssh-agentに最初に追加された`circleci_test`のcheckout keyで認証に成功しますが`submodule_1`, `submodule_2`へのアクセス権が無いため認可に失敗。ただしssh認証自体は成功しているので次の鍵の試行は行わないという流れのようです。

## (x) 自身のUser Keyを利用する

`circleci_test`や`submodule_1`, `submodule_2`にRead/Write権限のある自身のUser KeyをCircleCIのProjectに追加する案です。

成功はするのですが、Projectに不必要に広い権限を与えてしまっているため、非推奨です。
秘密鍵は実行中のcontainerの`~/.ssh/`以下に挿入されるため、例えば同じProjectに参加している他人があなたの権限を持つ秘密鍵を覗き、Projectに関係ないprivateなrepositoryを読み書きする事ができてしまいます。

## (o) Machine UserのUser Keyを利用する

> If your server needs to access multiple repositories, you can create a new GitHub account and attach an SSH key that will be used exclusively for automation. Since this GitHub account won't be used by a human, it's called a machine user. 

Machine Userと呼ばれる権限管理のためのアカウントを新たに作成します。
必要な権限をMachine Userに付与した上でProjectにMachine UserのUser Keyを追加することで、必要なrepositoryだけを引けるようになります。

CircleCIはこの方法を推奨しているようです。

> Your best bet, for fine-grained access to more than one repo, is to create what GitHub calls a machine user. Give this user exactly the permissions your build requires, and then associate its user key with your project on CircleCI.

discussionやドキュメントにも記載があります。
https://discuss.circleci.com/t/multiple-ssh-deploy-keys-with-same-hostname/4288/2
https://circleci.com/docs/2.0/gh-bb-integration/#controlling-access-via-a-machine-user

必要十分な権限を与えられる点は良いのですが、Project毎に権限のためだけのgithubアカウントを作成して管理するのか…? という疑問があり、個人的にはあまりやりたくありません。

## (o) add_ssh_keysの後に`.ssh/config`を掃除する

根本の原因は`add_ssh_keys`した際に`.ssh/config`に同一名のブロックが現れることが問題でした。
そこで、「add_ssh_keys -> githubアクセス -> configを消去」を1セットにして1つずつ鍵を使うとうまく行く気がします。
具体的には次のようなconfigになります。

```yaml:.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"    
      - run:
          name: pull submodule
          command: git submodule update --init submodule_1
      - run:
          name: remove ssh keys
          command: rm ~/.ssh/config
      - add_ssh_keys:
          fingerprints:
            - "29:ff:6c:d6:d5:5f:cc:92:b9:33:90:d9:63:33:c6:f7"    
      - run:
          name: pull submodule
          command: git submodule update --init submodule_1
      - run:
          name: remove ssh keys
          command: rm ~/.ssh/config
```

CircleCIのversion2.1以降でcommandsが使える場合は、次のようにしてcommandにまとめてしまうのが良いでしょう。

```yaml

version: 2.1

commands:
  checkout_submodule:
    description: "Checkout a submodule"
    parameters:
      fingerprint:
        type: string
      module_name:
        type: string
    steps:
      - add_ssh_keys:
          fingerprints:
            - << parameters.fingerprint >>
      - run:
          name: pull submodule
          command: git submodule update --init << parameters.module_name >>
      - run:
          name: remove ssh keys
          command: rm ~/.ssh/config

jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - checkout
      - checkout_submodule:
          module_name: "submodule_1"
          fingerprint: "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"
      - checkout_submodule:
          module_name: "submodule_2"    
          fingerprint: "29:ff:6c:d6:d5:5f:cc:92:b9:33:90:d9:63:33:c6:f7"
```

少しhackyですが、submoduleの依存関係を取り除くケースではworkします。
ただし、[こちら](https://medium.com/veltra-engineering/there-may-be-security-risks-in-your-clone-of-multiple-private-repositories-in-circleci-6ae6368eacb9)言及されているように、`go get`など複数のprivate repositoryへのアクセスが同時に自動で行われる場合には適用できません。

## (o) `./ssh/config`を書き換えてalias名でアクセスする

[ここ](https://discuss.circleci.com/t/multiple-deploy-keys-for-github/25658/11)で言及されている案で、add_ssh_keysで追加されるconfigに`HostName`を追加することで、`Host`をaliasとして使おうという作戦です。

`~/.ssh/config`が次のようになります。
Hostが重複しないため、複数のブロックの設定が活きます。

```config:~/.ssh/config
Host some_alias_name.com
  HostName github.com
  IdentitiesOnly yes
  IdentityFile /root/.ssh/id_rsa_7a50914d0c1a98e17cc4784d873a397b
Host some_another_alias_name.com
  HostName github.com
  IdentitiesOnly yes
  IdentityFile /root/.ssh/id_rsa_29ff6cd6d55fcc92b93390d96333c6f7
```

具体的にやってみます。

まずは重複しない適当な名前でCircleCIのProjectに秘密鍵を登録しておきます。
<img width="664" alt="スクリーンショット 2019-08-15 22.12.54.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/98447ff2-cd7c-90b4-293f-a6c61c9ef8ad.png">

そして、add_ssh_keys stepの直後で`~/.ssh/config`を書き換えます。(`add alias of ssh target hostname`)
さらに、ssh targetのHostを`submodule1.github.com`のように変更したので、`git submodule update`する際にも`github.com`ではなく`submodule1.github.com`を見に行く必要があります。(`modify submodule hostname to alias name`)
~/.circleci/config.ymlは次のようになります。

```yaml:~/.circleci/config.yml

version: 2

jobs:
  build:
    docker:
      - image: circleci/python:stretch
    steps:
      - checkout
      - add_ssh_keys:
          fingerprints:
            - "7a:50:91:4d:0c:1a:98:e1:7c:c4:78:4d:87:3a:39:7b"
            - "29:ff:6c:d6:d5:5f:cc:92:b9:33:90:d9:63:33:c6:f7"
      - run:
          name: add alias of ssh target hostname
          command: |
            sed -i -e 's/Host submodule1.github.com/Host submodule1.github.com\n  HostName github.com/g' ~/.ssh/config
            sed -i -e 's/Host submodule2.github.com/Host submodule2.github.com\n  HostName github.com/g' ~/.ssh/config
      - run:
          name: modify submodule hostname to alias name
          command: |
            git config submodule.submodule_1.url git@submodule1.github.com:ymaki/submodule_1.git
            git config submodule.submodule_2.url git@submodule2.github.com:ymaki/submodule_2.git
      - run:
          name: pull submodule
          command: git submodule update --init

```

これを実行すると、成功したようです。

<img width="859" alt="スクリーンショット 2019-08-15 22.56.03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/266939/f55fc8c4-0f97-40c4-485b-fe743124726d.png">

この方法は手を入れる箇所が多いですが、成功はします。
今回はsubmoduleだったので`git config`コマンドでURLを変更しましたが、例えば`go get`の場合はソースコード中のURLを変更するのでしょうか? ちょっと考えたくありません…。

# まとめ

CircleCIでは同じドメインの複数Deploy Keyの追加はサポートされていません。
以下の案を検討しましたが適用できるケースとできないケースがあり、適用できないケースではおとなしくMachine Userを作成するのが良さそうです。
それぞれ一長一短なので許容できるデメリットを考えて(特に推奨されていない方法は自己責任で)利用してください。
また、セキュリティリスクや他に良さそうな方法があったら教えて下さい🙇

## 失敗または脆弱
- 単一のDeploy Keyを使い回す案
- 自分のUser Keyを利用する案
- `IdentitiesOnly no` + `ssh-add` 案

## 成功
- Machine Userを作成しそのUser Keyを利用する案(公式推奨)
- add_ssh_keyのあと都度 `.ssh/config` を削除する案
- `./ssh/config`を書き換えてaliasでアクセスする案

# 参考

https://circleci.com/docs/2.0/
https://discuss.circleci.com/
https://man.openbsd.org/ssh_config
https://linux.die.net/man/5/ssh_config
https://help.github.com/ja/articles/error-key-already-in-use
https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols#_the_ssh_protocol
https://medium.com/veltra-engineering/there-may-be-security-risks-in-your-clone-of-multiple-private-repositories-in-circleci-6ae6368eacb9