diff --git a/changelogs/fragments/73819-git-accept_new_host_key.yaml b/changelogs/fragments/73819-git-accept_new_host_key.yaml new file mode 100644 index 00000000000..b2299143bb2 --- /dev/null +++ b/changelogs/fragments/73819-git-accept_new_host_key.yaml @@ -0,0 +1,3 @@ +--- +minor_changes: + - git - Add ``accept_newhostkey`` option (https://github.com/ansible/ansible/issues/69846). diff --git a/lib/ansible/modules/git.py b/lib/ansible/modules/git.py index d268257d3c8..f5b63f94e3b 100644 --- a/lib/ansible/modules/git.py +++ b/lib/ansible/modules/git.py @@ -48,6 +48,15 @@ options: type: bool default: 'no' version_added: "1.5" + accept_newhostkey: + description: + - As of OpenSSH 7.5, "-o StrictHostKeyChecking=accept-new" can be + used which is safer and will only accepts host keys which are + not present or are the same. if C(yes), ensure that + "-o StrictHostKeyChecking=accept-new" is present as an ssh option. + type: bool + default: 'no' + version_added: "2.12" ssh_opts: description: - Creates a wrapper script and exports the path as GIT_SSH @@ -317,6 +326,7 @@ from distutils.version import LooseVersion from ansible.module_utils.basic import AnsibleModule from ansible.module_utils.six import b, string_types from ansible.module_utils._text import to_native, to_text +from ansible.module_utils.common.process import get_bin_path def relocate_repo(module, result, repo_dir, old_repo_dir, worktree_dir): @@ -462,6 +472,21 @@ def get_version(module, git_path, dest, ref="HEAD"): return sha +def ssh_supports_acceptnewhostkey(module): + try: + ssh_path = get_bin_path('ssh') + except ValueError as err: + module.fail_json( + msg='Remote host is missing ssh command, so you cannot ' + 'use acceptnewhostkey option.', details=to_text(err)) + supports_acceptnewhostkey = True + cmd = [ssh_path, '-o', 'StrictHostKeyChecking=accept-new', '-V'] + rc, stdout, stderr = module.run_command(cmd) + if rc != 0: + supports_acceptnewhostkey = False + return supports_acceptnewhostkey + + def get_submodule_versions(git_path, module, dest, version='HEAD'): cmd = [git_path, 'submodule', 'foreach', git_path, 'rev-parse', version] (rc, out, err) = module.run_command(cmd, cwd=dest) @@ -1107,6 +1132,7 @@ def main(): verify_commit=dict(default='no', type='bool'), gpg_whitelist=dict(default=[], type='list', elements='str'), accept_hostkey=dict(default='no', type='bool'), + accept_newhostkey=dict(default='no', type='bool'), key_file=dict(default=None, type='path', required=False), ssh_opts=dict(default=None, required=False), executable=dict(default=None, type='path'), @@ -1119,7 +1145,7 @@ def main(): archive_prefix=dict(), separate_git_dir=dict(type='path'), ), - mutually_exclusive=[('separate_git_dir', 'bare')], + mutually_exclusive=[('separate_git_dir', 'bare'), ('accept_hostkey', 'accept_newhostkey')], required_by={'archive_prefix': ['archive']}, supports_check_mode=True ) @@ -1150,11 +1176,21 @@ def main(): if module.params['accept_hostkey']: if ssh_opts is not None: - if "-o StrictHostKeyChecking=no" not in ssh_opts: + if ("-o StrictHostKeyChecking=no" not in ssh_opts) and ("-o StrictHostKeyChecking=accept-new" not in ssh_opts): ssh_opts += " -o StrictHostKeyChecking=no" else: ssh_opts = "-o StrictHostKeyChecking=no" + if module.params['accept_newhostkey']: + if not ssh_supports_acceptnewhostkey(module): + module.warn("Your ssh client does not support accept_newhostkey option, therefore it cannot be used.") + else: + if ssh_opts is not None: + if ("-o StrictHostKeyChecking=no" not in ssh_opts) and ("-o StrictHostKeyChecking=accept-new" not in ssh_opts): + ssh_opts += " -o StrictHostKeyChecking=accept-new" + else: + ssh_opts = "-o StrictHostKeyChecking=accept-new" + # evaluate and set the umask before doing anything else if umask is not None: if not isinstance(umask, string_types): diff --git a/test/integration/targets/git/tasks/main.yml b/test/integration/targets/git/tasks/main.yml index c5aeacbe6c0..ed06eab5aa7 100644 --- a/test/integration/targets/git/tasks/main.yml +++ b/test/integration/targets/git/tasks/main.yml @@ -21,6 +21,7 @@ - import_tasks: formats.yml - import_tasks: missing_hostkey.yml +- import_tasks: missing_hostkey_acceptnew.yml - import_tasks: no-destination.yml - import_tasks: specific-revision.yml - import_tasks: submodules.yml diff --git a/test/integration/targets/git/tasks/missing_hostkey.yml b/test/integration/targets/git/tasks/missing_hostkey.yml index 02d5be35129..6e4d53c3b3a 100644 --- a/test/integration/targets/git/tasks/missing_hostkey.yml +++ b/test/integration/targets/git/tasks/missing_hostkey.yml @@ -46,3 +46,16 @@ that: - git_result is changed when: github_ssh_private_key is defined + +- name: MISSING-HOSTEKY | Remove github.com hostkey from known_hosts + lineinfile: + dest: '{{ output_dir }}/known_hosts' + regexp: "github.com" + state: absent + when: github_ssh_private_key is defined + +- name: MISSING-HOSTKEY | clear checkout_dir + file: + state: absent + path: '{{ checkout_dir }}' + when: github_ssh_private_key is defined diff --git a/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml b/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml new file mode 100644 index 00000000000..fb8bb063d87 --- /dev/null +++ b/test/integration/targets/git/tasks/missing_hostkey_acceptnew.yml @@ -0,0 +1,78 @@ +- name: MISSING-HOSTKEY | check accept_newhostkey support + shell: ssh -o StrictHostKeyChecking=accept-new -V + register: ssh_supports_accept_newhostkey + ignore_errors: true + +- block: + - name: MISSING-HOSTKEY | accept_newhostkey when ssh does not support the option + git: + repo: '{{ repo_format2 }}' + dest: '{{ checkout_dir }}' + accept_newhostkey: true + ssh_opts: '-o UserKnownHostsFile={{ output_dir }}/known_hosts' + register: git_result + ignore_errors: true + + - assert: + that: + - git_result is failed + - git_result.warnings is search("does not support") + + when: ssh_supports_accept_newhostkey.rc != 0 + +- name: MISSING-HOSTKEY | checkout ssh://git@github.com repo without accept_newhostkey (expected fail) + git: + repo: '{{ repo_format2 }}' + dest: '{{ checkout_dir }}' + ssh_opts: '-o UserKnownHostsFile={{ output_dir }}/known_hosts' + register: git_result + ignore_errors: true + +- assert: + that: + - git_result is failed + +- block: + - name: MISSING-HOSTKEY | checkout git@github.com repo with accept_newhostkey (expected pass) + git: + repo: '{{ repo_format2 }}' + dest: '{{ checkout_dir }}' + accept_newhostkey: true + key_file: '{{ github_ssh_private_key }}' + ssh_opts: '-o UserKnownHostsFile={{ output_dir }}/known_hosts' + register: git_result + + - assert: + that: + - git_result is changed + + - name: MISSING-HOSTKEY | clear checkout_dir + file: + state: absent + path: '{{ checkout_dir }}' + + - name: MISSING-HOSTKEY | checkout ssh://git@github.com repo with accept_newhostkey (expected pass) + git: + repo: '{{ repo_format3 }}' + dest: '{{ checkout_dir }}' + version: 'master' + accept_newhostkey: false # should already have been accepted + key_file: '{{ github_ssh_private_key }}' + ssh_opts: '-o UserKnownHostsFile={{ output_dir }}/known_hosts' + register: git_result + + - assert: + that: + - git_result is changed + + - name: MISSING-HOSTEKY | Remove github.com hostkey from known_hosts + lineinfile: + dest: '{{ output_dir }}/known_hosts' + regexp: "github.com" + state: absent + + - name: MISSING-HOSTKEY | clear checkout_dir + file: + state: absent + path: '{{ checkout_dir }}' + when: github_ssh_private_key is defined and ssh_supports_accept_newhostkey.rc == 0