Bypass fragile git ssh wrapper (#73404)

git module now uses env vars exclusively

  - updated docs to clarify usage
  - now env vars append instead of overwrite to allow existing custom setups to keep working
    fixes #38104, #64673, #64674
  - added note for hostkeychecking more securely
    fixes #69846
  - keep script cause old versions still choke on env
  - env var cannot hold more than 'command' for older versions
  - all ssh_opts in one place
pull/76974/head
Brian Coca 2 years ago committed by GitHub
parent be19863e44
commit b493c590bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -0,0 +1,3 @@
bugfixes:
- git module no longer uses wrapper script for ssh options.
- git module is more consistent and clearer about which ssh options are added to git calls.

@ -43,8 +43,10 @@ options:
default: "HEAD" default: "HEAD"
accept_hostkey: accept_hostkey:
description: description:
- If C(yes), ensure that "-o StrictHostKeyChecking=no" is - Will ensure or not that "-o StrictHostKeyChecking=no" is present as an ssh option.
present as an ssh option. - Be aware that this disables a protection against MITM attacks.
- Those using OpenSSH >= 7.5 might want to set I(ssh_opt) to 'StrictHostKeyChecking=accept-new'
instead, it does not remove the MITM issue but it does restrict it to the first attempt.
type: bool type: bool
default: 'no' default: 'no'
version_added: "1.5" version_added: "1.5"
@ -59,16 +61,19 @@ options:
version_added: "2.12" version_added: "2.12"
ssh_opts: ssh_opts:
description: description:
- Creates a wrapper script and exports the path as GIT_SSH - Options git will pass to ssh when used as protocol, it works via GIT_SSH_OPTIONS.
which git then automatically uses to override ssh arguments. - For older versions it appends GIT_SSH_OPTIONS to GIT_SSH/GIT_SSH_COMMAND.
An example value could be "-o StrictHostKeyChecking=no" - Other options can add to this list, like I(key_file) and I(accept_hostkey).
(although this particular option is better set by - An example value could be "-o StrictHostKeyChecking=no" (although this particular
I(accept_hostkey)). option is better set by I(accept_hostkey)).
- The module ensures that 'BatchMode=yes' is always present to avoid prompts.
type: str type: str
version_added: "1.5" version_added: "1.5"
key_file: key_file:
description: description:
- Specify an optional private key file path, on the target host, to use for the checkout. - Specify an optional private key file path, on the target host, to use for the checkout.
- This ensures 'IdentitiesOnly=yes' is present in ssh_opts.
type: path type: path
version_added: "1.5" version_added: "1.5"
reference: reference:
@ -418,57 +423,88 @@ def get_submodule_update_params(module, git_path, cwd):
return params return params
def write_ssh_wrapper(module_tmpdir): def write_ssh_wrapper(module):
'''
This writes an shell wrapper for ssh options to be used with git
this is only relevant for older versions of gitthat cannot
handle the options themselves. Returns path to the script
'''
try: try:
# make sure we have full permission to the module_dir, which # make sure we have full permission to the module_dir, which
# may not be the case if we're sudo'ing to a non-root user # may not be the case if we're sudo'ing to a non-root user
if os.access(module_tmpdir, os.W_OK | os.R_OK | os.X_OK): if os.access(module.tmpdir, os.W_OK | os.R_OK | os.X_OK):
fd, wrapper_path = tempfile.mkstemp(prefix=module_tmpdir + '/') fd, wrapper_path = tempfile.mkstemp(prefix=module.tmpdir + '/')
else: else:
raise OSError raise OSError
except (IOError, OSError): except (IOError, OSError):
fd, wrapper_path = tempfile.mkstemp() fd, wrapper_path = tempfile.mkstemp()
fh = os.fdopen(fd, 'w+b')
# use existing git_ssh/ssh_command, fallback to 'ssh'
template = b("""#!/bin/sh template = b("""#!/bin/sh
if [ -z "$GIT_SSH_OPTS" ]; then %s $GIT_SSH_OPTS
BASEOPTS="" """ % os.environ.get('GIT_SSH', os.environ.get('GIT_SSH_COMMAND', 'ssh')))
else
BASEOPTS=$GIT_SSH_OPTS # write it
fi with os.fdopen(fd, 'w+b') as fh:
fh.write(template)
# Let ssh fail rather than prompt
BASEOPTS="$BASEOPTS -o BatchMode=yes" # set execute
if [ -z "$GIT_KEY" ]; then
ssh $BASEOPTS "$@"
else
ssh -i "$GIT_KEY" -o IdentitiesOnly=yes $BASEOPTS "$@"
fi
""")
fh.write(template)
fh.close()
st = os.stat(wrapper_path) st = os.stat(wrapper_path)
os.chmod(wrapper_path, st.st_mode | stat.S_IEXEC) os.chmod(wrapper_path, st.st_mode | stat.S_IEXEC)
module.debug('Wrote temp git ssh wrapper (%s): %s' % (wrapper_path, template))
# ensure we cleanup after ourselves
module.add_cleanup_file(path=wrapper_path)
return wrapper_path return wrapper_path
def set_git_ssh(ssh_wrapper, key_file, ssh_opts): def set_git_ssh_env(key_file, ssh_opts, git_version, module):
'''
use environment variables to configure git's ssh execution,
which varies by version but this functino should handle all.
'''
if os.environ.get("GIT_SSH"): # initialise to existing ssh opts and/or append user provided
del os.environ["GIT_SSH"] if ssh_opts is None:
os.environ["GIT_SSH"] = ssh_wrapper ssh_opts = os.environ.get('GIT_SSH_OPTS', '')
else:
ssh_opts = os.environ.get('GIT_SSH_OPTS', '') + ' ' + ssh_opts
# hostkey acceptance
accept_key = "StrictHostKeyChecking=no"
if module.params['accept_hostkey'] and accept_key not in ssh_opts:
ssh_opts += " -o %s" % accept_key
if os.environ.get("GIT_KEY"): # avoid prompts
del os.environ["GIT_KEY"] force_batch = 'BatchMode=yes'
if force_batch not in ssh_opts:
ssh_opts += ' -o %s' % (force_batch)
# deal with key file
if key_file: if key_file:
os.environ["GIT_KEY"] = key_file os.environ["GIT_KEY"] = key_file
if os.environ.get("GIT_SSH_OPTS"): ikey = 'IdentitiesOnly=yes'
del os.environ["GIT_SSH_OPTS"] if ikey not in ssh_opts:
ssh_opts += ' -o %s' % ikey
# we should always have finalized string here.
os.environ["GIT_SSH_OPTS"] = ssh_opts
# older than 2.3 does not know how to use git_ssh_opts,
# so we force it into ssh command var
if git_version < LooseVersion('2.3.0'):
# these versions don't support GIT_SSH_OPTS so have to write wrapper
wrapper = write_ssh_wrapper(module)
if ssh_opts: # force use of git_ssh_opts via wrapper
os.environ["GIT_SSH_OPTS"] = ssh_opts os.environ["GIT_SSH"] = wrapper
# same as above but older git uses git_ssh_command
os.environ["GIT_SSH_COMMAND"] = wrapper
def get_version(module, git_path, dest, ref="HEAD"): def get_version(module, git_path, dest, ref="HEAD"):
@ -1122,9 +1158,9 @@ def create_archive(git_path, module, dest, archive, archive_prefix, version, rep
git_archive(git_path, module, dest, archive, archive_fmt, archive_prefix, version) git_archive(git_path, module, dest, archive, archive_fmt, archive_prefix, version)
result.update(changed=True) result.update(changed=True)
# =========================================== # ===========================================
def main(): def main():
module = AnsibleModule( module = AnsibleModule(
argument_spec=dict( argument_spec=dict(
@ -1246,15 +1282,12 @@ def main():
) )
gitconfig = os.path.join(repo_path, 'config') gitconfig = os.path.join(repo_path, 'config')
# create a wrapper script and export # iface changes so need it to make decisions
# GIT_SSH=<path> as an environment variable
# for git to use the wrapper script
ssh_wrapper = write_ssh_wrapper(module.tmpdir)
set_git_ssh(ssh_wrapper, key_file, ssh_opts)
module.add_cleanup_file(path=ssh_wrapper)
git_version_used = git_version(git_path, module) git_version_used = git_version(git_path, module)
# GIT_SSH=<path> as an environment variable, might create sh wrapper script for older versions.
set_git_ssh_env(key_file, ssh_opts, git_version_used, module)
if depth is not None and git_version_used < LooseVersion('1.9.1'): if depth is not None and git_version_used < LooseVersion('1.9.1'):
module.warn("git version is too old to fully support the depth argument. Falling back to full checkouts.") module.warn("git version is too old to fully support the depth argument. Falling back to full checkouts.")
depth = None depth = None

Loading…
Cancel
Save