1# Copyright (c) 2012, Michael DeHaan <michael.dehaan@gmail.com>
2# Copyright 2015 Abhijit Menon-Sen <ams@2ndQuadrant.com>
3# Copyright 2017 Toshio Kuratomi <tkuratomi@ansible.com>
4# Copyright (c) 2017 Ansible Project
5# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
6
7from __future__ import annotations
8
9DOCUMENTATION = """
10 name: ssh
11 short_description: connect via SSH client binary
12 description:
13 - This connection plugin allows Ansible to communicate to the target machines through normal SSH command line.
14 - Ansible does not expose a channel to allow communication between the user and the SSH process to accept
15 a password manually to decrypt an SSH key when using this connection plugin (which is the default). The
16 use of C(ssh-agent) is highly recommended.
…lines 17–261 unchanged (245 lines)
262 key: pipelining
263 - section: ssh_connection
264 key: pipelining
265 vars:
266 - name: ansible_pipelining
267 - name: ansible_ssh_pipelining
268 private_key_file:
269 description:
270 - Path to private key file to use for authentication.
271 type: string
272 ini:
273 - section: defaults
274 key: private_key_file
275 env:
276 - name: ANSIBLE_PRIVATE_KEY_FILE
277 vars:
278 - name: ansible_private_key_file
279 - name: ansible_ssh_private_key_file
280 cli:
281 - name: private_key_file
282 option: '--private-key'
283 private_key:
284 description:
285 - Private key contents in PEM format. Requires the C(SSH_AGENT) configuration to be enabled.
286 type: string
287 env:
288 - name: ANSIBLE_PRIVATE_KEY
289 vars:
290 - name: ansible_private_key
291 - name: ansible_ssh_private_key
292 version_added: '2.19'
293 private_key_passphrase:
294 description:
295 - Private key passphrase, dependent on O(private_key).
296 - This does NOT have any effect when used with O(private_key_file).
297 type: string
298 env:
299 - name: ANSIBLE_PRIVATE_KEY_PASSPHRASE
300 vars:
301 - name: ansible_private_key_passphrase
302 - name: ansible_ssh_private_key_passphrase
303 version_added: '2.19'
304 control_path:
305 description:
306 - This is the location to save SSH's ControlPath sockets, it uses SSH's variable substitution.
307 - Since 2.3, if null (default), ansible will generate a unique hash. Use ``%(directory)s`` to indicate where to use the control dir path setting.
308 - Before 2.3 it defaulted to ``control_path=%(directory)s/ansible-ssh-%%h-%%p-%%r``.
309 - Be aware that this setting is ignored if C(-o ControlPath) is set in ssh args.
…lines 310–413 unchanged (104 lines)
414import re
415import selectors
416import shlex
417import shutil
418import subprocess
419import sys
420import tempfile
421import time
422import typing as t
423from functools import wraps
424from multiprocessing.shared_memory import SharedMemory
425
426from ansible import constants as C
427from ansible.errors import (
428 AnsibleAuthenticationFailure,
429 AnsibleConnectionFailure,
430 AnsibleError,
431 AnsibleFileNotFound,
432)
433from ansible.module_utils.six import text_type, binary_type
434from ansible.module_utils.common.text.converters import to_bytes, to_native, to_text
435from ansible.plugins.connection import ConnectionBase, BUFSIZE
436from ansible.plugins.shell.powershell import _replace_stderr_clixml
437from ansible.utils.display import Display
438from ansible.utils.path import unfrackpath, makedirs_safe
439from ansible.utils._ssh_agent import SshAgentClient, _key_data_into_crypto_objects
440
441try:
442 from cryptography.hazmat.primitives import serialization
443except ImportError:
444 HAS_CRYPTOGRAPHY = False
445else:
446 HAS_CRYPTOGRAPHY = True
447
448
449display = Display()
450
451P = t.ParamSpec('P')
452
…lines 453–664 unchanged (212 lines)
665
666 # parser to discover 'passed options', used later on for pipelining resolution
667 self._tty_parser = argparse.ArgumentParser()
668 self._tty_parser.add_argument('-t', action='count')
669 self._tty_parser.add_argument('-o', action='append')
670
671 self._populated_agent: pathlib.Path | None = None
672
673 # The connection is created by running ssh/scp/sftp from the exec_command,
674 # put_file, and fetch_file methods, so we don't need to do any connection
675 # management here.
676
677 def _connect(self) -> Connection:
…lines 678–740 unchanged (63 lines)
741 were added. It will be displayed with a high enough verbosity.
742 .. note:: This function does its work via side-effect. The b_command list has the new arguments appended.
743 """
744 display.vvvvv(u'SSH: %s: (%s)' % (explanation, ')('.join(to_text(a) for a in b_args)), host=self.host)
745 b_command += b_args
746
747 def _populate_agent(self) -> pathlib.Path:
748 """Adds configured private key identity to the SSH agent. Returns a path to a file containing the public key."""
749 if self._populated_agent:
750 return self._populated_agent
751
752 if (auth_sock := C.config.get_config_value('SSH_AGENT')) == 'none':
753 raise AnsibleError('Cannot utilize private_key with SSH_AGENT disabled')
754
755 key_data = self.get_option('private_key')
756 passphrase = self.get_option('private_key_passphrase')
757
758 private_key, public_key, fingerprint = _key_data_into_crypto_objects(
759 to_bytes(key_data),
760 to_bytes(passphrase) if passphrase else None,
761 )
762
763 with SshAgentClient(auth_sock) as client:
764 if public_key not in client:
765 display.vvv(f'SSH: SSH_AGENT adding {fingerprint} to agent', host=self.host)
766 client.add(
767 private_key,
768 f'[added by ansible: PID={os.getpid()}, UID={os.getuid()}, EUID={os.geteuid()}, TIME={time.time()}]',
769 C.config.get_config_value('SSH_AGENT_KEY_LIFETIME'),
770 )
771 else:
772 display.vvv(f'SSH: SSH_AGENT {fingerprint} exists in agent', host=self.host)
773 # Write the public key to disk, to be provided as IdentityFile.
774 # This allows ssh to pick an explicit key in the agent to use,
775 # preventing ssh from attempting all keys in the agent.
776 pubkey_path = self._populated_agent = pathlib.Path(C.DEFAULT_LOCAL_TMP).joinpath(
777 fingerprint.replace('/', '-') + '.pub'
778 )
779 if os.path.exists(pubkey_path):
780 return pubkey_path
781
782 with tempfile.NamedTemporaryFile(dir=C.DEFAULT_LOCAL_TMP, delete=False) as f:
783 f.write(public_key.public_bytes(
784 encoding=serialization.Encoding.OpenSSH,
785 format=serialization.PublicFormat.OpenSSH
786 ))
787 # move atomically to prevent race conditions, silently succeeds if the target exists
788 os.rename(f.name, pubkey_path)
789 os.chmod(pubkey_path, mode=0o400)
790
791 return self._populated_agent
792
793 def _build_command(self, binary: str, subsystem: str, *other_args: bytes | str) -> list[bytes]:
794 """
795 Takes a executable (ssh, scp, sftp or wrapper) and optional extra arguments and returns the remote command
796 wrapped in local ssh shell commands and ready for execution.
797
…lines 798–870 unchanged (73 lines)
871 self._add_args(b_command, b_args, u"ANSIBLE_HOST_KEY_CHECKING/host_key_checking disabled")
872
873 self.port = self.get_option('port')
874 if self.port is not None:
875 b_args = (b"-o", b"Port=" + to_bytes(self.port, nonstring='simplerepr', errors='surrogate_or_strict'))
876 self._add_args(b_command, b_args, u"ANSIBLE_REMOTE_PORT/remote_port/ansible_port set")
877
878 if self.get_option('private_key'):
879 try:
880 key = self._populate_agent()
881 except Exception as e:
882 raise AnsibleAuthenticationFailure(
883 'Failed to add configured private key into ssh-agent',
884 orig_exc=e,
885 )
886 b_args = (b'-o', b'IdentitiesOnly=yes', b'-o', to_bytes(f'IdentityFile="{key}"', errors='surrogate_or_strict'))
887 self._add_args(b_command, b_args, "ANSIBLE_PRIVATE_KEY/private_key set")
888 elif key := self.get_option('private_key_file'):
889 b_args = (b"-o", b'IdentityFile="' + to_bytes(os.path.expanduser(key), errors='surrogate_or_strict') + b'"')
890 self._add_args(b_command, b_args, u"ANSIBLE_PRIVATE_KEY_FILE/private_key_file/ansible_ssh_private_key_file set")
891
892 if not conn_password:
893 self._add_args(
894 b_command, (
…lines 895–1623 unchanged (729 lines)