Cloning private GitHub repositories with Ansible on a remote server through SSH

One of Ansible's strengths is the fact that its 'agentless' architecture uses SSH for control of remote servers. And one classic problem in remote Git administration is authentication; if you're cloning a private Git repository that requires authentication, how can you do this while also protecting your own private SSH key (by not copying it to the remote server)?

As an example, here's a task that clones a private repository to a particular folder:

- name: Clone a private repository into /opt.
  git:
    repo: git@github.com:geerlingguy/private-repo.git
    version: master
    dest: /opt/private-repo
    accept_hostkey: yes
  # ssh-agent doesn't allow key to pass through remote sudo commands.
  become: no

If you run this task, you'll probably end up with something like:

TASK [geerlingguy.role : Clone a private repository into /opt.] *******************************************
fatal: [server]: FAILED! => {"changed": false, "cmd": "/usr/bin/git clone --origin origin '' /opt/private-repo", "failed": true, "msg": "Cloning into '/opt/private-repo'...\nWarning: Permanently added the RSA host key for IP address 'server' to the list of known hosts.\r\nPermission denied (publickey).\r\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.", "rc": 128, "stderr": "Cloning into '/opt/private-repo'...\nWarning: Permanently added the RSA host key for IP address 'server' to the list of known hosts.\r\nPermission denied (publickey).\r\nfatal: Could not read from remote repository.\n\nPlease make sure you have the correct access rights\nand the repository exists.\n", "stderr_lines": ["Cloning into '/opt/private-repo'...", "Warning: Permanently added the RSA host key for IP address 'server' to the list of known hosts.", "Permission denied (publickey).", "fatal: Could not read from remote repository.", "", "Please make sure you have the correct access rights", "and the repository exists."], "stdout": "", "stdout_lines": []}

(Or, more succinctly: Permission denied (publickey)., meaning GitHub refused the clone request.)

The problem is you have an SSH key locally that allows access to the Git repository, but the remote server doesn't see that key (even if you have ssh-agent running and your key loaded via ssh-add).

The simplest solutions are either:

  1. Add a task that copies your local SSH private key to the remote server. (I tend to avoid doing this—if the remote server is ever hacked, your own private key would be exposed!)
  2. Add a task that generates a private key on the remote server, then another task (or manual step) of adding the generated public key to GitHub, so that server can authenticate for Git commands.

But if you want to avoid having any private keys on the remote server (sometimes this can be a necessary security requirement), you can pass your own private key through to the remote server via Ansible's SSH connection.

Using SSH Agent

First, add the following SSH configuration to your ~/.ssh/config file:

Host [server-address-here] [ip-address-here]
    ForwardAgent yes

This enables forwarding keys loaded into ssh-agent to remote SSH connections.

Check which keys are loaded currently using ssh-add -l, and add any additional required keys using ssh-add ~/.ssh/key-here.

The next time you run the Git task in your playbook, you should see something like:

TASK [geerlingguy.role : Clone a private repository into /opt.] *******************************************
changed: [server]

Any time you run the Ansible playbook (or ad hoc tasks), the Ansible's SSH connection will hold all the loaded SSH Agent keys, so you can perform private Git repository operations without tasks failing.

Copying a key to the server

The other method, which I tend to avoid as I don't like having keys on remote servers too often, is to copy a private key to the server then use it for the git operation. As an example, I recently set up an Apache-based Ubuntu webserver with an application codebase cloned into the /var/www/html docroot, and used the following tasks to copy a GitHub deploy key into place and use it:

- name: Ensure /var/www/html directory has correct permissions.
  file:
    path: /var/www/html
    state: directory
    owner: www-data
    group: www-data

- name: Ensure .ssh directory exists.
  file:
    path: /var/www/.ssh
    state: directory
    mode: 0700
    owner: www-data
    group: www-data

- name: Ensure GitHub deploy key is present on the server.
  copy:
    content: "{{ deploy_private_key }}"
    dest: /var/www/.ssh/deploy_key
    mode: 0600
    owner: www-data
    group: www-data

# See: https://stackoverflow.com/a/37096534/100134
- name: Ensure setfacl support is present.
  package: name=acl

- name: Clone the code repository to the docroot.
  git:
    repo: "{{ git_repo }}"
    dest: /var/www/html
    accept_hostkey: yes
    key_file: /var/www/.ssh/deploy_key
  become_user: www-data

Comments

Hi Jeff,

What about having a private key stored in an Ansible Vault and on playbook run, you create the ssh key file on the target, pull the repository and then deleting the key from the target machine?

Is there any reason why that couldn't be an alternative solution here?

That's another viable option; though it very slightly increases the potential attack surface, due the fact that the file exists briefly (or longer, if the fit task fails and the file isn't deleted) on the server's volume.

Been working on a small repo at https://github.com/jasperf/stedding and followed your steps after implementing ones from Digital Ocean trying to make what I have work. But as I have /var/www as www-data and chmod 0700 and run all as user laravel it seems it all is stopped due to permission issues. If I use it all as it is now in the repository:

- name: create /var/www/ directory
    file: dest=/var/www/ state=directory owner=www-data group=www-data mode=0700

  - name: Clone git repository
    git:
      repo: "{{ repo_url }}"
      dest: /var/www/laravel
      version: master
      update: no
      accept_hostkey: yes
    become: yes
    #become_flags: "-E"
    become_user: www-data
    register: cloned

there are no permission issues, but it will hang stating:

<128.199.35.232> ESTABLISH SSH CONNECTION FOR USER: laravel
<128.199.35.232> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=laravel -o ConnectTimeout=10 -o ControlPath=/Users/jasper/.ansible/cp/ansible-ssh-%h-%p-%r 128.199.35.232 '/bin/sh -c '"'"'setfacl -m u:www-data:r-x /tmp/ansible-tmp-1494744153.39-60754923202260/ /tmp/ansible-tmp-1494744153.39-60754923202260/git.py && sleep 0'"'"''
<128.199.35.232> ESTABLISH SSH CONNECTION FOR USER: laravel
<128.199.35.232> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s -o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o User=laravel -o ConnectTimeout=10 -o ControlPath=/Users/jasper/.ansible/cp/ansible-ssh-%h-%p-%r -tt 128.199.35.232 '/bin/sh -c '"'"'sudo -H -S  -p "[sudo via ansible, key=key] password: " -u www-data /bin/sh -c '"'"'"'"'"'"'"'"'echo BECOME-SUCCESS-key; /usr/bin/python /tmp/ansible-tmp-1494744153.39-60754923202260/git.py'"'"'"'"'"'"'"'"' && sleep 0'"'"''

Any ideas?