├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTORS.md ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── SSHKeyDistribut0r ├── __init__.py ├── command_line.py └── key_distribut0r.py ├── config ├── keys.sample.yml └── servers.sample.yml ├── requirements.txt └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.toptal.com/developers/gitignore/api/python,pycharm+all,vscode,visualstudiocode 3 | # Edit at https://www.toptal.com/developers/gitignore?templates=python,pycharm+all,vscode,visualstudiocode 4 | 5 | ### PyCharm+all ### 6 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 7 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 8 | 9 | # User-specific stuff 10 | .idea/**/workspace.xml 11 | .idea/**/tasks.xml 12 | .idea/**/usage.statistics.xml 13 | .idea/**/dictionaries 14 | .idea/**/shelf 15 | 16 | # Generated files 17 | .idea/**/contentModel.xml 18 | 19 | # Sensitive or high-churn files 20 | .idea/**/dataSources/ 21 | .idea/**/dataSources.ids 22 | .idea/**/dataSources.local.xml 23 | .idea/**/sqlDataSources.xml 24 | .idea/**/dynamic.xml 25 | .idea/**/uiDesigner.xml 26 | .idea/**/dbnavigator.xml 27 | 28 | # Gradle 29 | .idea/**/gradle.xml 30 | .idea/**/libraries 31 | 32 | # Gradle and Maven with auto-import 33 | # When using Gradle or Maven with auto-import, you should exclude module files, 34 | # since they will be recreated, and may cause churn. Uncomment if using 35 | # auto-import. 36 | # .idea/artifacts 37 | # .idea/compiler.xml 38 | # .idea/jarRepositories.xml 39 | # .idea/modules.xml 40 | # .idea/*.iml 41 | # .idea/modules 42 | # *.iml 43 | # *.ipr 44 | 45 | # CMake 46 | cmake-build-*/ 47 | 48 | # Mongo Explorer plugin 49 | .idea/**/mongoSettings.xml 50 | 51 | # File-based project format 52 | *.iws 53 | 54 | # IntelliJ 55 | out/ 56 | 57 | # mpeltonen/sbt-idea plugin 58 | .idea_modules/ 59 | 60 | # JIRA plugin 61 | atlassian-ide-plugin.xml 62 | 63 | # Cursive Clojure plugin 64 | .idea/replstate.xml 65 | 66 | # Crashlytics plugin (for Android Studio and IntelliJ) 67 | com_crashlytics_export_strings.xml 68 | crashlytics.properties 69 | crashlytics-build.properties 70 | fabric.properties 71 | 72 | # Editor-based Rest Client 73 | .idea/httpRequests 74 | 75 | # Android studio 3.1+ serialized cache file 76 | .idea/caches/build_file_checksums.ser 77 | 78 | ### PyCharm+all Patch ### 79 | # Ignores the whole .idea folder and all .iml files 80 | # See https://github.com/joeblau/gitignore.io/issues/186 and https://github.com/joeblau/gitignore.io/issues/360 81 | 82 | .idea/ 83 | 84 | # Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-249601023 85 | 86 | *.iml 87 | modules.xml 88 | .idea/misc.xml 89 | *.ipr 90 | 91 | # Sonarlint plugin 92 | .idea/sonarlint 93 | 94 | ### Python ### 95 | # Byte-compiled / optimized / DLL files 96 | __pycache__/ 97 | *.py[cod] 98 | *$py.class 99 | 100 | # C extensions 101 | *.so 102 | 103 | # Distribution / packaging 104 | .Python 105 | build/ 106 | develop-eggs/ 107 | dist/ 108 | downloads/ 109 | eggs/ 110 | .eggs/ 111 | lib/ 112 | lib64/ 113 | parts/ 114 | sdist/ 115 | var/ 116 | wheels/ 117 | pip-wheel-metadata/ 118 | share/python-wheels/ 119 | *.egg-info/ 120 | .installed.cfg 121 | *.egg 122 | MANIFEST 123 | 124 | # PyInstaller 125 | # Usually these files are written by a python script from a template 126 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 127 | *.manifest 128 | *.spec 129 | 130 | # Installer logs 131 | pip-log.txt 132 | pip-delete-this-directory.txt 133 | 134 | # Unit test / coverage reports 135 | htmlcov/ 136 | .tox/ 137 | .nox/ 138 | .coverage 139 | .coverage.* 140 | .cache 141 | nosetests.xml 142 | coverage.xml 143 | *.cover 144 | *.py,cover 145 | .hypothesis/ 146 | .pytest_cache/ 147 | pytestdebug.log 148 | 149 | # Translations 150 | *.mo 151 | *.pot 152 | 153 | # Django stuff: 154 | *.log 155 | local_settings.py 156 | db.sqlite3 157 | db.sqlite3-journal 158 | 159 | # Flask stuff: 160 | instance/ 161 | .webassets-cache 162 | 163 | # Scrapy stuff: 164 | .scrapy 165 | 166 | # Sphinx documentation 167 | docs/_build/ 168 | doc/_build/ 169 | 170 | # PyBuilder 171 | target/ 172 | 173 | # Jupyter Notebook 174 | .ipynb_checkpoints 175 | 176 | # IPython 177 | profile_default/ 178 | ipython_config.py 179 | 180 | # pyenv 181 | .python-version 182 | 183 | # pipenv 184 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 185 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 186 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 187 | # install all needed dependencies. 188 | #Pipfile.lock 189 | 190 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 191 | __pypackages__/ 192 | 193 | # Celery stuff 194 | celerybeat-schedule 195 | celerybeat.pid 196 | 197 | # SageMath parsed files 198 | *.sage.py 199 | 200 | # Environments 201 | .env 202 | .venv 203 | env/ 204 | venv/ 205 | ENV/ 206 | env.bak/ 207 | venv.bak/ 208 | pythonenv* 209 | 210 | # Spyder project settings 211 | .spyderproject 212 | .spyproject 213 | 214 | # Rope project settings 215 | .ropeproject 216 | 217 | # mkdocs documentation 218 | /site 219 | 220 | # mypy 221 | .mypy_cache/ 222 | .dmypy.json 223 | dmypy.json 224 | 225 | # Pyre type checker 226 | .pyre/ 227 | 228 | # pytype static type analyzer 229 | .pytype/ 230 | 231 | # profiling data 232 | .prof 233 | 234 | ### VisualStudioCode ### 235 | .vscode/* 236 | #!.vscode/tasks.json 237 | #!.vscode/launch.json 238 | *.code-workspace 239 | 240 | ### VisualStudioCode Patch ### 241 | # Ignore all local history of files 242 | .history 243 | .ionide 244 | 245 | ### vscode ### 246 | #!.vscode/settings.json 247 | #!.vscode/extensions.json 248 | 249 | # End of https://www.toptal.com/developers/gitignore/api/python,pycharm+all,vscode,visualstudiocode 250 | 251 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.6" 4 | install: 5 | - pip install -r requirements.txt 6 | deploy: 7 | provider: pypi 8 | user: thomai 9 | password: 10 | secure: o6iKbSPtr0WVIYw/d5B+mlQeWQtFPUWcFgQcOpo5WUUSEz+UbnT2v+GKf+Y22W62IfT5h9NXUCmw4xUGLiQbJC/2VqWFsygd/qaEtAogbpzCAzzBegb4vDd9sVVtpu4NgElAwX6pdXjH3fXSnlMugagJyOxZSrXjZSKsNk52M+plrN3CQzHXU2BuRW6uerRr/ODv8iV/+vY5Otna96unu0/V14NmOlfEUcYoQwRW9nqnUGdIx1EcN+9AZ0jNTy2oPiXUfGjc83HF2QI599GySTDcUjKp/uSPXUt60k1FGBqY5YW7OINXolqqfY7g1FbK04bEwWXh+a8YddmofU0No3XpmRwfWdmsAAuAIYwMDaJ6id7lKKtW3CtLQ8CuT+Jbik+Gc6iRs5cyFp+yWu9Og6d9fUJ+V5Dbahy+M7nNstTb5HyTImhfaDQ3QkHcBv/ldv5l/fNwG6iVmpEAun/1ZC+y2pCaTZmTf4y0vR9tWsIhHkuBKzQq8wgz8Ka1I4HS1CHt0Pp2jZh7j/+KVyYguj3zoIfP3kdICxy4LoElkiD+ESFbtPc9V6xRQXDjsSG5m0c2q0bdg0fd14elTkZ98T8VYcBFr8f5JJ2geGhzF2j3vEScx1+tDhETy9X0XzerDlDoKffy0Aw43XR6KXTGfY69WOrKh92P1tO0Iekvi3Q= 11 | on: 12 | branch: master 13 | tags: true 14 | script: echo 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2.5 (2021-02-05) 4 | New version number required because of PyPI upload issues 5 | 6 | ## 0.2.4 (2021-02-04) 7 | - Error if users in key and server configuration do not match 8 | - Updated README 9 | 10 | ## 0.2.3 11 | - Clarified exception messages 12 | - Fixed Python 2.7 issues 13 | - Updated requirements 14 | 15 | ## 0.2.2 16 | Fixed version issues 17 | 18 | ## 0.2.1 19 | Fixed version issues 20 | 21 | ## 0.2.0 22 | - Port to Python 3 23 | - Replaced temporary files with in-memory streams 24 | 25 | ## 0.1.2 26 | - MANIFEST added to resolve PyPI 27 | - Updated .gitignore 28 | 29 | ## 0.1.1 30 | Travis CI integration 31 | 32 | ## 0.1.0 (2018-10-13) 33 | - Dry run option 34 | - PyPI packaging 35 | - Cleaner config load 36 | 37 | ## 0.0.2 (2016-11-02) 38 | Deprecated json configuration files 39 | 40 | ## 0.0.1 (2016-04-07) 41 | Initial release 42 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | # SSHKeyDistribut0r Contributors 2 | * **[Armin Grodon](https://github.com/x4121)** 3 | * **[Ikai Lan](https://github.com/ikai)** 4 | * **[Christian Affolter](https://github.com/paraenggu)** 5 | * **[melhir](https://github.com/melhir)** 6 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | This work is licensed under the Creative Commons Attribution 4.0 2 | International License. To view a copy of this license, visit 3 | http://creativecommons.org/licenses/by/4.0/. 4 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include *.txt 2 | recursive-include config *.txt *.py 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CC BY](http://mirrors.creativecommons.org/presskit/buttons/80x15/svg/by.svg)](http://creativecommons.org/licenses/by/4.0/) 2 | [![Build Status](https://travis-ci.org/thomai/SSHKeyDistribut0r.svg?branch=master)](https://travis-ci.org/thomai/SSHKeyDistribut0r) 3 | 4 | SSHKeyDistribut0r has been written to make SSH key distribution easier 5 | for sysop teams. 6 | 7 | ![Screenshot](http://i.imgur.com/qoKm9dl.png) 8 | 9 | # How to use 10 | ## Install 11 | ``` 12 | pip install SSHKeyDistribut0r 13 | ``` 14 | 15 | ## Create configuration files 16 | First, copy the YAML sample files to your users config directory and customize them. 17 | 18 | The sample files should be in 19 | `$HOME/.local/share/SSHKeyDistribut0r/config_sample`, 20 | `/usr/local/share/SSHKeyDistribut0r/config_sample` or 21 | `/usr/share/SSHKeyDistribut0r/config_sample` 22 | 23 | The config files need to be copied to `$USER_CONFIG_DIR/SSHKeyDistribut0r/` 24 | (`$HOME/.config/...` on most Linux systems, check `SSHKeyDistribut0r -h` for 25 | the location on your system) 26 | 27 | The keys.yml file has to contain all users which are used in the 28 | servers.yml file. Every entry in the YML structure requires the 29 | following attributes: 30 | The `fullname` is a string value to mention the full name of a person. 31 | `keys` is a list of SSH keys in the format `ssh-rsa `. 32 | 33 | The servers.yml file contains all servers with the specified user 34 | permissions. It consists of a list of dictionaries with the following 35 | attributes: 36 | * `ip`: String value in the format `XXX.XXX.XXX.XXX` 37 | * `port`: Integer value which specifies the SSH port 38 | * `user`: String value which specifies the system user to log in. 39 | * `comment`: String value to describe the system 40 | * `authorized_users`: List of strings which specify a user. Every user 41 | has to be declared in the keys.yml file as a key. 42 | 43 | ## Usage 44 | Run `SSHKeyDistribut0r` to distribute your SSH keys :) 45 | 46 | Note, that a pre-installed public key on the server side with a corresponding 47 | private key (without a passphrase or pre-loaded into a local key agent) is 48 | required for SSHKeyDistribut0r to work. 49 | 50 | ### Options 51 | * `--dry-run`/`-n`: To verify your configuration whithout actually applying those changes. 52 | * `--keys`/`-k`: Custom path to keys file 53 | * `--server`/`-s`: Custom path to server file 54 | 55 | # New Release 56 | ## Build Python Package 57 | * Update `CHANGELOG.md` 58 | * Update version in `setup.py` 59 | * Update `CONTRIBUTORS.md` 60 | * Install package builder: `pip install build` 61 | * Build package: `python -m build` 62 | * Installation: `pip install ./dist/SSHKeyDistribut0r-.tar.gz` 63 | * Do tests 64 | 65 | ## Upload to PyPI 66 | * Install twine: `pip install twine` 67 | * Upload: `twine upload ./dist/SSHKeyDistribut0r-.tar.gz` 68 | 69 | ## Git Stuff 70 | * `git add .` 71 | * Commit new version: `git commit -m "Version x.x.x"` 72 | * `git push` 73 | * `git checkout master` 74 | * Update master branch: `git merge develop` 75 | * `git push` 76 | * Set version tag: `git tag x.x.x` 77 | * `git push --tags` 78 | 79 | -------------------------------------------------------------------------------- /SSHKeyDistribut0r/__init__.py: -------------------------------------------------------------------------------- 1 | from .key_distribut0r import main 2 | -------------------------------------------------------------------------------- /SSHKeyDistribut0r/command_line.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function 4 | import SSHKeyDistribut0r 5 | 6 | import appdirs 7 | import argparse 8 | import sys 9 | 10 | 11 | def main(): 12 | prog = 'SSHKeyDistribut0r' 13 | print() 14 | print(prog) 15 | print('=================') 16 | print('Welcome to the world of key distribution!') 17 | print() 18 | 19 | parser = argparse.ArgumentParser( 20 | description='A tool to automate key distribution with user authorization.') 21 | parser.add_argument('--dry-run', '-n', action='store_true', 22 | help='show pending changes without applying them') 23 | parser.add_argument('--keys', '-k', 24 | default='%s/%s/keys.yml' % (appdirs.user_config_dir(), prog), 25 | help="path to keys file\n(default: '%(default)s')") 26 | parser.add_argument('--server', '-s', 27 | default='%s/%s/servers.yml' % (appdirs.user_config_dir(), prog), 28 | help="path to server file (default: '%(default)s')") 29 | args = parser.parse_args() 30 | 31 | try: 32 | SSHKeyDistribut0r.main(args) 33 | except KeyboardInterrupt: 34 | sys.exit(1) 35 | -------------------------------------------------------------------------------- /SSHKeyDistribut0r/key_distribut0r.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import print_function, unicode_literals 4 | import io 5 | import json 6 | import logging 7 | import os 8 | import re 9 | import socket 10 | import sys 11 | import yaml 12 | 13 | import paramiko 14 | import scp 15 | 16 | logging.raiseExceptions = False 17 | 18 | TIMEOUT_ON_CONNECT = 2 # in seconds 19 | 20 | # Colors for console outputs 21 | COLOR_RED = '\033[91m' 22 | COLOR_GREEN = '\033[92m' 23 | COLOR_END = '\033[0m' 24 | 25 | # File extension regex 26 | YAML_EXT = re.compile("^\\.ya?ml$") 27 | JSON_EXT = re.compile("^\\.json$") 28 | 29 | 30 | def remove_special_chars(original_string): 31 | return ''.join(e for e in original_string if e.isalnum()) 32 | 33 | 34 | def error_log(message): 35 | print(u'%s✗ Error: %s%s' % (COLOR_RED, message, COLOR_END)) 36 | 37 | 38 | def server_error_log(ip, comment, message): 39 | error_log('%s/%s - %s' % (ip, comment, message)) 40 | 41 | 42 | def info_log(message): 43 | print(u'%s✓ %s%s' % (COLOR_GREEN, message, COLOR_END)) 44 | 45 | 46 | def server_info_log(ip, comment, users): 47 | info_log('%s/%s - %s' % (ip, comment, users)) 48 | 49 | 50 | def read_config(config_file): 51 | ext = os.path.splitext(config_file)[-1] 52 | try: 53 | if YAML_EXT.match(ext): 54 | return yaml.load(open(config_file)) 55 | elif JSON_EXT.match(ext): 56 | return json.load(open(config_file)) 57 | else: 58 | error_log("Configuration file extension '%s' not supported." \ 59 | " Please use .json or .yml." % ext) 60 | sys.exit(1) 61 | except (ValueError, yaml.scanner.ScannerError): 62 | error_log('Cannot parse malformed configuration file.') 63 | sys.exit(1) 64 | 65 | 66 | def main(args): 67 | # Load config files 68 | servers = read_config(args.server) 69 | keys = read_config(args.keys) 70 | 71 | for server in servers: 72 | if server['authorized_users']: 73 | # Generate key file for this server 74 | # key_stream = io.BytesIO() 75 | key_stream = io.StringIO() 76 | server_users = [] 77 | 78 | # Write all keys of users with permissions for this server 79 | for authorized_user in server['authorized_users']: 80 | # user_name = '%s (%s)' % (keys[authorized_user]['fullname'], authorized_user) 81 | user_name = authorized_user 82 | server_users.append(user_name) 83 | if authorized_user in list(keys.keys()): 84 | for key in keys[authorized_user]['keys']: 85 | key_stream.write('%s\n' % key) 86 | else: 87 | error_log("Cannot find user '%s' in the key configuration file" \ 88 | " for '%s/%s'." % (authorized_user, server['ip'], server['comment'])) 89 | sys.exit(1) 90 | 91 | if args.dry_run: 92 | server_info_log(server['ip'], server['comment'], ', '.join(server_users)) 93 | else: 94 | # Configure SSH client 95 | ssh_client = paramiko.SSHClient() 96 | ssh_client.load_system_host_keys() # Load host keys to check whether they are matching 97 | ssh_client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) # Add missing host keys automatically 98 | try: 99 | # Establish connection 100 | ssh_client.connect(server['ip'], port=server['port'], username=server['user'], 101 | timeout=TIMEOUT_ON_CONNECT) 102 | 103 | # Upload key file 104 | with scp.SCPClient(ssh_client.get_transport()) as scp_client: 105 | key_stream.seek(0) 106 | scp_client.putfo(key_stream, '.ssh/authorized_keys') 107 | 108 | key_stream.close() 109 | server_info_log(server['ip'], server['comment'], ', '.join(server_users)) 110 | 111 | except paramiko.ssh_exception.PasswordRequiredException: 112 | server_error_log( 113 | server['ip'], 114 | server['comment'], 115 | 'The private key file is protected by a passphrase, which is currently not supported.' 116 | ) 117 | except paramiko.ssh_exception.AuthenticationException: 118 | server_error_log( 119 | server['ip'], 120 | server['comment'], 121 | 'Cannot connect to server because of an authentication problem.' 122 | ) 123 | except scp.SCPException: 124 | server_error_log(server['ip'], server['comment'], 'Cannot send file to server.') 125 | except (paramiko.ssh_exception.NoValidConnectionsError, paramiko.ssh_exception.SSHException): 126 | server_error_log(server['ip'], server['comment'], 'Cannot connect to server.') 127 | except socket.timeout: 128 | server_error_log(server['ip'], server['comment'], 'Cannot connect to server because of a timeout.') 129 | else: 130 | server_error_log(server['ip'], server['comment'], 'No user mentioned in configuration file!') 131 | -------------------------------------------------------------------------------- /config/keys.sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | monica: 3 | fullname: Monica Kirby 4 | keys: 5 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDibQUFkXrG5VR+QS2eLCsVHsaWebZ4DFCphzjT7lKv8ZB8M+zQsWROPC6wTcJwrxsr6BXIybnbvgR4l6YczKAAHcF3/qOZiKbm3ddd2GUgqmH9NJKoBatif1SXgzH5cs1D5ELEHJvTdRow1YXlxrLoO2rB1IyALdD/Id2inHezf53QvGlL6xz/l5tutW0zIHkiBwz50JBEC8drgIWJGYyxhOgQbJ78oSNY7tI8EuabiZ/iYcBwICdsguoCp8dqVjcbMP88QhS8f5b2fKZ7VzRuJI/NwjtSKdlNQH3jVgU9kNEVwIJrB2BABAq3yPCjzVLDszA1vFQUt8tuDCiCxXTYNv5Jc7stSkO71i+pvVikC3TpN3GOKqgiZRP+3hI48HaRK8rITRiH4uTr/ppQ0l2XSY8X05Wf+QztE/zYbUNNOf2wpH4IMp82VBg6fgDrjcpj/EgQ1j/6Izb63s9TO9yblnde/mwQLduniIYOml8qZwDuIPiln8hEo5edQj1dyvh9VVRIdDoiaPHH2Weq01Kf/Xn4lkahfrz3v+3SLxjzf4hdtq4DrxAzZWa2QGZCiQlMGmWcjaoCDAnCkEk2+d96cUxhA9z2N70QOhuOvC/nZV0Wq/Lx5bAqw4e+hcjuX+Kl5wA3N9fn9DdzzNwY/q7E5rc/ePN9gkr0fVRmeJWejw== monica@acme.org 6 | andy: 7 | fullname: Andrew Otoole 8 | keys: 9 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQCw3mZzauZ/0boKP2slhvyq+8fJhYTpErZtDSo/lE8YLsJuTrXSn8s0yVBadqOlBVVtXKiqciykUU2deubyt8mUfu6j9haNFOJ8rHWywCjl+ie1im3L7C1X2SzOXPEiLopYPkz/jblLSXh40+8M4O8aEcQaLfqy8uJiOV84mdiLd4DY5UfgKrMAzooUVGi7xgaRoqA/OD2bfiL4vURM2kHu+JX+f/wk1yF8vH/ec+1th3ebXjQwwTtKXGQQSu2mwmc60J5Blcues4/Ecbzys4YxaCVMIDBEdA639l9KiJ7naXg3+i2UcdGKkVwhzSjw6ZLp/1/BcCrQBvPn6JbYZ0qiLhyTIwj4igv3ikjEq6xgMWrEU6cnBkApCzFqgxfeAHBw9IO3flTkeGRtRnxwCjxVBSTodp8WiwUlB9RCexXKnhDuXopXHaYV7XciMtt1hAqrcCtIopChvI3U8yTM3Itr3tSuCqnkuxUIvuODSuDSLdGKuIRFhdNk9CxnR7vS1Wnna85ASa3v9c9gEad3WzK9BAAxuG6wNB5Wn1GtB+doFF0rD96HR8gx1p+KZYZPNrps7Y7ewJpan3ZeVreRVf6lFrRzBNz3s4swvVg918pbPnTVQfq2D1FL24/nBSoTY+aWJ/fo0aDgvHDikF4Hw4HEQcpcQ3JZ515hJUuocu0Q5Q== andrew@acme.org 10 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDk6AoYc7kAJ5qw3UZs9uGYTW9IK1HDVkGgtD2P//Uicczv/eZoB2NjPoXaE4CUNTxwUWubteEenxMErOyifClkG2zrtrdkwh/83vPlCTHahCpBI+lhIzYgVARMaV9TDn4AaspkIvZQldwHLkc5YlkyrpU7fj7LMmvA/Ut/mCQlqWRW5yqzpfQnjDbKfR2vLRfuPL90bCDwBI8RZb2G2g3T64a7tuOL4+ysO+QCDrQvLSp+zRxtQKyQmYaLau79KcGB8QfqMhpqQF2J4A4EGHJozgqGjLHe6cZzDOOAPepk5jA0x/FUIIyY9olZaxFa4ucSZKuzRvuFFIOklXmWhLtPo7Tl3uDyCyeOPdpLbT871jQuueV2MUN8lX5WF4jX8Ep/OZ7YQp5ZB3GWT4PvK+0DI+r3FFYb48SHYaD+7prTKtmr2k2fi4+bU3uwzuHpWDM9t6NB6Hp1DXwJJwRvOodahYc4hNqFFOBQGapKAo6xu0mCVPU3oQ19mrVZuli1Zl//f9DMKgRMihBJJCq79/nfXTgDFePwtZz44CxrVfZPODvv+vv4fvxVX2D9lx08FdLz+6EBK4Q6EjnyfTGplCMqh3rpzmAxiB5ESBNBlPnX9L8unQpPyRD9jFTZ8ofuAEyTj9ce/HUiYu+36EU+Y5KuJMTjBzlGHuXsy/89QX3RIQ== andrew@otoole.com 11 | shannon: 12 | fullname: Shannon Tripp 13 | keys: 14 | - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQC0LR0MNJ2sd17kVpIHspRFkXQEPgEiN3HdWenqjPiw7VsIwRsm3mxJLaYSRDIfjRTujFbBwXzShG6XX16cLfrZBeGCxJNgFOjd8YAKo/ApFqqsqtZYwZU/fUnM4iVXIQyV8ghGT4jyeM8psWZVo20DPqdBfOLNYy+l3/w3GmiET0YwfDCU/ZV6+ajoOybALsxQHA32qQBMCKfCLgnOSQ1ygXGqRlQnLEvKTSOnhKy1yB8FO7peatbkasvQPBppTE6eEjzKzZZPKC19Di4ENE+5WJ8R+1UUB+P2OYS4PcIWqMqoqEONxC6zRMFVQnPDxUr7P/RZV2Wi/005tb25+Q3ajiV0GVhdejGyQ8MTZYW+bKUfpHW9+TBVY+zVgBppltWy5Rt0PDESSSldxKt+RF+BD37zvQOlJEaNus3Zs6LCanGclUO664YDUI4zNFD1rTQb0KzZgfiMf/IUkyYB/MspsaPB0DKsWeu73xGqLFxYNbEg2PPYhMdIbdqE7qHzrGqNM0rRf8UT4dREEcBW7Es422/dPrrCM/SsrmHZy+4Nqe7e80Z/0tbCu1jDEefXj54W8UMNagrnrBlb/A/YByO4MvdmQZ/ZNK3tXrutLpQvODAkxS25lqZRo4bEGBpp6PoIU0qyP7Ou0ttkD7rfaMIMwr9s5e9ALClvVbHcxawohw== shannon@acme.org 15 | ... 16 | -------------------------------------------------------------------------------- /config/servers.sample.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - ip: 10.0.0.1 3 | port: 22 4 | user: admin 5 | comment: samba server 6 | authorized_users: [monica] 7 | - ip: 10.0.0.2 8 | port: 22 9 | user: admin 10 | comment: monitoring server 11 | authorized_users: [monica] 12 | - ip: 10.0.0.3 13 | port: 22 14 | user: admin 15 | comment: 1st workstation 16 | authorized_users: [monica, andy, shannon] 17 | - ip: 10.0.0.4 18 | port: 22 19 | user: admin 20 | comment: 2nd workstation 21 | authorized_users: [monica, andy, shannon] 22 | - ip: 10.0.0.5 23 | port: 22 24 | user: admin 25 | comment: file server 26 | authorized_users: [andrew, shannon, monica] 27 | ... 28 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | appdirs~=1.4.4 2 | ecdsa~=0.16.1 3 | paramiko~=2.7.2 4 | pycrypto~=2.6.1 5 | pyyaml~=5.4.1 6 | scp~=0.13.3 7 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | from codecs import open 3 | from os import path 4 | 5 | here = path.abspath(path.dirname(__file__)) 6 | name = 'SSHKeyDistribut0r' 7 | version = '0.2.5' 8 | 9 | with open(path.join(here, 'README.md'), encoding='utf-8') as f: 10 | long_description = f.read() 11 | 12 | 13 | def get_requirements(): 14 | req = [] 15 | for line in open('requirements.txt', 'r'): 16 | req.append(line.split()[0]) 17 | return req 18 | 19 | 20 | setup(name=name, 21 | version=version, 22 | description='A tool which has been written to make SSH key distribution easier for sysop teams.', 23 | long_description=long_description, 24 | long_description_content_type='text/markdown', 25 | url='https://github.com/thomai/SSHKeyDistribut0r', 26 | author='Thomas Maier', 27 | author_email='info@wurps.de', 28 | license='CC BY', 29 | classifiers=['Development Status :: 5 - Production/Stable', 30 | 'Intended Audience :: Developers', 31 | 'Intended Audience :: System Administrators', 32 | 'Programming Language :: Python', 33 | 'Programming Language :: Python :: 2', 34 | 'Programming Language :: Python :: 2.7', 35 | 'Programming Language :: Python :: 3', 36 | 'Programming Language :: Python :: 3.6', 37 | 'Programming Language :: Python :: 3.7', 38 | 'Operating System :: OS Independent', 39 | 'Topic :: Communications :: File Sharing', 40 | 'Topic :: Security', 41 | 'Topic :: Security :: Cryptography', 42 | 'Topic :: System :: Networking', 43 | 'Topic :: System :: Operating System', 44 | 'Topic :: System :: Shells', 45 | 'Topic :: System :: Systems Administration', 46 | 'Topic :: System :: Systems Administration :: Authentication/Directory', 47 | 'Topic :: System :: System Shells', 48 | 'Topic :: Terminals', 49 | 'Topic :: Utilities'], 50 | keywords='ssh key distribution', 51 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 52 | install_requires=get_requirements(), 53 | data_files=[('share/%s/config_sample' % name, 54 | ['config/keys.sample.yml', 'config/servers.sample.yml'])], 55 | entry_points={ 56 | 'console_scripts': [ 57 | '%s = %s.command_line:main' % (name, name), 58 | ], 59 | },) 60 | --------------------------------------------------------------------------------