From b2a0e8144b2ac361f5508d8380b9b1d9661c5e55 Mon Sep 17 00:00:00 2001 From: maddes-b Date: Sun, 10 Jan 2021 21:39:14 +0000 Subject: [PATCH 1/4] * Add prefix support to print. * Use pre-defined prefixes for debug and error messages to better distinguish log messages. --- ssh-ident | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/ssh-ident b/ssh-ident index 320aa6b..8ea8bef 100755 --- a/ssh-ident +++ b/ssh-ident @@ -122,14 +122,14 @@ to solve the problem: rsync -e '/path/to/ssh-ident' ... scp -S '/path/to/ssh-ident' ... -4) Replace the real ssh on the system with ssh-ident, and set the +4) Replace the real ssh on the system with ssh-ident, and set the BINARY_SSH configuration parameter to the original value. On Debian based system, you can make this change in a way that will survive automated upgrades and audits by running: dpkg-divert --divert /usr/bin/ssh.ssh-ident --rename /usr/bin/ssh - + After which, you will need to use: BINARY_SSH="/usr/bin/ssh.ssh-ident" @@ -391,6 +391,20 @@ class SshIdentPrint(object): loglevel = LOG_INFO if ShouldPrint(self.config, loglevel): + if "prefix" in kwargs: + args = list(args) + args.insert(0, kwargs["prefix"]) + args = tuple(args) + # make sure not to pass the prefix parameter to print + del kwargs["prefix"] + elif loglevel == LOG_DEBUG: + args = list(args) + args.insert(0, "[debug]") + args = tuple(args) + elif loglevel == LOG_ERROR: + args = list(args) + args.insert(0, "[ERROR]") + args = tuple(args) self.python_print(*args, **kwargs) __call__ = write @@ -632,7 +646,7 @@ def GetSessionTty(): tell us anything about the session having a /dev/tty associated or not. - For example, running + For example, running ssh -t user@remotehost './test.sh < /dev/null > /dev/null' @@ -890,7 +904,7 @@ def AutodetectBinary(argv, config): # The logic here is pretty straightforward: # - Try to eliminate the path of ssh-ident from PATH. # - Search for a binary with the same name of ssh-ident to run. - # + # # If this fails, we may end up in some sort of loop, where ssh-ident # tries to run itself. This should normally be detected later on, # where the code checks for the next binary to run. @@ -942,7 +956,7 @@ def AutodetectBinary(argv, config): ssh-ident was invoked in place of the binary {0} (determined from argv[0]). Neither this binary nor 'ssh' could be found in $PATH. - PATH="{1}" + PATH="{1}" You need to adjust your setup for ssh-ident to work: consider setting BINARY_SSH or BINARY_DIR in your config, or running ssh-ident some @@ -1004,8 +1018,8 @@ def main(argv): message = textwrap.dedent("""\ ssh-ident found '{0}' as the next command to run. Based on argv[0] ({1}), it seems like this will create a - loop. - + loop. + Please use BINARY_SSH, BINARY_DIR, or change the way ssh-ident is invoked (eg, a different argv[0]) to make it work correctly.""") From 746d174ee42e9dd7d8a14d675ee8170b9f282635 Mon Sep 17 00:00:00 2001 From: maddes-b Date: Sun, 10 Jan 2021 23:18:51 +0000 Subject: [PATCH 2/4] * Add test for MATCH_ARGV against all elements in a single string. Allows to check combinations of values in different elements, e.g. git passes host and repository as separate parameters, useful for GitHub, GitLab, Bitbucket or similar. * Enhance docstring to reflect the enhancement and give GitHub example. --- ssh-ident | 41 +++++++++++++++++++++++++++++++++-------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/ssh-ident b/ssh-ident index 8ea8bef..d30380a 100755 --- a/ssh-ident +++ b/ssh-ident @@ -156,15 +156,27 @@ To have multiple identities, all I have to do is: (r"opt/private", "secret"), ] - # If any of the ssh arguments have 'cweb' in it, the 'personal' identity - # has to be used. For example: "ssh myhost.cweb.com" will have cweb in - # argv, and the "personal" identity will be used. - # This is optional - don't include any MATCH_ARGV if you don't - # need it. + # Specifies which identity to use depending on the arguments I'm running + # ssh with. At first each element is separately tested against all patterns, + # then a string of all elements is tested against all patterns. + # This is optional - don't include any MATCH_ARGV if you don't need it. + # For example: If any of the ssh arguments have 'cweb' in it, the 'personal' + # identity has to be used. "ssh myhost.cweb.com" will have cweb in argv, and + # the "personal" identity will be used. MATCH_ARGV = [ (r"cweb", "personal"), (r"corp", "work"), ] + # Another example: Choose an identity for git depending on the accessed + # repository on GitHub (similar for GitLab, Bitbucket, etc.). + # Recognize the elements with the host and the repository path to switch + # between 'personal' and 'work' identity. + MATCH_ARGV = [ + (r"\s(git@)?github\.com\s.*'company\/.+\.git'", "work"), + (r"\s(git@)?github\.com\s.*'ccontavalli\/.+\.git'", "personal"), + (r"\s(git@)?gist\.github\.com\s.*'abcdef01234567890fedcba912345678\.git'", "work"), + (r"^(git@)?(gist\.)?github\.com$", "personal"), + ] # Note that if no match is found, the DEFAULT_IDENTITY is used. This is # generally your loginname, no need to change it. @@ -506,7 +518,7 @@ class Config(object): """Sets configuration option parameter to value.""" self.values[parameter] = value -def FindIdentityInList(elements, identities): +def FindIdentityInList(elements, identities, all_elements): """Matches a list of identities to a list of elements. Args: @@ -518,9 +530,22 @@ def FindIdentityInList(elements, identities): The identity specified in identities for the first regular expression matching the first element in elements. """ + # Test against each element separately for element in elements: for regex, identity in identities: if re.search(regex, element): + print("Matching: {0}".format(element), + file=sys.stderr, + loglevel=LOG_DEBUG) + return identity + # Test against all elements in a single string + if all_elements and len(elements) > 1: + element = " ".join(elements) + for regex, identity in identities: + if re.search(regex, element): + print("Matching: {0}".format(element), + file=sys.stderr, + loglevel=LOG_DEBUG) return identity return None @@ -537,8 +562,8 @@ def FindIdentity(argv, config): """ paths = set([os.getcwd(), os.path.abspath(os.getcwd()), os.path.normpath(os.getcwd())]) return ( - FindIdentityInList(argv, config.Get("MATCH_ARGV")) or - FindIdentityInList(paths, config.Get("MATCH_PATH")) or + FindIdentityInList(argv, config.Get("MATCH_ARGV"), True) or + FindIdentityInList(paths, config.Get("MATCH_PATH"), False) or config.Get("DEFAULT_IDENTITY")) def FindKeys(identity, config): From 698d94f53b027a91427eff6691936d79df12b4ca Mon Sep 17 00:00:00 2001 From: maddes-b Date: Mon, 11 Jan 2021 17:37:47 +0000 Subject: [PATCH 3/4] Enhance pattern handling for more flexibility * Transform PATTERN_KEYS into a list to avoid an unmanageable single complex regex, instead split in several simple regex * Transform hard-coded public and private key determination into configurable pattern lists: PATTERN_PUBKEYS, PATTERN_PRIVKEYS * A second parameter after the regex can be used to define what part to remove for key pair matching * PATTERN_KEYS, PATTERN_PUBKEYS, PATTERN_PRIVKEYS new only search/test against filename, and nomore their full path * Compile regex for PATTERN_KEYS, PATTERN_PUBKEYS, PATTERN_PRIVKEYS, MATCH_PATH, MATCH_ARGV for better performance. This is done in a general way within Config.get() * Add some debug prints for analyzing pattern matching * Add debug print of found key pairs including hint if corresponding public or private key couldn't be found * Enhance docstring to reflect changes * Enhance docstring with more details to key pair handling and pattern matching * Enhance docstring with already present features of per-identity ssh config file --- ssh-ident | 229 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 183 insertions(+), 46 deletions(-) diff --git a/ssh-ident b/ssh-ident index d30380a..e7c7bf8 100755 --- a/ssh-ident +++ b/ssh-ident @@ -22,9 +22,9 @@ In any case, ssh-ident: actually need them, once. No matter how many terminals, ssh or login sessions you have, no matter if your home is shared via NFS. -- can prepare and use a different agent and different set of keys depending - on the host you are connecting to, or the directory you are using ssh - from. +- can prepare and use a different agent, different set of keys and different + ssh config file depending on the host you are connecting to, or the + directory you are using ssh from. This allows for isolating keys when using agent forwarding with different sites (eg, university, work, home, secret evil internet identity, ...). It also allows to use multiple accounts on sites like github, unfuddle @@ -199,6 +199,7 @@ To have multiple identities, all I have to do is: # need it. # Otherwise, provides options to be passed to 'ssh' for specific # identities. + # Note that separate ssh config files per identity are possible too. SSH_OPTIONS = { # Disable forwarding of the agent, but enable X forwarding, # when using the work profile. @@ -241,6 +242,11 @@ To have multiple identities, all I have to do is: # Generate keys to be used for work only, rsa $ ssh-keygen -t rsa -b 4096 -f ~/.ssh/identities/work/id_rsa +5) Optionally create separate ssh config files for those identities that + need special ssh settings in general or for specific hosts: + + $ ${EDITOR} ~/.ssh/identities/secret/config + ... @@ -260,18 +266,79 @@ ssh-ident will be invoked instead, and: access only to the agent for the identity work, and the corresponding keys. -Note that ssh-ident needs to access both your private and public keys. Note -also that it identifies public keys by the .pub extension. All files in your -identities subdirectories will be considered keys. - -If you want to only load keys that have "key" in the name, you can add -to your .ssh-ident: - - PATTERN_KEYS = "key" +Notes about key files: +ssh-ident needs to access both your private and public keys. Both files of each +key pair have to reside in the same directory. +All files in your identities subdirectories that match PATTERN_KEYS will be +considered key files (either private or public). If a different naming scheme +is used, then make sure that PATTERN_KEYS matches filenames for both types. +By default ssh-ident identifies public keys by the .pub extension or +a "public" inside the filename, while private keys have no explicit extension +or a "private" inside the filename. To recognize a key pair these specific name +parts are removed, the remaining filenames compared and connected if they match. +A key is only recognized and loaded if the key pair is complete. +The public key file is necessary to detect if a key is already loaded into +ssh-agent to avoid adding it again and therefore asking for password again. +If a public key file is missing check out the '-y' parameter of 'ssh-keygen'. +All patterns to detect key files in general plus public and private keys are +defined in lists, which can hold multiple regular expressions or simple +compare strings. The first match is taken and no further tests done. +Patterns are tested against the filename, not the full path. + +The defaults of PATTERN_KEYS against the filename for the general key file +determination are: + + PATTERN_KEYS = [ + r"^id_", + r"^identity", + r"^ssh[0-9]-", + ] + +The defaults of PATTERN_PUBKEYS and PATTERN_PRIVKEYS for the public and private +key determination are: + + PATTERN_PUBKEYS = [ + [r"\.pub$", 0], + [r"public", 0], + ] + PATTERN_PRIVKEYS = [ + [r"private", 0], + # Fallback for all remaining files. + [r"", None], + ] + +Notes about PATTERN_PUBKEYS and PATTERN_PRIVKEYS: +ssh-ident first checks if the file is a public key, then if it is a private key. +The second parameter after the patterns defines which group to remove to +recognize key pairs. A zero (0) means remove the whole match. 'None' means do +not remove anything and leave filename as is. + +If you want to only load keys that have "mykey" in their filename, you can +define in your .ssh-ident: + + PATTERN_KEYS = [ + "mykey", + ] + +If you want to also load keys that have the extension ".key" or ".pub", then +you can define in your .ssh-ident: + + PATTERN_KEYS = [ + r"^id_", + r"^identity", + r"^ssh[0-9]-", + r"(\.key|\.pub)$", + ] + PATTERN_PRIVKEYS = [ + [r"\.key$", 0], + [r"private", 0], + # Fallback for all remaining files. + [r"", None], + ] + +Note: As the ".pub" and ".key" patterns come first, those filenames can also +have "public" or "private" in their name, e.g. their user name. -The default is: - - PATTERN_KEYS = r"/(id_.*|identity.*|ssh[0-9]-.*)" You can also redefine: @@ -435,7 +502,21 @@ class Config(object): "DIR_AGENTS": "$HOME/.ssh/agents", # How to identify key files in the identities directory. - "PATTERN_KEYS": r"/(id_.*|identity.*|ssh[0-9]-.*)", + "PATTERN_KEYS": [ + r"^id_", + r"^identity", + r"^ssh[0-9]-", + ], + # How to recognize public and private key files in the identities directory. + "PATTERN_PUBKEYS": [ + [r"\.pub$", 0], + [r"public", 0], + ], + "PATTERN_PRIVKEYS": [ + [r"private", 0], + # Fallback for all remaining files. + [r"", None], + ], # How to identify ssh config files. "PATTERN_CONFIG": r"/config$", @@ -502,23 +583,49 @@ class Config(object): def Get(self, parameter): """Returns the value of a parameter, or causes the script to exit.""" if parameter in os.environ: - return self.Expand(os.environ[parameter]) - if parameter in self.values: - return self.Expand(self.values[parameter]) - if parameter in self.defaults: - return self.Expand(self.defaults[parameter]) - - print( - "Parameter '{0}' needs to be defined in " - "config file or defaults".format(parameter), file=sys.stderr, - loglevel=LOG_ERROR) - sys.exit(2) + result = self.Expand(os.environ[parameter]) + elif parameter in self.values: + result = self.Expand(self.values[parameter]) + elif parameter in self.defaults: + result = self.Expand(self.defaults[parameter]) + else: + print( + "Parameter '{0}' needs to be defined in " + "config file or defaults".format(parameter), file=sys.stderr, + loglevel=LOG_ERROR) + sys.exit(2) + + # Compile patterns for speed + if parameter in ("PATTERN_KEYS", "PATTERN_PUBKEYS", "PATTERN_PRIVKEYS", "MATCH_PATH", "MATCH_ARGV"): + # Convert old format string, or wrongly used tuple, to list + if not isinstance(result, list): + # Convert tuple to list, as we need it mutable + if isinstance(result, tuple): + result = list(result) + else: + result = [result] + # Compile regex pattern [in first element] of each list entry + for index, entry in enumerate(result): + # Convert tuple to list, as we need it mutable + if isinstance(entry, tuple): + entry = result[index] = list(entry) + # Compile regex + if isinstance(entry, list): + entry[0] = re.compile(entry[0]) + else: + entry = result[index] = re.compile(entry) + # + print("{0} #{1}: {2}".format(parameter, index+1, entry), + file=sys.stderr, + loglevel=LOG_DEBUG) + + return result def Set(self, parameter, value): """Sets configuration option parameter to value.""" self.values[parameter] = value -def FindIdentityInList(elements, identities, all_elements): +def FindIdentityInList(elements, identities, all_elements, pattern_name): """Matches a list of identities to a list of elements. Args: @@ -532,18 +639,22 @@ def FindIdentityInList(elements, identities, all_elements): """ # Test against each element separately for element in elements: + index = 0 for regex, identity in identities: + index += 1 if re.search(regex, element): - print("Matching: {0}".format(element), + print("Matching {0} #{1}: {2}".format(pattern_name, index, element), file=sys.stderr, loglevel=LOG_DEBUG) return identity # Test against all elements in a single string if all_elements and len(elements) > 1: element = " ".join(elements) + index = 0 for regex, identity in identities: + index += 1 if re.search(regex, element): - print("Matching: {0}".format(element), + print("Matching {0} #{1}: {2}".format(pattern_name, index, element), file=sys.stderr, loglevel=LOG_DEBUG) return identity @@ -562,8 +673,8 @@ def FindIdentity(argv, config): """ paths = set([os.getcwd(), os.path.abspath(os.getcwd()), os.path.normpath(os.getcwd())]) return ( - FindIdentityInList(argv, config.Get("MATCH_ARGV"), True) or - FindIdentityInList(paths, config.Get("MATCH_PATH"), False) or + FindIdentityInList(argv, config.Get("MATCH_ARGV"), True, "MATCH_ARGV") or + FindIdentityInList(paths, config.Get("MATCH_PATH"), False, "MATCH_PATH") or config.Get("DEFAULT_IDENTITY")) def FindKeys(identity, config): @@ -587,7 +698,9 @@ def FindKeys(identity, config): if identity == getpass.getuser(): directories.append(os.path.expanduser("~/.ssh")) - pattern = re.compile(config.Get("PATTERN_KEYS")) + pattern_keys = config.Get("PATTERN_KEYS") + pattern_pub = config.Get("PATTERN_PUBKEYS") + pattern_priv = config.Get("PATTERN_PRIVKEYS") found = collections.defaultdict(dict) for directory in directories: try: @@ -597,28 +710,52 @@ def FindKeys(identity, config): continue raise - for key in keyfiles: - key = os.path.join(directory, key) - if not os.path.isfile(key): + for keyname in keyfiles: + keypath = os.path.join(directory, keyname) + if not os.path.isfile(keypath): continue - if not pattern.search(key): + # + match = None + for pattern in pattern_keys: + match = pattern.search(keyname) + if match: + break + if match is None: continue - - kinds = ( - ("private", "priv"), - ("public", "pub"), - (".pub", "pub"), - ("", "priv"), - ) - for match, kind in kinds: - if match in key: - found[key.replace(match, "")][kind] = key + # + match = None + if match is None: + for pattern in pattern_pub: + match = pattern[0].search(keyname) + if match: + kind = 'pub' + break + if match is None: + for pattern in pattern_priv: + match = pattern[0].search(keyname) + if match: + kind = 'priv' + break + if match is None: + continue + # + if match and not pattern[1] is None: + key = keyname[:match.start(pattern[1])] + keyname[match.end(pattern[1]):] + else: + key = keyname + key = os.path.join(directory, key) + found[key][kind] = keypath if not found: print("Warning: no keys found for identity {0} in:".format(identity), file=sys.stderr, loglevel=LOG_WARN) print(directories, file=sys.stderr, loglevel=LOG_WARN) + else: + index = 0 + for keyname in found: + index += 1 + print("Found key pair #{0} {1}: {2}{3}".format(index, keyname, found[keyname], " MISSING PUB" if not 'pub' in found[keyname] else " MISSING PRIV" if not 'priv' in found[keyname] else ""), file=sys.stderr, loglevel=LOG_DEBUG) return found From a0195d5634f0ee795d860b92c67323fd3c091ae6 Mon Sep 17 00:00:00 2001 From: maddes-b Date: Sat, 8 Jan 2022 00:34:20 +0000 Subject: [PATCH 4/4] Moved to https://github.com/ssh-ident/ssh-ident1 --- LICENSE | 27 - README | 591 +------------------ debian/changelog | 5 - debian/compat | 1 - debian/control | 15 - debian/copyright | 28 - debian/rules | 4 - debian/source/format | 1 - debian/ssh-ident.docs | 1 - debian/ssh-ident.install | 1 - ssh-ident | 1205 -------------------------------------- 11 files changed, 1 insertion(+), 1878 deletions(-) delete mode 100644 LICENSE delete mode 100644 debian/changelog delete mode 100644 debian/compat delete mode 100644 debian/control delete mode 100644 debian/copyright delete mode 100755 debian/rules delete mode 100644 debian/source/format delete mode 100644 debian/ssh-ident.docs delete mode 100644 debian/ssh-ident.install delete mode 100755 ssh-ident diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 88133d7..0000000 --- a/LICENSE +++ /dev/null @@ -1,27 +0,0 @@ -Copyright (c) 2012,2013 Carlo Contavalli (ccontavalli@gmail.com). -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY Carlo Contavalli ''AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL Carlo Contavalli OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are -those of the authors and should not be interpreted as representing official -policies, either expressed or implied, of Carlo Contavalli. diff --git a/README b/README index 1b7f719..c65decb 100644 --- a/README +++ b/README @@ -1,590 +1 @@ -Help on module ssh-ident: - -NAME - ssh-ident - Start and use ssh-agent and load identities as necessary. - -FILE - /opt/projects/ssh-ident.git/ssh-ident - -DESCRIPTION - Use this script to start ssh-agents and load ssh keys on demand, - when they are first needed. - - All you have to do is modify your .bashrc to have: - - alias ssh='/path/to/ssh-ident' - - or add a link to ssh-ident from a directory in your PATH, for example: - - ln -s /path/to/ssh-ident ~/bin/ssh - - If you use scp or rsync regularly, you should add a few more lines described - below. - - In any case, ssh-ident: - - - will create an ssh-agent and load the keys you need the first time you - actually need them, once. No matter how many terminals, ssh or login - sessions you have, no matter if your home is shared via NFS. - - - can prepare and use a different agent and different set of keys depending - on the host you are connecting to, or the directory you are using ssh - from. - This allows for isolating keys when using agent forwarding with different - sites (eg, university, work, home, secret evil internet identity, ...). - It also allows to use multiple accounts on sites like github, unfuddle - and gitorious easily. - - - allows to specify different options for each set of keys. For example, you - can provide a -t 60 to keep keys loaded for at most 60 seconds. Or -c to - always ask for confirmation before using a key. - - - Installation - ============ - - All you need to run ssh-ident is a standard installation of python >= 2.6, - python > 3 is supported. - - If your system has wget and are impatient to use it, you can install - ssh-ident with two simple commands: - - mkdir -p ~/bin; wget -O ~/bin/ssh goo.gl/MoJuKB; chmod 0755 ~/bin/ssh - - echo 'export PATH=~/bin:$PATH' >> ~/.bashrc - - Logout, login, and done. SSH should now invoke ssh-ident instead of the - standard ssh. - - - Alternatives - ============ - - In .bashrc, I have: - - alias ssh=/home/ccontavalli/scripts/ssh-ident - - all I have to do now is logout, login and then: - - $ ssh somewhere - - ssh-ident will be called instead of ssh, and it will: - - check if an agent is running. If not, it will start one. - - try to load all the keys in ~/.ssh, if not loaded. - - If I now ssh again, or somewhere else, ssh-ident will reuse the same agent - and the same keys, if valid. - - - About scp, rsync, and friends - ============================= - - scp, rsync, and most similar tools internally invoke ssh. If you don't tell - them to use ssh-ident instead, key loading won't work. There are a few ways - to solve the problem: - - 1) Rename 'ssh-ident' to 'ssh' or create a symlink 'ssh' pointing to - ssh-ident in a directory in your PATH before /usr/bin or /bin, similarly - to what was described previously. For example, add to your .bashrc: - - export PATH="~/bin:$PATH" - - And run: - - ln -s /path/to/ssh-ident ~/bin/ssh - - Make sure `echo $PATH` shows '~/bin' *before* '/usr/bin' or '/bin'. You - can verify this is working as expected with `which ssh`, which should - show ~/bin/ssh. - - This works for rsync and git, among others, but not for scp and sftp, as - these do not look for ssh in your PATH but use a hard-coded path to the - binary. - - If you want to use ssh-ident with scp or sftp, you can simply create - symlinks for them as well: - - ln -s /path/to/ssh-ident ~/bin/scp - ln -s /path/to/ssh-ident ~/bin/sftp - - 2) Add a few more aliases in your .bashrc file, for example: - - alias scp='BINARY_SSH=scp /path/to/ssh-ident' - alias rsync='BINARY_SSH=rsync /path/to/ssh-ident' - ... - - The first alias will make the 'scp' command invoke 'ssh-ident' instead, - but tell 'ssh-ident' to invoke 'scp' instead of the plain 'ssh' command - after loading the necessary agents and keys. - - Note that aliases don't work from scripts - if you have any script that - you expect to use with ssh-ident, you may prefer method 1), or you will - need to update the script accordingly. - - 3) Use command specific methods to force them to use ssh-ident instead of - ssh, for example: - - rsync -e '/path/to/ssh-ident' ... - scp -S '/path/to/ssh-ident' ... - - 4) Replace the real ssh on the system with ssh-ident, and set the - BINARY_SSH configuration parameter to the original value. - - On Debian based system, you can make this change in a way that - will survive automated upgrades and audits by running: - - dpkg-divert --divert /usr/bin/ssh.ssh-ident --rename /usr/bin/ssh - - After which, you will need to use: - - BINARY_SSH="/usr/bin/ssh.ssh-ident" - - - Config file with multiple identities - ==================================== - - To have multiple identities, all I have to do is: - - 1) create a ~/.ssh-ident file. In this file, I need to tell ssh-ident which - identities to use and when. The file should look something like: - - # Specifies which identity to use depending on the path I'm running ssh - # from. - # For example: ("mod-xslt", "personal") means that for any path that - # contains the word "mod-xslt", the "personal" identity should be used. - # This is optional - don't include any MATCH_PATH if you don't need it. - MATCH_PATH = [ - # (directory pattern, identity) - (r"mod-xslt", "personal"), - (r"ssh-ident", "personal"), - (r"opt/work", "work"), - (r"opt/private", "secret"), - ] - - # If any of the ssh arguments have 'cweb' in it, the 'personal' identity - # has to be used. For example: "ssh myhost.cweb.com" will have cweb in - # argv, and the "personal" identity will be used. - # This is optional - don't include any MATCH_ARGV if you don't - # need it. - MATCH_ARGV = [ - (r"cweb", "personal"), - (r"corp", "work"), - ] - - # Note that if no match is found, the DEFAULT_IDENTITY is used. This is - # generally your loginname, no need to change it. - # This is optional - don't include any DEFAULT_IDENTITY if you don't - # need it. - # DEFAULT_IDENTITY = "foo" - - # This is optional - don't include any SSH_ADD_OPTIONS if you don't - # need it. - SSH_ADD_OPTIONS = { - # Regardless, ask for confirmation before using any of the - # work keys. - "work": "-c", - # Forget about secret keys after ten minutes. ssh-ident will - # automatically ask you your passphrase again if they are needed. - "secret": "-t 600", - } - - # This is optional - dont' include any SSH_OPTIONS if you don't - # need it. - # Otherwise, provides options to be passed to 'ssh' for specific - # identities. - SSH_OPTIONS = { - # Disable forwarding of the agent, but enable X forwarding, - # when using the work profile. - "work": "-Xa", - - # Always forward the agent when using the secret identity. - "secret": "-A", - } - - # Options to pass to ssh by default. - # If you don't specify anything, UserRoaming=no is passed, due - # to CVE-2016-0777. Leave it empty to disable this. - SSH_DEFAULT_OPTIONS = "-oUseRoaming=no" - - # Which options to use by default if no match with SSH_ADD_OPTIONS - # was found. Note that ssh-ident hard codes -t 7200 to prevent your - # keys from remaining in memory for too long. - SSH_ADD_DEFAULT_OPTIONS = "-t 7200" - - # Output verbosity - # valid values are: LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG - VERBOSITY = LOG_INFO - - 2) Create the directory where all the identities and agents - will be kept: - - $ mkdir -p ~/.ssh/identities; chmod u=rwX,go= -R ~/.ssh - - 3) Create a directory for each identity, for example: - - $ mkdir -p ~/.ssh/identities/personal - $ mkdir -p ~/.ssh/identities/work - $ mkdir -p ~/.ssh/identities/secret - - 4) Generate (or copy) keys for those identities: - - # Default keys are for my personal account - $ cp ~/.ssh/id_rsa* ~/.ssh/identities/personal - - # Generate keys to be used for work only, rsa - $ ssh-keygen -t rsa -b 4096 -f ~/.ssh/identities/work/id_rsa - - ... - - - Now if I run: - - $ ssh corp.mywemployer.com - - ssh-ident will be invoked instead, and: - - 1) check ssh argv, determine that the "work" identity has to be used. - 2) look in ~/.ssh/agents, for a "work" agent loaded. If there is no - agent, it will prepare one. - 3) look in ~/.ssh/identities/work/* for a list of keys to load for this - identity. It will try to load any key that is not already loaded in - the agent. - 4) finally run ssh with the environment setup such that it will have - access only to the agent for the identity work, and the corresponding - keys. - - Note that ssh-ident needs to access both your private and public keys. Note - also that it identifies public keys by the .pub extension. All files in your - identities subdirectories will be considered keys. - - If you want to only load keys that have "key" in the name, you can add - to your .ssh-ident: - - PATTERN_KEYS = "key" - - The default is: - - PATTERN_KEYS = r"/(id_.*|identity.*|ssh[0-9]-.*)" - - You can also redefine: - - DIR_IDENTITIES = "$HOME/.ssh/identities" - DIR_AGENTS = "$HOME/.ssh/agents" - - To point somewhere else if you so desire. - - - BUILDING A DEBIAN PACKAGE - ========================= - - If you need to use ssh-ident on a debian / ubuntu / or any other - derivate, you can now build debian packages. - - 1. Make sure you have devscripts installed: - - sudo apt-get install devscripts debhelper - - 2. Download ssh-ident in a directory of your choice (ssh-ident) - - git clone https://github.com/ccontavalli/ssh-ident.git ssh-ident - - 3. Build the .deb package: - - cd ssh-ident && debuild -us -uc - - 4. Profit: - - cd ..; dpkg -i ssh-ident*.deb - - - CREDITS - ======= - - - Carlo Contavalli, http://www.github.com/ccontavalli, main author. - - Hubert depesz Lubaczewski, http://www.github.com/despez, support - for using environment variables for configuration. - - Flip Hess, http://www.github.com/fliphess, support for building - a .deb out of ssh-ident. - - Terrel Shumway, https://www.github.com/scholarly, port to python3. - - black2754, https://www.github.com/black2754, vim modeline, support - for verbosity settings, and BatchMode passing. - - Michael Heap, https://www.github.com/mheap, support for per - identities config files. - - Carl Drougge, https://www.github.com/drougge, CVE-2016-0777 fix, - fix for per user config files, and use /bin/env instead of python - path. - -CLASSES - __builtin__.object - AgentManager - Config - SshIdentPrint - - class AgentManager(__builtin__.object) - | Manages the ssh-agent for one identity. - | - | Methods defined here: - | - | FindUnloadedKeys(self, keys) - | Determines which keys have not been loaded yet. - | - | Args: - | keys: dict as returned by FindKeys. - | - | Returns: - | iterable of strings, paths to private key files to load. - | - | GetLoadedKeys(self) - | Returns an iterable of strings, each the fingerprint of a loaded key. - | - | GetShellArgs(self) - | Returns the flags to be passed to the shell to run a command. - | - | LoadKeyFiles(self, keys) - | Load all specified keys. - | - | Args: - | keys: iterable of strings, each string a path to a key to load. - | - | LoadUnloadedKeys(self, keys) - | Loads all the keys specified that are not loaded. - | - | Args: - | keys: dict as returned by FindKeys. - | - | RunSSH(self, argv) - | Execs ssh with the specified arguments. - | - | __init__(self, identity, sshconfig, config) - | Initializes an AgentManager object. - | - | Args: - | identity: string, identity the ssh-agent managed by this instance of - | an AgentManager will control. - | config: object implementing the Config interface, allows access to - | the user configuration parameters. - | - | Attributes: - | identity: same as above. - | config: same as above. - | agents_path: directory where the config of all agents is kept. - | agent_file: the config of the agent corresponding to this identity. - | - | Parameters: - | DIR_AGENTS: used to compute agents_path. - | BINARY_SSH: path to the ssh binary. - | - | ---------------------------------------------------------------------- - | Static methods defined here: - | - | EscapeShellArguments(argv) - | Escapes all arguments to the shell, returns a string. - | - | GetAgentFile(path, identity) - | Returns the path to an agent config file. - | - | Args: - | path: string, the path where agent config files are kept. - | identity: string, identity for which to load the agent. - | - | Returns: - | string, path to the agent file. - | - | GetPublicKeyFingerprint(key) - | Returns the fingerprint of a public key as a string. - | - | IsAgentFileValid(agentfile) - | Returns true if the specified agentfile refers to a running agent. - | - | RunShellCommand(command) - | Runs a shell command, returns (status, stdout), (int, string). - | - | RunShellCommandInAgent(agentfile, command, stdin=None, stdout=-1) - | Runs a shell command with an agent configured in the environment. - | - | ---------------------------------------------------------------------- - | Data descriptors defined here: - | - | __dict__ - | dictionary for instance variables (if defined) - | - | __weakref__ - | list of weak references to the object (if defined) - - class Config(__builtin__.object) - | Holds and loads users configurations. - | - | Methods defined here: - | - | Get(self, parameter) - | Returns the value of a parameter, or causes the script to exit. - | - | Load(self) - | Load configurations from the default user file. - | - | Set(self, parameter, value) - | Sets configuration option parameter to value. - | - | __init__(self) - | - | ---------------------------------------------------------------------- - | Static methods defined here: - | - | Expand(value) - | Expand environment variables or ~ in string parameters. - | - | ---------------------------------------------------------------------- - | Data descriptors defined here: - | - | __dict__ - | dictionary for instance variables (if defined) - | - | __weakref__ - | list of weak references to the object (if defined) - | - | ---------------------------------------------------------------------- - | Data and other attributes defined here: - | - | defaults = {'BINARY_DIR': None, 'BINARY_SSH': None, 'DEFAULT_IDENTITY'... - - class SshIdentPrint(__builtin__.object) - | Wrapper around python's print function. - | - | Methods defined here: - | - | __call__ = write(self, *args, **kwargs) - | - | __init__(self, config) - | config: object implementing the Config interface, allows access to - | the user configuration parameters. - | - | Attributes: - | config: same as above. - | python_print: python's print function (hopefully) - | - | Parameters: - | SSH_BATCH_MODE: used to check if messages should be printed or not - | VERBOSITY: used to check if messages should be printed or not - | - | write(self, *args, **kwargs) - | Passes all parameters to python's print, - | unless output is disabled by the configuration. - | The interface is compatible with python's print, but supports the - | optional parameter 'loglevel' in addition. - | - | ---------------------------------------------------------------------- - | Data descriptors defined here: - | - | __dict__ - | dictionary for instance variables (if defined) - | - | __weakref__ - | list of weak references to the object (if defined) - -FUNCTIONS - AutodetectBinary(argv, config) - Detects the correct binary to run and sets BINARY_SSH accordingly, - if it is not already set. - - FindIdentity(argv, config) - Returns the identity to use based on current directory or argv. - - Args: - argv: iterable of string, argv passed to this program. - config: instance of an object implementing the same interface as - the Config class. - - Returns: - string, the name of the identity to use. - - FindIdentityInList(elements, identities) - Matches a list of identities to a list of elements. - - Args: - elements: iterable of strings, arbitrary strings to match on. - identities: iterable of (string, string), with first string - being a regular expression, the second string being an identity. - - Returns: - The identity specified in identities for the first regular expression - matching the first element in elements. - - FindKeys(identity, config) - Finds all the private and public keys associated with an identity. - - Args: - identity: string, name of the identity to load strings of. - config: object implementing the Config interface, providing configurations - for the user. - - Returns: - dict, {"key name": {"pub": "/path/to/public/key", "priv": - "/path/to/private/key"}}, for each key found, the path of the public - key and private key. The key name is just a string representing the - key. Note that for a given key, it is not guaranteed that both the - public and private key will be found. - The return value is affected by DIR_IDENTITIES and PATTERN_KEYS - configuration parameters. - - FindSSHConfig(identity, config) - Finds a config file if there's one associated with an identity - - Args: - identity: string, name of the identity to load strings of. - config: object implementing the Config interface, providing configurations - for the user. - - Returns: - string, the configuration file to use - - GetSessionTty() - Returns a file descriptor for the session TTY, or None. - - In *nix systems, each process is tied to one session. Each - session can be tied (or not) to a terminal, "/dev/tty". - - Additionally, when a command is run, its stdin or stdout can - be any file descriptor, including one that represent a tty. - - So for example: - - ./test.sh < /dev/null > /dev/null - - will have stdin and stdout tied to /dev/null - but does not - tell us anything about the session having a /dev/tty associated - or not. - - For example, running - - ssh -t user@remotehost './test.sh < /dev/null > /dev/null' - - have a tty associated, while the same command without -t will not. - - When ssh is invoked by tools like git or rsyn, its stdin and stdout - is often tied to a file descriptor which is not a terminal, has - the tool wants to provide the input and process the output. - - ssh-ident internally has to invoke ssh-add, which needs to know if - it has any terminal it can use at all. - - This function returns an open file if the session has an usable terminal, - None otherwise. - - ParseCommandLine(argv, config) - Parses the command line parameters in argv - and modifies config accordingly. - - ShouldPrint(config, loglevel) - Returns true if a message by the specified loglevel should be printed. - - main(argv) - -DATA - LOG_CONSTANTS = {'LOG_DEBUG': 4, 'LOG_ERROR': 1, 'LOG_INFO': 3, 'LOG_W... - LOG_DEBUG = 4 - LOG_ERROR = 1 - LOG_INFO = 3 - LOG_WARN = 2 - print_function = _Feature((2, 6, 0, 'alpha', 2), (3, 0, 0, 'alpha', 0)... - - +Moved to https://github.com/ssh-ident/ssh-ident1 diff --git a/debian/changelog b/debian/changelog deleted file mode 100644 index d5c8306..0000000 --- a/debian/changelog +++ /dev/null @@ -1,5 +0,0 @@ -ssh-ident (20140619.2318.1) unstable; urgency=low - - * Setup initial debian package - - -- Flip Hess Thu, 19 Jun 2014 23:22:22 +0200 diff --git a/debian/compat b/debian/compat deleted file mode 100644 index f11c82a..0000000 --- a/debian/compat +++ /dev/null @@ -1 +0,0 @@ -9 \ No newline at end of file diff --git a/debian/control b/debian/control deleted file mode 100644 index 08ffe3b..0000000 --- a/debian/control +++ /dev/null @@ -1,15 +0,0 @@ -Source: ssh-ident -Maintainer: Carlo Contavalli (ccontavalli@gmail.com). -Section: admin -Priority: extra -Build-Depends: debhelper (>= 7) -Standards-Version: 3.9.4 -Vcs-Git: git://github.com/ccontavalli/ssh-ident -Vcs-Browser: https://github.com/ccontavalli/ssh-ident - -Package: ssh-ident -Architecture: all -Depends: bash -Description: ssh-ident - Start and use ssh-agent and load identities as necessary - Use this script to start ssh-agents and load ssh keys on demand, - when they are first needed. diff --git a/debian/copyright b/debian/copyright deleted file mode 100644 index 54ef9c6..0000000 --- a/debian/copyright +++ /dev/null @@ -1,28 +0,0 @@ -Files: * -Copyright (c) 2012,2013 Carlo Contavalli (ccontavalli@gmail.com). -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - - 1. Redistributions of source code must retain the above copyright notice, - this list of conditions and the following disclaimer. - - 2. Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY Carlo Contavalli ''AS IS'' AND ANY EXPRESS OR -IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF -MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO -EVENT SHALL Carlo Contavalli OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, -INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF -THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -The views and conclusions contained in the software and documentation are -those of the authors and should not be interpreted as representing official -policies, either expressed or implied, of Carlo Contavalli. diff --git a/debian/rules b/debian/rules deleted file mode 100755 index 2d33f6a..0000000 --- a/debian/rules +++ /dev/null @@ -1,4 +0,0 @@ -#!/usr/bin/make -f - -%: - dh $@ diff --git a/debian/source/format b/debian/source/format deleted file mode 100644 index 89ae9db..0000000 --- a/debian/source/format +++ /dev/null @@ -1 +0,0 @@ -3.0 (native) diff --git a/debian/ssh-ident.docs b/debian/ssh-ident.docs deleted file mode 100644 index e845566..0000000 --- a/debian/ssh-ident.docs +++ /dev/null @@ -1 +0,0 @@ -README diff --git a/debian/ssh-ident.install b/debian/ssh-ident.install deleted file mode 100644 index 3db76f4..0000000 --- a/debian/ssh-ident.install +++ /dev/null @@ -1 +0,0 @@ -ssh-ident usr/sbin/ diff --git a/ssh-ident b/ssh-ident deleted file mode 100755 index e7c7bf8..0000000 --- a/ssh-ident +++ /dev/null @@ -1,1205 +0,0 @@ -#!/usr/bin/env python -# vim: tabstop=2 shiftwidth=2 expandtab -"""Start and use ssh-agent and load identities as necessary. - -Use this script to start ssh-agents and load ssh keys on demand, -when they are first needed. - -All you have to do is modify your .bashrc to have: - - alias ssh='/path/to/ssh-ident' - -or add a link to ssh-ident from a directory in your PATH, for example: - - ln -s /path/to/ssh-ident ~/bin/ssh - -If you use scp or rsync regularly, you should add a few more lines described -below. - -In any case, ssh-ident: - -- will create an ssh-agent and load the keys you need the first time you - actually need them, once. No matter how many terminals, ssh or login - sessions you have, no matter if your home is shared via NFS. - -- can prepare and use a different agent, different set of keys and different - ssh config file depending on the host you are connecting to, or the - directory you are using ssh from. - This allows for isolating keys when using agent forwarding with different - sites (eg, university, work, home, secret evil internet identity, ...). - It also allows to use multiple accounts on sites like github, unfuddle - and gitorious easily. - -- allows to specify different options for each set of keys. For example, you - can provide a -t 60 to keep keys loaded for at most 60 seconds. Or -c to - always ask for confirmation before using a key. - - -Installation -============ - -All you need to run ssh-ident is a standard installation of python >= 2.6, -python > 3 is supported. - -If your system has wget and are impatient to use it, you can install -ssh-ident with two simple commands: - - mkdir -p ~/bin; wget -O ~/bin/ssh goo.gl/MoJuKB; chmod 0755 ~/bin/ssh - - echo 'export PATH=~/bin:$PATH' >> ~/.bashrc - -Logout, login, and done. SSH should now invoke ssh-ident instead of the -standard ssh. - - -Alternatives -============ - -In .bashrc, I have: - - alias ssh=/home/ccontavalli/scripts/ssh-ident - -all I have to do now is logout, login and then: - - $ ssh somewhere - -ssh-ident will be called instead of ssh, and it will: -- check if an agent is running. If not, it will start one. -- try to load all the keys in ~/.ssh, if not loaded. - -If I now ssh again, or somewhere else, ssh-ident will reuse the same agent -and the same keys, if valid. - - -About scp, rsync, and friends -============================= - -scp, rsync, and most similar tools internally invoke ssh. If you don't tell -them to use ssh-ident instead, key loading won't work. There are a few ways -to solve the problem: - -1) Rename 'ssh-ident' to 'ssh' or create a symlink 'ssh' pointing to - ssh-ident in a directory in your PATH before /usr/bin or /bin, similarly - to what was described previously. For example, add to your .bashrc: - - export PATH="~/bin:$PATH" - - And run: - - ln -s /path/to/ssh-ident ~/bin/ssh - - Make sure `echo $PATH` shows '~/bin' *before* '/usr/bin' or '/bin'. You - can verify this is working as expected with `which ssh`, which should - show ~/bin/ssh. - - This works for rsync and git, among others, but not for scp and sftp, as - these do not look for ssh in your PATH but use a hard-coded path to the - binary. - - If you want to use ssh-ident with scp or sftp, you can simply create - symlinks for them as well: - - ln -s /path/to/ssh-ident ~/bin/scp - ln -s /path/to/ssh-ident ~/bin/sftp - -2) Add a few more aliases in your .bashrc file, for example: - - alias scp='BINARY_SSH=scp /path/to/ssh-ident' - alias rsync='BINARY_SSH=rsync /path/to/ssh-ident' - ... - - The first alias will make the 'scp' command invoke 'ssh-ident' instead, - but tell 'ssh-ident' to invoke 'scp' instead of the plain 'ssh' command - after loading the necessary agents and keys. - - Note that aliases don't work from scripts - if you have any script that - you expect to use with ssh-ident, you may prefer method 1), or you will - need to update the script accordingly. - -3) Use command specific methods to force them to use ssh-ident instead of - ssh, for example: - - rsync -e '/path/to/ssh-ident' ... - scp -S '/path/to/ssh-ident' ... - -4) Replace the real ssh on the system with ssh-ident, and set the - BINARY_SSH configuration parameter to the original value. - - On Debian based system, you can make this change in a way that - will survive automated upgrades and audits by running: - - dpkg-divert --divert /usr/bin/ssh.ssh-ident --rename /usr/bin/ssh - - After which, you will need to use: - - BINARY_SSH="/usr/bin/ssh.ssh-ident" - - -Config file with multiple identities -==================================== - -To have multiple identities, all I have to do is: - -1) create a ~/.ssh-ident file. In this file, I need to tell ssh-ident which - identities to use and when. The file should look something like: - - # Specifies which identity to use depending on the path I'm running ssh - # from. - # For example: ("mod-xslt", "personal") means that for any path that - # contains the word "mod-xslt", the "personal" identity should be used. - # This is optional - don't include any MATCH_PATH if you don't need it. - MATCH_PATH = [ - # (directory pattern, identity) - (r"mod-xslt", "personal"), - (r"ssh-ident", "personal"), - (r"opt/work", "work"), - (r"opt/private", "secret"), - ] - - # Specifies which identity to use depending on the arguments I'm running - # ssh with. At first each element is separately tested against all patterns, - # then a string of all elements is tested against all patterns. - # This is optional - don't include any MATCH_ARGV if you don't need it. - # For example: If any of the ssh arguments have 'cweb' in it, the 'personal' - # identity has to be used. "ssh myhost.cweb.com" will have cweb in argv, and - # the "personal" identity will be used. - MATCH_ARGV = [ - (r"cweb", "personal"), - (r"corp", "work"), - ] - # Another example: Choose an identity for git depending on the accessed - # repository on GitHub (similar for GitLab, Bitbucket, etc.). - # Recognize the elements with the host and the repository path to switch - # between 'personal' and 'work' identity. - MATCH_ARGV = [ - (r"\s(git@)?github\.com\s.*'company\/.+\.git'", "work"), - (r"\s(git@)?github\.com\s.*'ccontavalli\/.+\.git'", "personal"), - (r"\s(git@)?gist\.github\.com\s.*'abcdef01234567890fedcba912345678\.git'", "work"), - (r"^(git@)?(gist\.)?github\.com$", "personal"), - ] - - # Note that if no match is found, the DEFAULT_IDENTITY is used. This is - # generally your loginname, no need to change it. - # This is optional - don't include any DEFAULT_IDENTITY if you don't - # need it. - # DEFAULT_IDENTITY = "foo" - - # This is optional - don't include any SSH_ADD_OPTIONS if you don't - # need it. - SSH_ADD_OPTIONS = { - # Regardless, ask for confirmation before using any of the - # work keys. - "work": "-c", - # Forget about secret keys after ten minutes. ssh-ident will - # automatically ask you your passphrase again if they are needed. - "secret": "-t 600", - } - - # This is optional - dont' include any SSH_OPTIONS if you don't - # need it. - # Otherwise, provides options to be passed to 'ssh' for specific - # identities. - # Note that separate ssh config files per identity are possible too. - SSH_OPTIONS = { - # Disable forwarding of the agent, but enable X forwarding, - # when using the work profile. - "work": "-Xa", - - # Always forward the agent when using the secret identity. - "secret": "-A", - } - - # Options to pass to ssh by default. - # If you don't specify anything, UserRoaming=no is passed, due - # to CVE-2016-0777. Leave it empty to disable this. - SSH_DEFAULT_OPTIONS = "-oUseRoaming=no" - - # Which options to use by default if no match with SSH_ADD_OPTIONS - # was found. Note that ssh-ident hard codes -t 7200 to prevent your - # keys from remaining in memory for too long. - SSH_ADD_DEFAULT_OPTIONS = "-t 7200" - - # Output verbosity - # valid values are: LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG - VERBOSITY = LOG_INFO - -2) Create the directory where all the identities and agents - will be kept: - - $ mkdir -p ~/.ssh/identities; chmod u=rwX,go= -R ~/.ssh - -3) Create a directory for each identity, for example: - - $ mkdir -p ~/.ssh/identities/personal - $ mkdir -p ~/.ssh/identities/work - $ mkdir -p ~/.ssh/identities/secret - -4) Generate (or copy) keys for those identities: - - # Default keys are for my personal account - $ cp ~/.ssh/id_rsa* ~/.ssh/identities/personal - - # Generate keys to be used for work only, rsa - $ ssh-keygen -t rsa -b 4096 -f ~/.ssh/identities/work/id_rsa - -5) Optionally create separate ssh config files for those identities that - need special ssh settings in general or for specific hosts: - - $ ${EDITOR} ~/.ssh/identities/secret/config - - ... - - -Now if I run: - - $ ssh corp.mywemployer.com - -ssh-ident will be invoked instead, and: - - 1) check ssh argv, determine that the "work" identity has to be used. - 2) look in ~/.ssh/agents, for a "work" agent loaded. If there is no - agent, it will prepare one. - 3) look in ~/.ssh/identities/work/* for a list of keys to load for this - identity. It will try to load any key that is not already loaded in - the agent. - 4) finally run ssh with the environment setup such that it will have - access only to the agent for the identity work, and the corresponding - keys. - -Notes about key files: -ssh-ident needs to access both your private and public keys. Both files of each -key pair have to reside in the same directory. -All files in your identities subdirectories that match PATTERN_KEYS will be -considered key files (either private or public). If a different naming scheme -is used, then make sure that PATTERN_KEYS matches filenames for both types. -By default ssh-ident identifies public keys by the .pub extension or -a "public" inside the filename, while private keys have no explicit extension -or a "private" inside the filename. To recognize a key pair these specific name -parts are removed, the remaining filenames compared and connected if they match. -A key is only recognized and loaded if the key pair is complete. -The public key file is necessary to detect if a key is already loaded into -ssh-agent to avoid adding it again and therefore asking for password again. -If a public key file is missing check out the '-y' parameter of 'ssh-keygen'. -All patterns to detect key files in general plus public and private keys are -defined in lists, which can hold multiple regular expressions or simple -compare strings. The first match is taken and no further tests done. -Patterns are tested against the filename, not the full path. - -The defaults of PATTERN_KEYS against the filename for the general key file -determination are: - - PATTERN_KEYS = [ - r"^id_", - r"^identity", - r"^ssh[0-9]-", - ] - -The defaults of PATTERN_PUBKEYS and PATTERN_PRIVKEYS for the public and private -key determination are: - - PATTERN_PUBKEYS = [ - [r"\.pub$", 0], - [r"public", 0], - ] - PATTERN_PRIVKEYS = [ - [r"private", 0], - # Fallback for all remaining files. - [r"", None], - ] - -Notes about PATTERN_PUBKEYS and PATTERN_PRIVKEYS: -ssh-ident first checks if the file is a public key, then if it is a private key. -The second parameter after the patterns defines which group to remove to -recognize key pairs. A zero (0) means remove the whole match. 'None' means do -not remove anything and leave filename as is. - -If you want to only load keys that have "mykey" in their filename, you can -define in your .ssh-ident: - - PATTERN_KEYS = [ - "mykey", - ] - -If you want to also load keys that have the extension ".key" or ".pub", then -you can define in your .ssh-ident: - - PATTERN_KEYS = [ - r"^id_", - r"^identity", - r"^ssh[0-9]-", - r"(\.key|\.pub)$", - ] - PATTERN_PRIVKEYS = [ - [r"\.key$", 0], - [r"private", 0], - # Fallback for all remaining files. - [r"", None], - ] - -Note: As the ".pub" and ".key" patterns come first, those filenames can also -have "public" or "private" in their name, e.g. their user name. - - -You can also redefine: - - DIR_IDENTITIES = "$HOME/.ssh/identities" - DIR_AGENTS = "$HOME/.ssh/agents" - -To point somewhere else if you so desire. - - -BUILDING A DEBIAN PACKAGE -========================= - -If you need to use ssh-ident on a debian / ubuntu / or any other -derivate, you can now build debian packages. - - 1. Make sure you have devscripts installed: - - sudo apt-get install devscripts debhelper - - 2. Download ssh-ident in a directory of your choice (ssh-ident) - - git clone https://github.com/ccontavalli/ssh-ident.git ssh-ident - - 3. Build the .deb package: - - cd ssh-ident && debuild -us -uc - - 4. Profit: - - cd ..; dpkg -i ssh-ident*.deb - - -CREDITS -======= - -- Carlo Contavalli, http://www.github.com/ccontavalli, main author. -- Hubert depesz Lubaczewski, http://www.github.com/despez, support - for using environment variables for configuration. -- Flip Hess, http://www.github.com/fliphess, support for building - a .deb out of ssh-ident. -- Terrel Shumway, https://www.github.com/scholarly, port to python3. -- black2754, https://www.github.com/black2754, vim modeline, support - for verbosity settings, and BatchMode passing. -- Michael Heap, https://www.github.com/mheap, support for per - identities config files. -- Carl Drougge, https://www.github.com/drougge, CVE-2016-0777 fix, - fix for per user config files, and use /bin/env instead of python - path. -""" - -from __future__ import print_function - -import collections -import distutils.spawn -import errno -import fcntl -import getpass -import glob -import os -import re -import socket -import subprocess -import sys -import termios -import textwrap - -# constants so noone has deal with cryptic numbers -LOG_CONSTANTS = {"LOG_ERROR": 1, "LOG_WARN": 2, "LOG_INFO": 3, "LOG_DEBUG": 4} -# load them directly into the global scope, for easy use -# not exactly pretty... -globals().update(LOG_CONSTANTS) - - -def ShouldPrint(config, loglevel): - """Returns true if a message by the specified loglevel should be printed.""" - # determine the current output verbosity - verbosity = config.Get("VERBOSITY") - - # verbosity may be a string, e.g. 'LOG_INFO' - # this happens when it comes from the OS env, but also if quotes are - # used in the config file - if isinstance(verbosity, str): - if verbosity in LOG_CONSTANTS: - # resolve the loglevel, e.g. 'LOG_INFO' -> 3 - verbosity = LOG_CONSTANTS[verbosity] - else: - # the string may also be a number, e.g. '3' -> 3 - verbosity = int(verbosity) - if loglevel <= verbosity: - return True - return False - - -class SshIdentPrint(object): - """Wrapper around python's print function.""" - - def __init__(self, config): - """ - config: object implementing the Config interface, allows access to - the user configuration parameters. - - Attributes: - config: same as above. - python_print: python's print function (hopefully) - - Parameters: - SSH_BATCH_MODE: used to check if messages should be printed or not - VERBOSITY: used to check if messages should be printed or not - """ - self.config = config - self.python_print = print - - def write(self, *args, **kwargs): - """Passes all parameters to python's print, - unless output is disabled by the configuration. - The interface is compatible with python's print, but supports the - optional parameter 'loglevel' in addition.""" - if self.config.Get("SSH_BATCH_MODE"): - # no output in BatchMode - return - - # determine the loglevel of this message - if "loglevel" in kwargs: - loglevel = kwargs["loglevel"] - # make sure not to pass the loglevel parameter to print - del kwargs["loglevel"] - else: - # if the loglevel is not given, default to INFO - loglevel = LOG_INFO - - if ShouldPrint(self.config, loglevel): - if "prefix" in kwargs: - args = list(args) - args.insert(0, kwargs["prefix"]) - args = tuple(args) - # make sure not to pass the prefix parameter to print - del kwargs["prefix"] - elif loglevel == LOG_DEBUG: - args = list(args) - args.insert(0, "[debug]") - args = tuple(args) - elif loglevel == LOG_ERROR: - args = list(args) - args.insert(0, "[ERROR]") - args = tuple(args) - self.python_print(*args, **kwargs) - - __call__ = write - - -class Config(object): - """Holds and loads users configurations.""" - - defaults = { - # Where to find the per-user configuration. - "FILE_USER_CONFIG": "$HOME/.ssh-ident", - - # Where to find all the identities for the user. - "DIR_IDENTITIES": "$HOME/.ssh/identities", - # Where to keep the information about each running agent. - "DIR_AGENTS": "$HOME/.ssh/agents", - - # How to identify key files in the identities directory. - "PATTERN_KEYS": [ - r"^id_", - r"^identity", - r"^ssh[0-9]-", - ], - # How to recognize public and private key files in the identities directory. - "PATTERN_PUBKEYS": [ - [r"\.pub$", 0], - [r"public", 0], - ], - "PATTERN_PRIVKEYS": [ - [r"private", 0], - # Fallback for all remaining files. - [r"", None], - ], - - # How to identify ssh config files. - "PATTERN_CONFIG": r"/config$", - - # Dictionary with identity as a key, automatically adds - # the specified options to the ssh command run. - "SSH_OPTIONS": {}, - # Additional options to append to ssh by default. - "SSH_DEFAULT_OPTIONS": "-oUseRoaming=no", - - # Complete path of full ssh binary to use. If not set, ssh-ident will - # try to find the correct binary in PATH. - "BINARY_SSH": None, - "BINARY_DIR": None, - - # Which identity to use by default if we cannot tell from - # the current working directory and/or argv. - "DEFAULT_IDENTITY": "$USER", - - # Those should really be overridden by the user. Look - # at the documentation for more details. - "MATCH_PATH": [], - "MATCH_ARGV": [], - - # Dictionary with identity as a key, allows to specify - # per identity options when using ssh-add. - "SSH_ADD_OPTIONS": {}, - # ssh-add default options. By default, don't keep a key longer - # than 2 hours. - "SSH_ADD_DEFAULT_OPTIONS": "-t 7200", - - # Like BatchMode in ssh, see man 5 ssh_config. - # In BatchMode ssh-ident will not print any output and not ask for - # any passphrases. - "SSH_BATCH_MODE": False, - - # Output verbosity - # valid values are: LOG_ERROR, LOG_WARN, LOG_INFO, LOG_DEBUG - # use 0 to disable ALL output (not recommended!) - "VERBOSITY": LOG_INFO, - } - - def __init__(self): - self.values = {} - - def Load(self): - """Load configurations from the default user file.""" - path = self.Get("FILE_USER_CONFIG") - variables = {} - try: - exec(compile(open(path).read(), path, 'exec'), LOG_CONSTANTS, variables) - except IOError: - return self - self.values = variables - return self - - @staticmethod - def Expand(value): - """Expand environment variables or ~ in string parameters.""" - if isinstance(value, str): - return os.path.expanduser(os.path.expandvars(value)) - return value - - def Get(self, parameter): - """Returns the value of a parameter, or causes the script to exit.""" - if parameter in os.environ: - result = self.Expand(os.environ[parameter]) - elif parameter in self.values: - result = self.Expand(self.values[parameter]) - elif parameter in self.defaults: - result = self.Expand(self.defaults[parameter]) - else: - print( - "Parameter '{0}' needs to be defined in " - "config file or defaults".format(parameter), file=sys.stderr, - loglevel=LOG_ERROR) - sys.exit(2) - - # Compile patterns for speed - if parameter in ("PATTERN_KEYS", "PATTERN_PUBKEYS", "PATTERN_PRIVKEYS", "MATCH_PATH", "MATCH_ARGV"): - # Convert old format string, or wrongly used tuple, to list - if not isinstance(result, list): - # Convert tuple to list, as we need it mutable - if isinstance(result, tuple): - result = list(result) - else: - result = [result] - # Compile regex pattern [in first element] of each list entry - for index, entry in enumerate(result): - # Convert tuple to list, as we need it mutable - if isinstance(entry, tuple): - entry = result[index] = list(entry) - # Compile regex - if isinstance(entry, list): - entry[0] = re.compile(entry[0]) - else: - entry = result[index] = re.compile(entry) - # - print("{0} #{1}: {2}".format(parameter, index+1, entry), - file=sys.stderr, - loglevel=LOG_DEBUG) - - return result - - def Set(self, parameter, value): - """Sets configuration option parameter to value.""" - self.values[parameter] = value - -def FindIdentityInList(elements, identities, all_elements, pattern_name): - """Matches a list of identities to a list of elements. - - Args: - elements: iterable of strings, arbitrary strings to match on. - identities: iterable of (string, string), with first string - being a regular expression, the second string being an identity. - - Returns: - The identity specified in identities for the first regular expression - matching the first element in elements. - """ - # Test against each element separately - for element in elements: - index = 0 - for regex, identity in identities: - index += 1 - if re.search(regex, element): - print("Matching {0} #{1}: {2}".format(pattern_name, index, element), - file=sys.stderr, - loglevel=LOG_DEBUG) - return identity - # Test against all elements in a single string - if all_elements and len(elements) > 1: - element = " ".join(elements) - index = 0 - for regex, identity in identities: - index += 1 - if re.search(regex, element): - print("Matching {0} #{1}: {2}".format(pattern_name, index, element), - file=sys.stderr, - loglevel=LOG_DEBUG) - return identity - return None - -def FindIdentity(argv, config): - """Returns the identity to use based on current directory or argv. - - Args: - argv: iterable of string, argv passed to this program. - config: instance of an object implementing the same interface as - the Config class. - - Returns: - string, the name of the identity to use. - """ - paths = set([os.getcwd(), os.path.abspath(os.getcwd()), os.path.normpath(os.getcwd())]) - return ( - FindIdentityInList(argv, config.Get("MATCH_ARGV"), True, "MATCH_ARGV") or - FindIdentityInList(paths, config.Get("MATCH_PATH"), False, "MATCH_PATH") or - config.Get("DEFAULT_IDENTITY")) - -def FindKeys(identity, config): - """Finds all the private and public keys associated with an identity. - - Args: - identity: string, name of the identity to load strings of. - config: object implementing the Config interface, providing configurations - for the user. - - Returns: - dict, {"key name": {"pub": "/path/to/public/key", "priv": - "/path/to/private/key"}}, for each key found, the path of the public - key and private key. The key name is just a string representing the - key. Note that for a given key, it is not guaranteed that both the - public and private key will be found. - The return value is affected by DIR_IDENTITIES and PATTERN_KEYS - configuration parameters. - """ - directories = [os.path.join(config.Get("DIR_IDENTITIES"), identity)] - if identity == getpass.getuser(): - directories.append(os.path.expanduser("~/.ssh")) - - pattern_keys = config.Get("PATTERN_KEYS") - pattern_pub = config.Get("PATTERN_PUBKEYS") - pattern_priv = config.Get("PATTERN_PRIVKEYS") - found = collections.defaultdict(dict) - for directory in directories: - try: - keyfiles = os.listdir(directory) - except OSError as e: - if e.errno == errno.ENOENT: - continue - raise - - for keyname in keyfiles: - keypath = os.path.join(directory, keyname) - if not os.path.isfile(keypath): - continue - # - match = None - for pattern in pattern_keys: - match = pattern.search(keyname) - if match: - break - if match is None: - continue - # - match = None - if match is None: - for pattern in pattern_pub: - match = pattern[0].search(keyname) - if match: - kind = 'pub' - break - if match is None: - for pattern in pattern_priv: - match = pattern[0].search(keyname) - if match: - kind = 'priv' - break - if match is None: - continue - # - if match and not pattern[1] is None: - key = keyname[:match.start(pattern[1])] + keyname[match.end(pattern[1]):] - else: - key = keyname - key = os.path.join(directory, key) - found[key][kind] = keypath - - if not found: - print("Warning: no keys found for identity {0} in:".format(identity), - file=sys.stderr, - loglevel=LOG_WARN) - print(directories, file=sys.stderr, loglevel=LOG_WARN) - else: - index = 0 - for keyname in found: - index += 1 - print("Found key pair #{0} {1}: {2}{3}".format(index, keyname, found[keyname], " MISSING PUB" if not 'pub' in found[keyname] else " MISSING PRIV" if not 'priv' in found[keyname] else ""), file=sys.stderr, loglevel=LOG_DEBUG) - - return found - - -def FindSSHConfig(identity, config): - """Finds a config file if there's one associated with an identity - - Args: - identity: string, name of the identity to load strings of. - config: object implementing the Config interface, providing configurations - for the user. - - Returns: - string, the configuration file to use - """ - directories = [os.path.join(config.Get("DIR_IDENTITIES"), identity)] - - pattern = re.compile(config.Get("PATTERN_CONFIG")) - sshconfigs = collections.defaultdict(dict) - for directory in directories: - try: - sshconfigs = os.listdir(directory) - except OSError as e: - if e.errno == errno.ENOENT: - continue - raise - - for sshconfig in sshconfigs: - sshconfig = os.path.join(directory, sshconfig) - if os.path.isfile(sshconfig) and pattern.search(sshconfig): - return sshconfig - - return False - - -def GetSessionTty(): - """Returns a file descriptor for the session TTY, or None. - - In *nix systems, each process is tied to one session. Each - session can be tied (or not) to a terminal, "/dev/tty". - - Additionally, when a command is run, its stdin or stdout can - be any file descriptor, including one that represent a tty. - - So for example: - - ./test.sh < /dev/null > /dev/null - - will have stdin and stdout tied to /dev/null - but does not - tell us anything about the session having a /dev/tty associated - or not. - - For example, running - - ssh -t user@remotehost './test.sh < /dev/null > /dev/null' - - have a tty associated, while the same command without -t will not. - - When ssh is invoked by tools like git or rsyn, its stdin and stdout - is often tied to a file descriptor which is not a terminal, has - the tool wants to provide the input and process the output. - - ssh-ident internally has to invoke ssh-add, which needs to know if - it has any terminal it can use at all. - - This function returns an open file if the session has an usable terminal, - None otherwise. - """ - try: - fd = open("/dev/tty", "r") - fcntl.ioctl(fd, termios.TIOCGPGRP, " ") - except IOError: - return None - return fd - - -class AgentManager(object): - """Manages the ssh-agent for one identity.""" - - def __init__(self, identity, sshconfig, config): - """Initializes an AgentManager object. - - Args: - identity: string, identity the ssh-agent managed by this instance of - an AgentManager will control. - config: object implementing the Config interface, allows access to - the user configuration parameters. - - Attributes: - identity: same as above. - config: same as above. - agents_path: directory where the config of all agents is kept. - agent_file: the config of the agent corresponding to this identity. - - Parameters: - DIR_AGENTS: used to compute agents_path. - BINARY_SSH: path to the ssh binary. - """ - self.identity = identity - self.config = config - self.ssh_config = sshconfig - self.agents_path = os.path.abspath(config.Get("DIR_AGENTS")) - self.agent_file = self.GetAgentFile(self.agents_path, self.identity) - - def LoadUnloadedKeys(self, keys): - """Loads all the keys specified that are not loaded. - - Args: - keys: dict as returned by FindKeys. - """ - toload = self.FindUnloadedKeys(keys) - if toload: - print("Loading keys:\n {0}".format( "\n ".join(toload)), - file=sys.stderr, loglevel=LOG_INFO) - self.LoadKeyFiles(toload) - else: - print("All keys already loaded", file=sys.stderr, loglevel=LOG_INFO) - - def FindUnloadedKeys(self, keys): - """Determines which keys have not been loaded yet. - - Args: - keys: dict as returned by FindKeys. - - Returns: - iterable of strings, paths to private key files to load. - """ - loaded = set(self.GetLoadedKeys()) - toload = set() - for key, config in keys.items(): - if "pub" not in config: - continue - if "priv" not in config: - continue - - fingerprint = self.GetPublicKeyFingerprint(config["pub"]) - if fingerprint in loaded: - continue - - toload.add(config["priv"]) - return toload - - def LoadKeyFiles(self, keys): - """Load all specified keys. - - Args: - keys: iterable of strings, each string a path to a key to load. - """ - keys = " ".join(keys) - options = self.config.Get("SSH_ADD_OPTIONS").get( - self.identity, self.config.Get("SSH_ADD_DEFAULT_OPTIONS")) - console = GetSessionTty() - self.RunShellCommandInAgent( - self.agent_file, "ssh-add {0} {1}".format(options, keys), - stdout=console, stdin=console) - - def GetLoadedKeys(self): - """Returns an iterable of strings, each the fingerprint of a loaded key.""" - retval, stdout = self.RunShellCommandInAgent(self.agent_file, "ssh-add -l") - if retval != 0: - return [] - - fingerprints = [] - for line in stdout.decode("utf-8").split("\n"): - try: - _, fingerprint, _ = line.split(" ", 2) - fingerprints.append(fingerprint) - except ValueError: - continue - return fingerprints - - @staticmethod - def GetPublicKeyFingerprint(key): - """Returns the fingerprint of a public key as a string.""" - retval, stdout = AgentManager.RunShellCommand( - "ssh-keygen -l -f {0} |tr -s ' '".format(key)) - if retval: - return None - - try: - _, fingerprint, _ = stdout.decode("utf-8").split(" ", 2) - except ValueError: - return None - return fingerprint - - @staticmethod - def GetAgentFile(path, identity): - """Returns the path to an agent config file. - - Args: - path: string, the path where agent config files are kept. - identity: string, identity for which to load the agent. - - Returns: - string, path to the agent file. - """ - # Create the paths, if they do not exist yet. - try: - os.makedirs(path, 0o700) - except OSError as e: - if e.errno != errno.EEXIST: - raise OSError( - "Cannot create agents directory, try manually with " - "'mkdir -p {0}'".format(path)) - - # Use the hostname as part of the path just in case this is on NFS. - agentfile = os.path.join( - path, "agent-{0}-{1}".format(identity, socket.gethostname())) - if os.access(agentfile, os.R_OK) and AgentManager.IsAgentFileValid(agentfile): - print("Agent for identity {0} ready".format(identity), file=sys.stderr, - loglevel=LOG_DEBUG) - return agentfile - - print("Preparing new agent for identity {0}".format(identity), file=sys.stderr, - loglevel=LOG_DEBUG) - retval = subprocess.call( - ["/usr/bin/env", "-i", "/bin/sh", "-c", "ssh-agent > {0}".format(agentfile)]) - return agentfile - - @staticmethod - def IsAgentFileValid(agentfile): - """Returns true if the specified agentfile refers to a running agent.""" - retval, output = AgentManager.RunShellCommandInAgent( - agentfile, "ssh-add -l >/dev/null 2>/dev/null") - if retval & 0xff not in [0, 1]: - print("Agent in {0} not running".format(agentfile), file=sys.stderr, - loglevel=LOG_DEBUG) - return False - return True - - @staticmethod - def RunShellCommand(command): - """Runs a shell command, returns (status, stdout), (int, string).""" - command = ["/bin/sh", "-c", command] - process = subprocess.Popen(command, stdout=subprocess.PIPE) - stdout, stderr = process.communicate() - return process.wait(), stdout - - @staticmethod - def RunShellCommandInAgent(agentfile, command, stdin=None, stdout=subprocess.PIPE): - """Runs a shell command with an agent configured in the environment.""" - command = ["/bin/sh", "-c", - ". {0} >/dev/null 2>/dev/null; {1}".format(agentfile, command)] - process = subprocess.Popen(command, stdin=stdin, stdout=stdout) - stdout, stderr = process.communicate() - return process.wait(), stdout - - @staticmethod - def EscapeShellArguments(argv): - """Escapes all arguments to the shell, returns a string.""" - escaped = [] - for arg in argv: - escaped.append("'{0}'".format(arg.replace("'", "'\"'\"'"))) - return " ".join(escaped) - - def GetShellArgs(self): - """Returns the flags to be passed to the shell to run a command.""" - shell_args = "-c" - if ShouldPrint(self.config, LOG_DEBUG): - shell_args = "-xc" - return shell_args - - def RunSSH(self, argv): - """Execs ssh with the specified arguments.""" - additional_flags = self.config.Get("SSH_OPTIONS").get( - self.identity, self.config.Get("SSH_DEFAULT_OPTIONS")) - if (self.ssh_config): - additional_flags += " -F {0}".format(self.ssh_config) - - command = [ - "/bin/sh", self.GetShellArgs(), - ". {0} >/dev/null 2>/dev/null; exec {1} {2} {3}".format( - self.agent_file, self.config.Get("BINARY_SSH"), - additional_flags, self.EscapeShellArguments(argv))] - os.execv("/bin/sh", command) - -def AutodetectBinary(argv, config): - """Detects the correct binary to run and sets BINARY_SSH accordingly, - if it is not already set.""" - # If BINARY_SSH is set by the user, respect that and do nothing. - if config.Get("BINARY_SSH"): - print("Will run '{0}' as ssh binary - set by user via BINARY_SSH" - .format(config.Get("BINARY_SSH")), loglevel=LOG_DEBUG) - return - - # If BINARY_DIR is set, look for the binary in this directory. - runtime_name = argv[0] - if config.Get("BINARY_DIR"): - binary_name = os.path.basename(runtime_name) - binary_path = os.path.join(config.Get("BINARY_DIR"), binary_name) - if not os.path.isfile(binary_path) or not os.access(binary_path, os.X_OK): - binary_path = os.path.join(config.Get("BINARY_DIR"), "ssh") - - config.Set("BINARY_SSH", binary_path) - print("Will run '{0}' as ssh binary - detected based on BINARY_DIR" - .format(config.Get("BINARY_SSH")), loglevel=LOG_DEBUG) - return - - # argv[0] could be pretty much anything the caller decides to set - # it to: an absolute path, a relative path (common in older systems), - # or even something entirely unrelated. - # - # Similar is true for __file__, which might even represent a location - # that is entirely unrelated to how ssh-ident was found. - # - # Consider also that there might be symlinks / hard links involved. - # - # The logic here is pretty straightforward: - # - Try to eliminate the path of ssh-ident from PATH. - # - Search for a binary with the same name of ssh-ident to run. - # - # If this fails, we may end up in some sort of loop, where ssh-ident - # tries to run itself. This should normally be detected later on, - # where the code checks for the next binary to run. - # - # Note also that users may not be relying on having ssh-ident in the - # PATH at all - for example, with "rsync -e '/path/to/ssh-ident' ..." - binary_name = os.path.basename(runtime_name) - ssh_ident_path = "" - if not os.path.dirname(runtime_name): - message = textwrap.dedent("""\ - argv[0] ("{0}") is a relative path. This means that ssh-ident does - not know its own directory, and can't exclude it from searching it - in $PATH: - - PATH="{1}" - - This may result in a loop, with 'ssh-ident' trying to run itself. - It is recommended that you set BINARY_SSH, BINARY_DIR, or run - ssh-ident differently to prevent this problem.""") - print(message.format(runtime_name, os.environ['PATH']), - loglevel=LOG_INFO) - else: - ssh_ident_path = os.path.abspath(os.path.dirname(runtime_name)) - - # Remove the path containing the ssh-ident symlink (or whatever) from - # the search path, so we do not cause an infinite loop. - # Note that: - # - paths in PATH may be not-normalized, example: "/usr/bin/../foo", - # or "/opt/scripts///". Normalize them before comparison. - # - paths in PATH may be repeated multiple times. We have to exclude - # all instances of the ssh-ident path. - normalized_path = [ - os.path.normpath(p) for p in os.environ['PATH'].split(os.pathsep)] - search_path = os.pathsep.join([ - p for p in normalized_path if p != ssh_ident_path]) - - # Find an executable with the desired name. - binary_path = distutils.spawn.find_executable(binary_name, search_path) - if not binary_path: - # Nothing found. Try to find something named 'ssh'. - binary_path = distutils.spawn.find_executable('ssh') - - if binary_path: - config.Set("BINARY_SSH", binary_path) - print("Will run '{0}' as ssh binary - detected from argv[0] and $PATH" - .format(config.Get("BINARY_SSH")), loglevel=LOG_DEBUG) - else: - message = textwrap.dedent("""\ - ssh-ident was invoked in place of the binary {0} (determined from argv[0]). - Neither this binary nor 'ssh' could be found in $PATH. - - PATH="{1}" - - You need to adjust your setup for ssh-ident to work: consider setting - BINARY_SSH or BINARY_DIR in your config, or running ssh-ident some - other way.""") - print(message.format(argv[0], os.environ['PATH']), loglevel=LOG_ERROR) - sys.exit(255) - -def ParseCommandLine(argv, config): - """Parses the command line parameters in argv - and modifies config accordingly.""" - # This function may need a lot of refactoring if it is ever used for more - # than checking for BatchMode for OpenSSH... - binary = os.path.basename(config.Get("BINARY_SSH")) - if binary == 'ssh' or binary == 'scp': - # OpenSSH accepts -o Options as well as -oOption, - # so let's convert argv to the latter form first - i = iter(argv) - argv = [p+next(i, '') if p == '-o' else p for p in i] - # OpenSSH accepts 'Option=yes' and 'Option yes', 'true' instead of 'yes' - # and treats everything case-insensitive - # if an option is given multiple times, - # OpenSSH considers the first occurrence only - re_batchmode = re.compile(r"-oBatchMode[= ](yes|true)", re.IGNORECASE) - re_nobatchmode = re.compile(r"-oBatchMode[= ](no|false)", re.IGNORECASE) - for p in argv: - if re.match(re_batchmode, p): - config.Set("SSH_BATCH_MODE", True) - break - elif re.match(re_nobatchmode, p): - config.Set("SSH_BATCH_MODE", False) - break - -def main(argv): - # Replace stdout and stderr with /dev/tty, so we don't mess up with scripts - # that use ssh in case we error out or similar. - try: - sys.stdout = open("/dev/tty", "w") - sys.stderr = open("/dev/tty", "w") - except IOError: - pass - - config = Config().Load() - # overwrite python's print function with the wrapper SshIdentPrint - global print - print = SshIdentPrint(config) - - AutodetectBinary(argv, config) - # Check that BINARY_SSH is not ssh-ident. - # This can happen if the user sets a binary name only (e.g. 'scp') and a - # symlink with the same name was set up. - # Note that this relies on argv[0] being set sensibly by the caller, - # which is not always the case. argv[0] may also just have the binary - # name if found in a path. - binary_path = os.path.realpath( - distutils.spawn.find_executable(config.Get("BINARY_SSH"))) - ssh_ident_path = os.path.realpath( - distutils.spawn.find_executable(argv[0])) - if binary_path == ssh_ident_path: - message = textwrap.dedent("""\ - ssh-ident found '{0}' as the next command to run. - Based on argv[0] ({1}), it seems like this will create a - loop. - - Please use BINARY_SSH, BINARY_DIR, or change the way - ssh-ident is invoked (eg, a different argv[0]) to make - it work correctly.""") - print(message.format(config.Get("BINARY_SSH"), argv[0]), loglevel=LOG_ERROR) - sys.exit(255) - ParseCommandLine(argv, config) - identity = FindIdentity(argv, config) - keys = FindKeys(identity, config) - sshconfig = FindSSHConfig(identity, config) - agent = AgentManager(identity, sshconfig, config) - - if not config.Get("SSH_BATCH_MODE"): - # do not load keys in BatchMode - agent.LoadUnloadedKeys(keys) - return agent.RunSSH(argv[1:]) - -if __name__ == "__main__": - try: - sys.exit(main(sys.argv)) - except KeyboardInterrupt: - print("Goodbye", file=sys.stderr, loglevel=LOG_DEBUG)