Конфликт локальных SSH ключей для общего домена GitLab

Обычно компании используют свои домены второго уровня для репозиториев (например, company.gitlab.com), и проблем с SSH доступом не возникает. Но в моем случае проект клиента оказался на том же gitlab.com, что и мой личный. Здесь я впервые столкнулся с "конфликтом" SSH ключей для одного домена.

Алиасы, названия ключей и проектов изменены.

Проблема

Проект клиента успешно склонировался, я настроил его и некоторое время работал. Но когда я вернулся к своему проекту, то получил неожиданную ошибку:

master@mi-pro:~/dev/www/me/app1$ git pull
remote: 
remote: ========================================================================
remote: 
remote: ERROR: The project you were looking for could not be found or you don't have permission to view it.

remote: 
remote: ========================================================================
remote: 
fatal: Could not read from remote repository.

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

Первым делом я проверил загруженные в SSH-агент ключи – все на месте:

master@mi-pro:~$ ssh-add -l
3072 SHA256:ABCD2 client1_gitlab.com (RSA)
3072 SHA256:ABCD1 my_gitlab.com (RSA)

В чем же дело? Проблема в том, что SSH-клиент, подключаясь к gitlab.com (порт 22), предлагает серверу все доступные ключи из агента по очереди. GitLab принимает первый ключ, который есть в системе (в моем случае это оказался последний добавленный – client1_gitlab_rsa). Для моего аккаунта этот ключ чужой, отсюда и ошибка доступа.

Ситуация стала окончательно ясна, когда я заглянул в конфиги Git:

master@mi-pro:~$ cat ~/dev/www/me/app1/.git/config | grep url
        url = git@gitlab.com:me/app1.git

master@mi-pro:~$ cat ~/dev/www/client1/app1/.git/config | grep url
        url = git@gitlab.com:client1/app1.git

Оба URL начинаются одинаково: git@gitlab.com:. Для Git это один и тот же удаленный сервер, но SSH использует для подключения не тот ключ.

Главной задачей для меня стала необходимость сделать мой основной SSH-ключ по умолчанию, поскольку мои проекты лежат не только в одном каталоге. А ключи клиентов подгружать отдельно.

Варианты решения

Есть несколько способов решить эту проблему. Опишу два наиболее оптимальных, на мой взгляд.

Вариант 1. Алиас хоста в ~/.ssh/config

Работая только со своим GitLab, у меня не было необходимости явно определять свой ключ в ~/.ssh/config. Но здесь этот файл стал удобным решением. Мы явно укажем, какой ключ для какого домена использовать, а также добавим параметр IdentitiesOnly yes, который запрещает SSH использовать ключи из агента, заставляя его применять только то, что указано в конфиге.

master@mi-pro:~$ nano ~/.ssh/config
...

# Мой ключ (по умолчанию для gitlab.com)
Host gitlab.com
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/my_gitlab_rsa
    IdentitiesOnly yes

# Ключ клиента (используем псевдоним хоста)
Host gitlab-client1
    HostName gitlab.com
    User git
    IdentityFile ~/.ssh/client1_gitlab_rsa
    IdentitiesOnly yes

После этого важно изменить URL в конфиге проекта клиента, заменив gitlab.com на наш новый алиас gitlab-client1:

master@mi-pro:~$ nano ~/dev/www/client1/app1/.git/config
[remote "origin"]
        url = git@gitlab-client1:client1/app.git
        fetch = +refs/heads/*:refs/remotes/origin/*

Теперь Git, видя алиас gitlab-client1, обратится к SSH-конфигу, который подставит правильный ключ.

Вариант 2. Контекстная конфигурация через ~/.gitconfig

Это подход позволяет автоматически подставлять настройки в зависимости от того, в какой директории вы находитесь. Для этого используется механизм includeIf в Git.

Создадим отдельные конфиги для каждого контекста:

master@mi-pro:~$ nano ~/.gitconfig-me
[core]
        sshCommand = ssh -i ~/.ssh/my_gitlab_rsa -F /dev/null

master@mi-pro:~$ nano ~/.gitconfig-client1
[core]
        sshCommand = ssh -i ~/.ssh/client1_gitlab_rsa -F /dev/null

master@mi-pro:~$ ls -la | grep .gitconfig
-rw-rw-r--  1 master master    66 Mar 17 13:19 .gitconfig
-rw-rw-r--  1 master master     0 Mar 17 17:32 .gitconfig-client1
-rw-rw-r--  1 master master     0 Mar 17 17:32 .gitconfig-me

Флаг -F /dev/null говорит SSH не использовать общий конфиг, чтобы избежать неожиданных пересечений.

Далее, в главном ~/.gitconfig пропишем правила подключения этих файлов в зависимости от пути к репозиторию.

master@mi-pro:~$ nano ~/.gitconfig
[user]
        name = Your Name
        email = main@email.com
[includeIf "gitdir:~/dev/www/me/"]
        path = ~/.gitconfig-me
[includeIf "gitdir:~/dev/www/client1/"]
        path = ~/.gitconfig-client1

Теперь, когда вы будете выполнять Git-команды в любой папке проектов, Git автоматически подставит команду ssh с вашим личным ключом. Для проектов клиента – с ключом клиента. URL в .git/config проектов менять не нужно, они остаются стандартными (git@gitlab.com:...).

Мой выбор

Я выбрал для себя Вариант 1, так как мой основной ключ SSH не должен зависеть от каталога проекта, плюс я привык контролировать настройки удаленных репозиториев в файле .git/config.

Однако Вариант 2 намного удобнее, если вы работаете с десятками проектов или не хотите вручную править их конфиги. Git сам "поймет", где вы находитесь, и сделает все за вас.

Также я изначально попробовал более короткий вариант через хук в ~/.bashrc:

PROMPT_COMMAND='update_git_ssh_command'

update_git_ssh_command() {
    if git rev-parse --git-dir > /dev/null 2>&1; then
        if [[ "$PWD" == *"dev/www/client1"* ]]; then
            export GIT_SSH_COMMAND="ssh -i ~/.ssh/client1_gitlab_rsa -F /dev/null"
        else
            export GIT_SSH_COMMAND="ssh -i ~/.ssh/my_gitlab_rsa -F /dev/null"
        fi
    else
        unset GIT_SSH_COMMAND
    fi
}

Но у него есть оверхед в плане производительности и возможных аномалий в консоли Linux, поэтому лучше использовать более предсказуемые варианты.

Источники и ссылки

  1. ssh_config(5) – Linux manual page
  2. FILES > ~/.gitconfig – Git Documentation
  3. CONFIGURATION FILE > Includes – Git Documentation