├── .gitignore ├── README.rst ├── ansible_keepass.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | Ansible Keepass Plugin 2 | ###################### 3 | Use **become** (sudo) in Ansible **without giving any password** and safely. This plugin connects to 4 | **keepassHTTP** or **KeepassXC Browser** to request the password. The connection is encrypted and requires a first 5 | confirmation. The token for Keepass HTTP is stored using the keyring of the system and the passwords are only 6 | accessible while the Keepass database is open. 7 | 8 | Installation 9 | ============ 10 | Clone or copy the plugin included:: 11 | 12 | sudo mkdir -p /usr/local/share/ansible/plugins/vars 13 | sudo curl https://raw.githubusercontent.com/Nekmo/ansible-keepass/master/ansible_keepass.py \ 14 | -o /usr/local/share/ansible/plugins/vars/ansible_keepass.py 15 | 16 | Set the var plugins directory to the same directory that contains ``ansible_keepass.py`` in ``ansible.cfg``:: 17 | 18 | /etc/ansible/ansible.cfg 19 | ------------------------ 20 | 21 | [defaults] 22 | ... 23 | vars_plugins = /usr/local/share/ansible/plugins/vars 24 | 25 | 26 | And install requirements from ``requirements.txt`` file in this project (install the modules in the same environment 27 | as Ansible):: 28 | 29 | sudo pip install -r requirements.txt 30 | 31 | Usage 32 | ===== 33 | This project supports **KeepassHTTP** and **KeepassXC Browser**. This plugin is able to detect your Keepass 34 | program automatically but if you have issues define the environment variable ``KEEPASS_CLASS`` (available values: 35 | ``KeepassXC`` and ``KeepassHTTP``). 36 | 37 | *Ansible-keepass* uses the url of the entry to find the entry to use. In the Keepass entries you must specify the url 38 | using the name of the inventory or inventory group. For example:: 39 | 40 | ssh: 41 | 42 | Or including username:: 43 | 44 | ssh:@ 45 | 46 | That is all! If you do not set a password now Ansible will ask Keepass for the password. You can try this plugin using:: 47 | 48 | $ ansible -a "/usr/bin/hdparm -C /dev/sda" --become 49 | 50 | 51 | Security 52 | ======== 53 | These are the security measures adopted by Keepass and this plugin: 54 | 55 | #. Keepass requests link permission the first time. This plugin stores the session using the OS Keyring. 56 | #. Keepass can authorize access permission for each key individually. 57 | #. Keys can not be listed using Keepass HTTP/XC Browser. A possible attacker must know key urls. 58 | #. The connection between this plugin and Keepass is encrypted. 59 | #. Passwords are not accessible while the Keepass database is closed. 60 | 61 | This plugin can be more secure than copy&paste: a malware installed on your machine can listen for changes 62 | in the clipboard. This plugin depends on the security of the OS Keyring and your personal password. 63 | -------------------------------------------------------------------------------- /ansible_keepass.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | import psutil 4 | 5 | import __main__ 6 | import requests 7 | import keyring 8 | from ansible.plugins.vars import BaseVarsPlugin 9 | from ansible.executor.task_executor import TaskExecutor as _TaskExecutor 10 | from ansible.executor import task_executor 11 | from ansible.executor.process import worker 12 | from ansible.utils.display import Display 13 | from keepasshttplib import keepasshttplib, encrypter 14 | from keepassxc_browser import Identity, Connection 15 | from keepassxc_browser.protocol import ProtocolError 16 | 17 | 18 | KEEPASSXC_CLIENT_ID = 'python-keepassxc-browser' 19 | KEEPASSXC_PROCESS_NAMES = set(('keepassxc', 'keepassxc.exe', 20 | 'keepassxc-proxy')) 21 | KEYRING_KEY = 'assoc' 22 | 23 | 24 | display = Display() 25 | 26 | 27 | class NONE: 28 | pass 29 | 30 | 31 | class AnsibleKeepassError(Exception): 32 | body = 'Error in the Ansible Keepass plugin.' 33 | 34 | def __init__(self, msg=''): 35 | body = self.body 36 | if msg: 37 | body += ' {}'.format(msg) 38 | super().__init__(body) 39 | 40 | 41 | class KeepassConnectionError(AnsibleKeepassError): 42 | body = 'Error on connection.' 43 | 44 | 45 | class KeepassHTTPError(AnsibleKeepassError): 46 | body = ('The password for root could not be obtained using Keepass ' 47 | 'HTTP.') 48 | 49 | 50 | class KeepassXCError(AnsibleKeepassError): 51 | body = ('The password for root could not be obtained using ' 52 | 'KeepassXC Browser.') 53 | 54 | 55 | class KeepassBase(object): 56 | def __init__(self): 57 | self.cached_passwords = {} 58 | 59 | def get_cached_password(self, host): 60 | hosts = get_host_names(host) 61 | for host_name in hosts: 62 | return self._get_cached_password(host_name) 63 | 64 | def _get_cached_password(self, host_name): 65 | password = self.cached_passwords.get(host_name, NONE) 66 | if password is NONE: 67 | password = self.get_password(host_name) 68 | self.cached_passwords[host_name] = password 69 | return password 70 | 71 | def get_password(self, host): 72 | raise NotImplementedError 73 | 74 | 75 | class KeepassHTTP(KeepassBase): 76 | def __init__(self): 77 | super(KeepassHTTP, self).__init__() 78 | self.k = keepasshttplib.Keepasshttplib() 79 | 80 | def get_password(self, host_name): 81 | if not self.test_connection(): 82 | raise KeepassHTTPError('Keepass is closed!') 83 | try: 84 | auth = self.k.get_credentials('ssh://{}'.format(host_name)) 85 | except Exception as e: 86 | raise KeepassHTTPError( 87 | 'Error obtaining host name {}: {}'.format(host_name, e) 88 | ) 89 | if auth: 90 | return auth[1] 91 | 92 | def test_connection(self): 93 | key = self.k.get_key_from_keyring() 94 | if key is None: 95 | key = encrypter.generate_key() 96 | id_ = self.k.get_id_from_keyring() 97 | try: 98 | return self.k.test_associate(key, id_) 99 | except requests.exceptions.ConnectionError as e: 100 | raise KeepassHTTPError('Connection Error: {}'.format(e)) 101 | 102 | 103 | class KeepassXC(KeepassBase): 104 | _connection = None 105 | 106 | def __init__(self): 107 | super(KeepassXC, self).__init__() 108 | try: 109 | self.identity = self.get_identity() 110 | except Exception as e: 111 | raise KeepassConnectionError( 112 | 'The identity could not be obtained from ' 113 | 'KeepassXC: {}'.format(e) 114 | ) 115 | 116 | def get_identity(self): 117 | data = keyring.get_password(KEEPASSXC_CLIENT_ID, KEYRING_KEY) 118 | if data: 119 | identity = Identity.unserialize(KEEPASSXC_CLIENT_ID, data) 120 | else: 121 | identity = Identity(KEEPASSXC_CLIENT_ID) 122 | return identity 123 | 124 | def get_connection(self, identity): 125 | c = Connection() 126 | c.connect() 127 | c.change_public_keys(identity) 128 | c.get_database_hash(identity) 129 | 130 | if not c.test_associate(identity): 131 | c.associate(identity) 132 | assert c.test_associate(identity), "Keepass Association failed" 133 | data = identity.serialize() 134 | keyring.set_password(KEEPASSXC_CLIENT_ID, KEYRING_KEY, data) 135 | del data 136 | return c 137 | 138 | @property 139 | def connection(self): 140 | if self._connection is None: 141 | try: 142 | self._connection = self.get_connection(self.identity) 143 | except ProtocolError as e: 144 | raise AnsibleKeepassError( 145 | 'ProtocolError on connection: {}'.format(e) 146 | ) 147 | except Exception as e: 148 | raise AnsibleKeepassError( 149 | 'Error on connection: {}'.format(e) 150 | ) 151 | return self._connection 152 | 153 | def get_password(self, host_name): 154 | try: 155 | logins = self.connection.get_logins( 156 | self.identity, 157 | url='ssh:{}'.format(host_name) 158 | ) 159 | except ProtocolError: 160 | return 161 | except Exception as e: 162 | raise KeepassXCError( 163 | 'Error obtaining host name {}: {}'.format(host_name, e) 164 | ) 165 | return next(iter(logins), {}).get('password') 166 | 167 | 168 | def get_host_names(host): 169 | return [host.name] + [group.name for group in host.groups] 170 | 171 | 172 | def get_keepass_class(): 173 | keepass_class = os.environ.get('KEEPASS_CLASS') 174 | if not keepass_class: 175 | for process in psutil.process_iter(): 176 | process_name = process.name().lower() or '' 177 | if process_name in KEEPASSXC_PROCESS_NAMES: 178 | keepass_class = 'KeepassXC' 179 | break 180 | return { 181 | 'KeepassXC': KeepassXC, 182 | 'KeepassHTTP': KeepassHTTP, 183 | }.get(keepass_class, KeepassHTTP) 184 | 185 | 186 | def get_or_create_conn(cls): 187 | if not getattr(__main__, '_keepass', None): 188 | __main__._keepass = cls() 189 | return __main__._keepass 190 | 191 | 192 | class TaskExecutor(_TaskExecutor): 193 | def __init__(self, host, task, job_vars, play_context, *args, 194 | **kwargs): 195 | become = task.become or play_context.become 196 | if become and not job_vars.get('ansible_become_pass'): 197 | password = NONE 198 | cls = get_keepass_class() 199 | try: 200 | kp = get_or_create_conn(cls) 201 | password = kp.get_cached_password(host) 202 | except AnsibleKeepassError as e: 203 | display.error(e) 204 | if password is None: 205 | display.warning( 206 | 'The password could not be obtained using ' 207 | '{}. Hosts tried: '.format(cls.__name__) + 208 | '{}. '.format(', '.join(get_host_names(host))) + 209 | 'Maybe the password is not in the database or does ' 210 | 'not have the url.' 211 | ) 212 | elif password not in [None, NONE]: 213 | job_vars['ansible_become_pass'] = password 214 | super(TaskExecutor, self).__init__(host, task, job_vars, 215 | play_context, *args, **kwargs) 216 | 217 | 218 | setattr(task_executor, 'TaskExecutor', TaskExecutor) 219 | setattr(worker, 'TaskExecutor', TaskExecutor) 220 | 221 | 222 | class VarsModule(BaseVarsPlugin): 223 | 224 | """ 225 | Loads variables for groups and/or hosts 226 | """ 227 | 228 | def get_vars(self, loader, path, entities): 229 | super(VarsModule, self).get_vars(loader, path, entities) 230 | return {} 231 | 232 | def get_host_vars(self, *args, **kwargs): 233 | return {} 234 | 235 | def get_group_vars(self, *args, **kwargs): 236 | return {} 237 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | git+https://github.com/hrehfeld/python-keepassxc-browser.git@master 2 | pysodium 3 | psutil 4 | keepasshttplib --------------------------------------------------------------------------------