├── .flake8 ├── .gitignore ├── LICENSE ├── README.md ├── docs ├── contributing │ └── README.md └── examples │ ├── README.md │ ├── example-playbook.yml │ ├── example.kdbx │ └── group_vars │ └── all ├── galaxy.yml ├── meta └── runtime.yml ├── plugins ├── lookup │ └── keepass.py └── modules │ └── attachment.py └── tests ├── keepass-keyfile-only ├── ansible.kdbx ├── ansible.keyx ├── hosts.ini ├── playbook.yml └── run.sh ├── keepass-password-keyfile ├── ansible.kdbx ├── ansible.keyx ├── hosts.ini ├── playbook.yml └── run.sh ├── keepass-password-only ├── ansible.kdbx ├── hosts.ini ├── playbook.yml └── run.sh └── parallel ├── ansible.cfg ├── ansible.kdbx ├── clear.sh ├── docker ├── .ssh │ ├── id_ed25519 │ └── id_ed25519.pub ├── Dockerfile ├── README.md └── docker-compose.yml ├── group_vars ├── SRV1 ├── SRV2 ├── SRV3 ├── SRV4 ├── SRV5 └── all ├── inventory.ini ├── playbook.yml └── run.sh /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | max-line-length = 88 3 | extend-ignore = E203 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Python template 3 | # Byte-compiled / optimized / DLL files 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # C extensions 9 | *.so 10 | 11 | # Distribution / packaging 12 | .Python 13 | build/ 14 | develop-eggs/ 15 | dist/ 16 | downloads/ 17 | eggs/ 18 | .eggs/ 19 | lib/ 20 | lib64/ 21 | parts/ 22 | sdist/ 23 | var/ 24 | wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | .hypothesis/ 50 | .pytest_cache/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | db.sqlite3 60 | 61 | # Flask stuff: 62 | instance/ 63 | .webassets-cache 64 | 65 | # Scrapy stuff: 66 | .scrapy 67 | 68 | # Sphinx documentation 69 | docs/_build/ 70 | 71 | # PyBuilder 72 | target/ 73 | 74 | # Jupyter Notebook 75 | .ipynb_checkpoints 76 | 77 | # pyenv 78 | .python-version 79 | 80 | # celery beat schedule file 81 | celerybeat-schedule 82 | 83 | # SageMath parsed files 84 | *.sage.py 85 | 86 | # Environments 87 | .env 88 | .venv 89 | env/ 90 | venv/ 91 | ENV/ 92 | env.bak/ 93 | venv.bak/ 94 | 95 | # Spyder project settings 96 | .spyderproject 97 | .spyproject 98 | 99 | # Rope project settings 100 | .ropeproject 101 | 102 | # mkdocs documentation 103 | /site 104 | 105 | # mypy 106 | .mypy_cache/ 107 | 108 | .idea 109 | /docs/examples/attachment* 110 | /*.tar.gz -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Victor Zemtsov 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Ansible KeePass Lookup Plugin 2 | 3 | This collection provides plugins that allows to read data from KeePass file (modifying is not supported) 4 | 5 | ## How it works 6 | 7 | The lookup plugin opens a UNIX socket with decrypted KeePass file. 8 | For performance reasons, decryption occurs only once at socket startup, 9 | and the KeePass file remains decrypted as long as the socket is open. 10 | The UNIX socket file is stored in a temporary folder according to OS. 11 | 12 | ## Installation 13 | 14 | Requirements: `python 3`, `pykeepass==4.0.3` 15 | 16 | pip install 'pykeepass==4.0.3' --user 17 | ansible-galaxy collection install viczem.keepass 18 | 19 | 20 | ## Variables 21 | 22 | - `keepass_dbx` - path to KeePass file 23 | - `keepass_psw` - *Optional*. Password (required if `keepass_key` is not set) 24 | - `keepass_key` - *Optional*. Path to keyfile (required if `keepass_psw` is not set) 25 | - `keepass_ttl` - *Optional*. Socket TTL (will be closed automatically when not used). 26 | Default 60 seconds. 27 | 28 | ## Environment Variables 29 | 30 | If you want to use ansible-keepass with continuous integration, it could be helpful not to use ansible variables but Shell environment variables. 31 | 32 | - `ANSIBLE_KEEPASS_PSW` Password 33 | - `ANSIBLE_KEEPASS_KEY` Path to keyfile 34 | - `ANSIBLE_KEEPASS_TTL` Socket TTL 35 | - `ANSIBLE_KEEPASS_SOCKET` Path to Keepass Socket 36 | 37 | The environment variables will only be used, if no ansible variable is set. 38 | 39 | You can than start the socket in another background process like this 40 | ```sh 41 | export ANSIBLE_KEEPASS_PSW=mySecret 42 | export ANSIBLE_KEEPASS_SOCKET=/home/build/.my-ansible-sock.${CI_JOB_ID} 43 | export ANSIBLE_TTL=600 # 10 Minutes 44 | /home/build/ansible-pyenv/bin/python3 /home/build/.ansible/roles/ansible_collections/viczem/keepass/plugins/lookup/keepass.py /path-to/my-keepass.kdbx & 45 | ansible-playbook -v playbook1.yml 46 | ansible-playbook -v playbook2.yml 47 | 48 | ``` 49 | 50 | ## Usage 51 | 52 | `ansible-doc -t lookup keepass` to get description of the plugin 53 | 54 | > **WARNING**: For security reasons, do not store KeePass passwords in plain text. 55 | Use `ansible-vault encrypt_string` to encrypt it and use it like below 56 | 57 | # file: group_vars/all 58 | 59 | keepass_dbx: "~/.keepass/database.kdbx" 60 | keepass_psw: !vault | 61 | $ANSIBLE_VAULT;1.1;AES256 62 | ...encrypted password... 63 | 64 | ### Examples 65 | 66 | More examples see in [/docs/examples](/docs/examples). 67 | 68 | #### Lookup 69 | 70 | ansible_user : "{{ lookup('viczem.keepass.keepass', 'path/to/entry', 'username') }}" 71 | ansible_become_pass : "{{ lookup('viczem.keepass.keepass', 'path/to/entry', 'password') }}" 72 | custom_field : "{{ lookup('viczem.keepass.keepass', 'path/to/entry', 'custom_properties', 'a_custom_property_name') }}" 73 | attachment : "{{ lookup('viczem.keepass.keepass', 'path/to/entry', 'attachments', 'a_file_name') }}" 74 | 75 | #### Module 76 | - name: "Export file: attachment.txt" 77 | viczem.keepass.attachment: 78 | database: "{{ keepass_dbx }}" 79 | password: "{{ keepass_psw }}" 80 | entrypath: example/attachments 81 | attachment: "attachment.txt" 82 | dest: "{{ keepass_attachment_1_name }}" 83 | 84 | ## Contributing 85 | 86 | See [/docs/contributing](docs/contributing). -------------------------------------------------------------------------------- /docs/contributing/README.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Create ansible.cfg in cloned directory: 4 | 5 | ``` 6 | [defaults] 7 | COLLECTIONS_PATH = ./collections 8 | ``` 9 | 10 | 2. Create requirements.yml in cloned directory: 11 | 12 | ``` 13 | --- 14 | collections: 15 | - name: namespace.collection_name 16 | source: /where/is/your/clone 17 | type: dir 18 | ``` 19 | 20 | 21 | 3. To install the collection _locally_ in your cloned directory, just install it through ansible-galaxy 22 | ```shell 23 | rm -rf ./collections && ansible-galaxy install -r requirements.yml 24 | ``` 25 | 26 | Note: Any change on your clone imply to reinstall the collection. 27 | 28 | 29 | Tip: You can place a ansible.cfg with `COLLECTIONS_PATH = ../../collections` in the examples dictory if you want to run the example on local collection in your cloned directory. 30 | -------------------------------------------------------------------------------- /docs/examples/README.md: -------------------------------------------------------------------------------- 1 | # Example 2 | 3 | `ansible-playbook example-playbook.yml --ask-vault-pass -vvv` 4 | 5 | Password: `spamham` 6 | 7 | 8 | ## Docker 9 | 10 | `DOCKER_BUILDKIT=1 docker-compose up --build` -------------------------------------------------------------------------------- /docs/examples/example-playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Example 3 | hosts: 127.0.0.1 4 | connection: local 5 | vars: 6 | spam_login: "{{ lookup('viczem.keepass.keepass', 'spam', 'username') }}" 7 | spam_password: "{{ lookup('viczem.keepass.keepass', 'spam', 'password') }}" 8 | ham_login: "{{ lookup('viczem.keepass.keepass', 'example/ham', 'username') }}" 9 | ham_password: "{{ lookup('viczem.keepass.keepass', 'example/ham', 'password') }}" 10 | slash_login: "{{ lookup('viczem.keepass.keepass', 'slash\\/group/slash\\/title', 'username') }}" 11 | slash_url: "{{ lookup('viczem.keepass.keepass', 'slash\\/group/slash\\/title', 'url') }}" 12 | pork_custom_property: "{{ lookup('viczem.keepass.keepass', 'example/pork', 'custom_properties', 'pork_custom_property')}}" 13 | attachment: "{{ lookup('viczem.keepass.keepass', 'example/pork', 'attachments', 'test.txt')}}" 14 | keepass_attachment_1_name: "attachment_1.txt" 15 | keepass_attachment_2_name: "attachment_2.zip" 16 | 17 | tasks: 18 | - debug: 19 | msg: "fetch entry: '/spam'; username: '{{ spam_login }}'; password: '{{ spam_password }}'" 20 | 21 | - debug: 22 | msg: "fetch entry: '/examples/ham'; username: '{{ ham_login }}'; password: '{{ ham_password }}'" 23 | 24 | - debug: 25 | msg: "fetch entry: '/examples/port'; attachments: 'text.txt' - '{{ attachment }}'" 26 | 27 | - name: pause to emulate long time operation (greater than keepass_ttl) 28 | pause: 29 | seconds: 5 30 | 31 | - debug: 32 | msg: "fetch entry: '/examples/pork'; custom_properties: 'pork_custom_property' - '{{ pork_custom_property }}'" 33 | 34 | - debug: 35 | msg: "fetch entry: '/slash\\/group/slash\\/title'; username: '{{ slash_login }}'; url: '{{ slash_url }}'" 36 | 37 | - debug: 38 | msg: "close {{ lookup('viczem.keepass.keepass', 'close') }}" 39 | 40 | - name: "Export file: {{ keepass_attachment_1_name }}" 41 | viczem.keepass.attachment: 42 | database: "{{ keepass_dbx }}" 43 | password: "{{ keepass_psw }}" 44 | entrypath: example/attachments 45 | attachment: "{{ keepass_attachment_1_name }}" 46 | dest: "{{ keepass_attachment_1_name }}" 47 | 48 | - name: "Export file: {{ keepass_attachment_2_name }}" 49 | viczem.keepass.attachment: 50 | database: "{{ keepass_dbx }}" 51 | password: "{{ keepass_psw }}" 52 | entrypath: example/attachments 53 | attachment: "{{ keepass_attachment_2_name }}" 54 | dest: "{{ keepass_attachment_2_name }}" 55 | mode: 0600 -------------------------------------------------------------------------------- /docs/examples/example.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viczem/ansible-keepass/b633a57fc316c86f5b0c2b3b7afcd5eccdde4a62/docs/examples/example.kdbx -------------------------------------------------------------------------------- /docs/examples/group_vars/all: -------------------------------------------------------------------------------- 1 | keepass_ttl: 3 2 | keepass_dbx: "./example.kdbx" 3 | keepass_psw: !vault | 4 | $ANSIBLE_VAULT;1.1;AES256 5 | 30656633313531336265353862356135373963636339376266373137376136636634393932623961 6 | 6138656232363861333932373066636237626232623566380a313964313733643532373139313636 7 | 62303365393630383037356334363332306239316566383061336263383134353139663161643331 8 | 3736316666613761380a646333353163633236323835313965313034373163343031616531393336 9 | 6538 10 | -------------------------------------------------------------------------------- /galaxy.yml: -------------------------------------------------------------------------------- 1 | ### REQUIRED 2 | # The namespace of the collection. This can be a company/brand/organization or product namespace under which all 3 | # content lives. May only contain alphanumeric lowercase characters and underscores. Namespaces cannot start with 4 | # underscores or numbers and cannot contain consecutive underscores 5 | namespace: viczem 6 | 7 | # The name of the collection. Has the same character restrictions as 'namespace' 8 | name: keepass 9 | 10 | # The version of the collection. Must be compatible with semantic versioning 11 | version: 0.7.5 12 | 13 | # The path to the Markdown (.md) readme file. This path is relative to the root of the collection 14 | readme: README.md 15 | 16 | # A list of the collection's content authors. Can be just the name or in the format 'Full Name (url) 17 | # @nicks:irc/im.site#channel' 18 | authors: 19 | - Victor Zemtsov 20 | 21 | 22 | ### OPTIONAL but strongly recommended 23 | # A short summary description of the collection 24 | description: The collection provides plugins that allow to read data from KeePass file. 25 | 26 | # The path to the license file for the collection. This path is relative to the root of the collection. This key is 27 | # mutually exclusive with 'license' 28 | license_file: 'LICENSE' 29 | 30 | # A list of tags you want to associate with the collection for indexing/searching. A tag name has the same character 31 | # requirements as 'namespace' and 'name' 32 | tags: 33 | - keepass 34 | - lookup 35 | - module 36 | - plugin 37 | 38 | # Collections that this collection requires to be installed for it to be usable. The key of the dict is the 39 | # collection label 'namespace.name'. The value is a version range 40 | # L(specifiers,https://python-semanticversion.readthedocs.io/en/latest/#requirement-specification). Multiple version 41 | # range specifiers can be set and are separated by ',' 42 | dependencies: {} 43 | 44 | # The URL of the originating SCM repository 45 | repository: https://github.com/viczem/ansible-keepass 46 | 47 | # The URL to any online docs 48 | documentation: https://github.com/viczem/ansible-keepass/blob/main/doc 49 | 50 | # The URL to the homepage of the collection/project 51 | homepage: https://github.com/viczem/ansible-keepass 52 | 53 | # The URL to the collection issue tracker 54 | issues: https://github.com/viczem/ansible-keepass/issues 55 | 56 | # A list of file glob-like patterns used to filter any files or directories that should not be included in the build 57 | # artifact. A pattern is matched from the relative path of the file or directory of the collection directory. This 58 | # uses 'fnmatch' to match the files or directories. Some directories and files like 'galaxy.yml', '*.pyc', '*.retry', 59 | # and '.git' are always filtered 60 | build_ignore: [] 61 | 62 | -------------------------------------------------------------------------------- /meta/runtime.yml: -------------------------------------------------------------------------------- 1 | requires_ansible: ">=2.10" 2 | 3 | -------------------------------------------------------------------------------- /plugins/lookup/keepass.py: -------------------------------------------------------------------------------- 1 | __metaclass__ = type 2 | 3 | import argparse 4 | import getpass 5 | import hashlib 6 | import fcntl 7 | import os 8 | import re 9 | import socket 10 | import subprocess 11 | import sys 12 | import tempfile 13 | import time 14 | import traceback 15 | 16 | from ansible.errors import AnsibleError 17 | from ansible.plugins.lookup import LookupBase 18 | from ansible.utils.display import Display 19 | from pykeepass import PyKeePass 20 | from pykeepass.exceptions import CredentialsError 21 | 22 | DOCUMENTATION = """ 23 | lookup: keepass 24 | author: Victor Zemtsov 25 | version_added: '0.7.5' 26 | short_description: Fetching data from KeePass file 27 | description: 28 | - This lookup returns a value of a property of a KeePass entry 29 | - which fetched by given path 30 | options: 31 | _terms: 32 | description: 33 | - first is a path to KeePass entry 34 | - second is a property name of the entry, e.g. username or password 35 | required: True 36 | notes: 37 | - https://github.com/viczem/ansible-keepass 38 | 39 | examples: 40 | - "{{ lookup('keepass', 'path/to/entry', 'username') }}" 41 | - "{{ lookup('keepass', 'path/to/entry', 'password') }}" 42 | - "{{ lookup('keepass', 'path/to/entry', 'custom_properties', 'my_prop_name') }}" 43 | - "{{ lookup('keepass', 'path/to/entry', 'attachments', 'my_file_name') }}" 44 | """ 45 | 46 | display = Display() 47 | 48 | 49 | class LookupModule(LookupBase): 50 | keepass = None 51 | 52 | def _var(self, var_value): 53 | return self._templar.template(var_value, fail_on_undefined=True) 54 | 55 | def run(self, terms, variables=None, **kwargs): 56 | if not terms: 57 | raise AnsibleError("KeePass: arguments is not set") 58 | if not all(isinstance(_, str) for _ in terms): 59 | raise AnsibleError("KeePass: invalid argument type, all must be string") 60 | 61 | if variables is not None: 62 | self._templar.available_variables = variables 63 | variables_ = getattr(self._templar, "_available_variables", {}) 64 | 65 | # Check keepass database file (required) 66 | var_dbx = self._var(variables_.get("keepass_dbx", "")) 67 | if not var_dbx: 68 | raise AnsibleError("KeePass: 'keepass_dbx' is not set") 69 | var_dbx = os.path.realpath(os.path.expanduser(os.path.expandvars(var_dbx))) 70 | if not os.path.isfile(var_dbx): 71 | raise AnsibleError("KeePass: '%s' is not found" % var_dbx) 72 | 73 | # Check key file (optional) 74 | var_key = self._var(variables_.get("keepass_key", "")) 75 | if not var_key and "ANSIBLE_KEEPASS_KEY_FILE" in os.environ: 76 | var_key = os.environ.get('ANSIBLE_KEEPASS_KEY_FILE') 77 | 78 | if var_key: 79 | var_key = os.path.realpath(os.path.expanduser(os.path.expandvars(var_key))) 80 | if not os.path.isfile(var_key): 81 | raise AnsibleError("KeePass: '%s' is not found" % var_key) 82 | 83 | # Check password (optional) 84 | var_psw = self._var(variables_.get("keepass_psw", "")) 85 | 86 | if not var_psw and "ANSIBLE_KEEPASS_PSW" in os.environ: 87 | var_psw = os.environ.get('ANSIBLE_KEEPASS_PSW') 88 | 89 | if not var_key and not var_psw: 90 | raise AnsibleError("KeePass: 'keepass_psw' and/or 'keepass_key' is not set") 91 | 92 | # TTL of keepass socket (optional, default: 60 seconds) 93 | default_ttl = "60" 94 | if "ANSIBLE_KEEPASS_TTL" in os.environ: 95 | default_ttl = os.environ.get("ANSIBLE_KEEPASS_TTL") 96 | var_ttl = self._var(str(variables_.get("keepass_ttl", default_ttl))) 97 | 98 | socket_path = _keepass_socket_path(var_dbx) 99 | lock_file_ = socket_path + ".lock" 100 | 101 | try: 102 | os.open(lock_file_, os.O_RDWR) 103 | except FileNotFoundError: 104 | cmd = [ 105 | sys.executable, 106 | os.path.abspath(__file__), 107 | var_dbx, 108 | socket_path, 109 | var_ttl, 110 | ] 111 | if var_key: 112 | cmd.append("--key=%s" % var_key) 113 | try: 114 | display.v("KeePass: run socket for %s" % var_dbx) 115 | subprocess.Popen(cmd) 116 | except OSError: 117 | os.remove(lock_file_) 118 | raise AnsibleError(traceback.format_exc()) 119 | 120 | attempts = 10 121 | success = False 122 | for _ in range(attempts): 123 | try: 124 | display.vvv("KeePass: try connect to socket %s/%s" % (_, attempts)) 125 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 126 | sock.connect(socket_path) 127 | # send password to the socket for decrypt keepass dbx 128 | display.vvv("KeePass: send password to '%s'" % socket_path) 129 | sock.send(_rq("password", str(var_psw))) 130 | resp = sock.recv(1024).decode().splitlines() 131 | 132 | if len(resp) == 2 and resp[0] == "password": 133 | if resp[1] == "0": 134 | success = True 135 | else: 136 | raise AnsibleError("KeePass: wrong dbx password") 137 | sock.close() 138 | break 139 | except FileNotFoundError: 140 | # wait until the above command open the socket 141 | time.sleep(1) 142 | 143 | if not success: 144 | raise AnsibleError("KeePass: socket connection failed for %s" % var_dbx) 145 | 146 | display.v("KeePass: open socket for %s -> %s" % (var_dbx, socket_path)) 147 | 148 | if len(terms) == 1 and terms[0] in ("quit", "exit", "close"): 149 | self._send(socket_path, terms[0], []) 150 | return [] 151 | else: 152 | # Fetching data from the keepass socket 153 | return self._send(socket_path, "fetch", terms) 154 | 155 | def _send(self, kp_soc, cmd, terms): 156 | display.vvv("KeePass: connect to '%s'" % kp_soc) 157 | sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) 158 | 159 | try: 160 | sock.connect(kp_soc) 161 | except FileNotFoundError: 162 | raise AnsibleError("KeePass: '%s' is not found" % kp_soc) 163 | 164 | try: 165 | display.vvv("KeePass: %s %s" % (cmd, terms)) 166 | sock.send(_rq(cmd, *terms)) 167 | 168 | data = b'' 169 | while True: 170 | _ = sock.recv(1024) 171 | data += _ 172 | if len(_) < 1024: 173 | break 174 | 175 | resp = data.decode().splitlines() 176 | resp_len = len(resp) 177 | if resp_len == 0: 178 | raise AnsibleError("KeePass: '%s' result is empty" % cmd) 179 | 180 | if resp_len >= 3: 181 | if resp[0] != cmd: 182 | raise AnsibleError( 183 | "KeePass: received command '%s', expected '%s'" % (resp[0], cmd) 184 | ) 185 | if resp[1] == "0": 186 | return [os.linesep.join(resp[2:])] 187 | else: 188 | raise AnsibleError("KeePass: '%s' has error '%s'" % (resp[2], cmd)) 189 | 190 | except Exception as e: 191 | raise AnsibleError(str(e)) 192 | finally: 193 | sock.close() 194 | display.vvv("KeePass: disconnect from '%s'" % kp_soc) 195 | 196 | 197 | def _keepass_socket(kdbx, kdbx_key, sock_path, ttl=60, kdbx_password=None): 198 | """ 199 | 200 | :param str kdbx: 201 | :param str kdbx_key: 202 | :param str sock_path: 203 | :param int ttl: in seconds 204 | :return: 205 | 206 | Socket messages have multiline format. 207 | First line is a command for both messages are request and response 208 | """ 209 | tmp_files = [] 210 | try: 211 | with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as s: 212 | s.bind(sock_path) 213 | s.listen(1) 214 | if ttl > 0: 215 | s.settimeout(ttl) 216 | if kdbx_password: 217 | kp = PyKeePass(kdbx, kdbx_password, kdbx_key) 218 | else: 219 | kp = None 220 | 221 | is_open = True 222 | 223 | while is_open: 224 | conn, addr = s.accept() 225 | with conn: 226 | if ttl > 0: 227 | conn.settimeout(ttl) 228 | while True: 229 | data = conn.recv(1024).decode() 230 | if not data: 231 | break 232 | 233 | rq = data.splitlines() 234 | if len(rq) == 0: 235 | conn.send(_resp("", 1, "empty request")) 236 | break 237 | 238 | cmd, *arg = rq 239 | arg_len = len(arg) 240 | 241 | # CMD: quit | exit | close 242 | if arg_len == 0 and cmd in ("quit", "exit", "close"): 243 | conn.send(_resp(cmd, 0)) 244 | conn.close() 245 | is_open = False 246 | break 247 | 248 | # CMD: password 249 | if kp is None: 250 | if cmd == "password" and arg_len > 0: 251 | kp = PyKeePass(kdbx, arg[0], kdbx_key) 252 | conn.send(_resp("password", 0)) 253 | break 254 | elif cmd == "password" and kdbx_key: 255 | kp = PyKeePass(kdbx, None, kdbx_key) 256 | conn.send(_resp("password", 0)) 257 | break 258 | else: 259 | conn.send(_resp("password", 1)) 260 | break 261 | elif cmd == "password": 262 | conn.send(_resp("password", 0)) 263 | break 264 | 265 | # CMD: fetch 266 | # Read data from decrypted KeePass file 267 | if cmd != "fetch": 268 | conn.send(_resp("fetch", 1, "unknown command '%s'" % cmd)) 269 | break 270 | 271 | if arg_len == 0: 272 | conn.send(_resp("fetch", 1, "path is not set")) 273 | break 274 | 275 | if arg_len == 1: 276 | conn.send( 277 | _resp( 278 | "fetch", 279 | 1, 280 | "property name is not set for '%s'" % arg[0], 281 | ) 282 | ) 283 | break 284 | 285 | path = [ 286 | _.replace("\\/", "/") 287 | for _ in re.split(r"(? 5 | # Copyright: (c) 2022, LFV 6 | 7 | __metaclass__ = type 8 | 9 | import traceback 10 | from ansible.module_utils.basic import AnsibleModule 11 | from ansible.module_utils.basic import missing_required_lib 12 | from ansible.module_utils._text import to_bytes, to_native 13 | 14 | import os 15 | import tempfile 16 | 17 | LIB_IMP_ERR = None 18 | try: 19 | from pykeepass import PyKeePass 20 | 21 | HAS_LIB = True 22 | except Exception: 23 | HAS_LIB = False 24 | LIB_IMP_ERR = traceback.format_exc() 25 | 26 | 27 | DOCUMENTATION = r""" 28 | --- 29 | module: attachment 30 | author: 31 | - Jimisola Laursen (@lfvjimisola) 32 | - Jimisola Laursen (@jimisola) 33 | 34 | short_description: Exports KeePass attachments 35 | description: 36 | - This module will export an attachment in a KeePass entry to a file. 37 | 38 | version_added: "0.1.0" 39 | 40 | extends_documentation_fragment: 41 | - files 42 | - action_common_attributes 43 | 44 | requirements: 45 | - pykeepass 46 | 47 | options: 48 | database: 49 | description: Path to KeePass database file 50 | required: true 51 | type: str 52 | password: 53 | description: Password for KeePass database file 54 | required: true 55 | type: str 56 | entrypath: 57 | description: Path to KeePass entry containing the attachment that should be exported 58 | required: true 59 | type: str 60 | attachment: 61 | description: Name of attachment that should be exported 62 | required: true 63 | type: str 64 | dest: 65 | description: Absolute path where the file should be exported to 66 | required: true 67 | type: str 68 | 69 | attributes: 70 | check_mode: 71 | support: none 72 | diff_mode: 73 | support: none 74 | platform: 75 | platforms: posix 76 | """ 77 | 78 | EXAMPLES = r""" 79 | # Export a file 80 | - name: Export a file from KeePass 81 | keepass: 82 | database: database.kdbx 83 | password: somepassword 84 | path: "group/subgroup/entry" 85 | attachment: somefile.txt 86 | dest: somefile_exported.txt 87 | """ 88 | 89 | RETURN = r""" # """ 90 | 91 | 92 | def check_file_attrs(module, result, diff): 93 | 94 | changed, msg = result["changed"], result["msg"] 95 | 96 | file_args = module.load_file_common_arguments(module.params) 97 | if module.set_fs_attributes_if_different(file_args, False, diff=diff): 98 | 99 | if changed: 100 | msg += " and " 101 | changed = True 102 | msg += "ownership, perms or SE linux context changed" 103 | 104 | result["changed"] = changed 105 | result["msg"] = msg 106 | 107 | return result 108 | 109 | 110 | def export_attachment(module, result): 111 | try: 112 | # load database 113 | kp = PyKeePass( 114 | module.params["database"], 115 | password=module.params["password"], 116 | keyfile=module.params["keyfile"]) 117 | 118 | entrypath = module.params["entrypath"] 119 | dest = module.params["dest"] 120 | attachment = module.params["attachment"] 121 | 122 | # find entry 123 | kp_entry = kp.find_entries(path=entrypath.split("/"), first=True) 124 | 125 | if kp_entry is None: 126 | module.fail_json(msg="Entry '{0}' not found".format(entrypath)) 127 | 128 | kp_attachment = None 129 | for item in kp_entry.attachments: 130 | if item.filename == attachment: 131 | kp_attachment = item 132 | 133 | if kp_attachment is None: 134 | module.fail_json( 135 | msg="Entry '{0}' does not contain attachment '{1}'".format( 136 | entrypath, attachment 137 | ) 138 | ) 139 | 140 | b_data = kp_attachment.binary 141 | 142 | tmpfd, tmpfile = tempfile.mkstemp() 143 | f = os.fdopen(tmpfd, "wb") 144 | f.write(b_data) 145 | f.close() 146 | 147 | module.atomic_move( 148 | tmpfile, 149 | to_native( 150 | os.path.realpath(to_bytes(dest, errors="surrogate_or_strict")), 151 | errors="surrogate_or_strict", 152 | ), 153 | unsafe_writes=module.params["unsafe_writes"], 154 | ) 155 | 156 | result["changed"] = True 157 | result["msg"] = "attachment '{0}' exported to file '{1}'".format( 158 | module.params["attachment"], dest 159 | ) 160 | 161 | except Exception as e: 162 | result["msg"] = "Module viczem.keepass.attachment failed: {0}".format(e) 163 | module.fail_json(**result) 164 | 165 | attr_diff = None 166 | 167 | result = check_file_attrs(module, result, attr_diff) 168 | 169 | module.exit_json(**result, diff=attr_diff) 170 | 171 | 172 | def main(): 173 | module_args = dict( 174 | database=dict(type="str", required=True), 175 | password=dict(type="str", no_log=True, required=True), 176 | keyfile=dict(type="str", no_log=True, required=False), 177 | entrypath=dict(type="str", required=True), 178 | attachment=dict(type="str", required=True), 179 | dest=dict(type="path", required=True), 180 | ) 181 | 182 | module = AnsibleModule( 183 | argument_spec=module_args, 184 | add_file_common_args=True, 185 | ) 186 | 187 | if not HAS_LIB: 188 | module.fail_json(msg=missing_required_lib("pykeepass"), exception=LIB_IMP_ERR) 189 | 190 | result = dict( 191 | changed=False, 192 | ) 193 | 194 | dest = module.params["dest"] 195 | b_dest = to_bytes(dest, errors="surrogate_or_strict") 196 | 197 | if os.path.isdir(b_dest): 198 | module.fail_json(rc=256, msg="Destination {0} is a directory!".format(dest)) 199 | 200 | export_attachment(module, result) 201 | 202 | 203 | if __name__ == "__main__": 204 | main() 205 | -------------------------------------------------------------------------------- /tests/keepass-keyfile-only/ansible.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viczem/ansible-keepass/b633a57fc316c86f5b0c2b3b7afcd5eccdde4a62/tests/keepass-keyfile-only/ansible.kdbx -------------------------------------------------------------------------------- /tests/keepass-keyfile-only/ansible.keyx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.0 5 | 6 | 7 | 8 | 8810353D 83453EDC 2266A931 A0A073F9 9 | 54B90B68 1E341EF4 6B47729B F42DBE0A 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/keepass-keyfile-only/hosts.ini: -------------------------------------------------------------------------------- 1 | [test] 2 | 127.0.0.1 keepass_dbx=./ansible.kdbx keepass_key=./ansible.keyx keepass_ttl=3 -------------------------------------------------------------------------------- /tests/keepass-keyfile-only/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: test-keepass-keyfile-only 3 | hosts: test 4 | connection: local 5 | vars: 6 | test_username: "{{ lookup('viczem.keepass.keepass', 'test', 'username') }}" 7 | test_password: "{{ lookup('viczem.keepass.keepass', 'test', 'password') }}" 8 | 9 | tasks: 10 | - debug: 11 | msg: "fetch entry: '/test'; username: '{{ test_username }}'; password: '{{ test_password }}'" 12 | -------------------------------------------------------------------------------- /tests/keepass-keyfile-only/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ansible-playbook -i hosts.ini -vvvv playbook.yml -------------------------------------------------------------------------------- /tests/keepass-password-keyfile/ansible.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viczem/ansible-keepass/b633a57fc316c86f5b0c2b3b7afcd5eccdde4a62/tests/keepass-password-keyfile/ansible.kdbx -------------------------------------------------------------------------------- /tests/keepass-password-keyfile/ansible.keyx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 2.0 5 | 6 | 7 | 8 | D7A7EA4F D6DCBFD7 B2DFE21C E89FFBB0 9 | B203AAA5 4A32C405 D6C1B3CA B69C40BF 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /tests/keepass-password-keyfile/hosts.ini: -------------------------------------------------------------------------------- 1 | [test] 2 | 127.0.0.1 keepass_dbx=./ansible.kdbx keepass_psw=spamham keepass_key=./ansible.keyx keepass_ttl=3 -------------------------------------------------------------------------------- /tests/keepass-password-keyfile/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: test-keepass-keyfile-only 3 | hosts: test 4 | connection: local 5 | vars: 6 | test_username: "{{ lookup('viczem.keepass.keepass', 'test', 'username') }}" 7 | test_password: "{{ lookup('viczem.keepass.keepass', 'test', 'password') }}" 8 | 9 | tasks: 10 | - debug: 11 | msg: "fetch entry: '/test'; username: '{{ test_username }}'; password: '{{ test_password }}'" 12 | -------------------------------------------------------------------------------- /tests/keepass-password-keyfile/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ansible-playbook -i hosts.ini -vvvv playbook.yml -------------------------------------------------------------------------------- /tests/keepass-password-only/ansible.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viczem/ansible-keepass/b633a57fc316c86f5b0c2b3b7afcd5eccdde4a62/tests/keepass-password-only/ansible.kdbx -------------------------------------------------------------------------------- /tests/keepass-password-only/hosts.ini: -------------------------------------------------------------------------------- 1 | [test] 2 | 127.0.0.1 keepass_dbx=./ansible.kdbx keepass_psw=spamham keepass_ttl=3 -------------------------------------------------------------------------------- /tests/keepass-password-only/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: test-keepass-keyfile-only 3 | hosts: test 4 | connection: local 5 | vars: 6 | test_username: "{{ lookup('viczem.keepass.keepass', 'test', 'username') }}" 7 | test_password: "{{ lookup('viczem.keepass.keepass', 'test', 'password') }}" 8 | 9 | tasks: 10 | - debug: 11 | msg: "fetch entry: '/test'; username: '{{ test_username }}'; password: '{{ test_password }}'" 12 | -------------------------------------------------------------------------------- /tests/keepass-password-only/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | ansible-playbook -i hosts.ini -vvvv playbook.yml -------------------------------------------------------------------------------- /tests/parallel/ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | host_key_checking = False 3 | inventory = ./inventory.ini -------------------------------------------------------------------------------- /tests/parallel/ansible.kdbx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/viczem/ansible-keepass/b633a57fc316c86f5b0c2b3b7afcd5eccdde4a62/tests/parallel/ansible.kdbx -------------------------------------------------------------------------------- /tests/parallel/clear.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | ssh-keygen -R 172.24.2.1 4 | ssh-keygen -R 172.24.2.2 5 | ssh-keygen -R 172.24.2.3 6 | ssh-keygen -R 172.24.2.4 7 | ssh-keygen -R 172.24.2.5 8 | 9 | cd ./docker || exit 10 | docker-compose down 11 | docker rmi ansible-keepass-test-1 ansible-keepass-test-2 ansible-keepass-test-3 ansible-keepass-test-4 ansible-keepass-test-5 12 | -------------------------------------------------------------------------------- /tests/parallel/docker/.ssh/id_ed25519: -------------------------------------------------------------------------------- 1 | -----BEGIN OPENSSH PRIVATE KEY----- 2 | b3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW 3 | QyNTUxOQAAACCW034btEPrMzPe4xNKw02O70DhQr+EOnwz0vqNNqUgtAAAAJjHR3u1x0d7 4 | tQAAAAtzc2gtZWQyNTUxOQAAACCW034btEPrMzPe4xNKw02O70DhQr+EOnwz0vqNNqUgtA 5 | AAAEBnKHslpVj1lBKjreOmPTIhd5mPgl3jaCHlEleLVmSfd5bTfhu0Q+szM97jE0rDTY7v 6 | QOFCv4Q6fDPS+o02pSC0AAAAE2V4YW1wbGVAZXhhbXBsZS5jb20BAg== 7 | -----END OPENSSH PRIVATE KEY----- 8 | -------------------------------------------------------------------------------- /tests/parallel/docker/.ssh/id_ed25519.pub: -------------------------------------------------------------------------------- 1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJbTfhu0Q+szM97jE0rDTY7vQOFCv4Q6fDPS+o02pSC0 example@example.com 2 | -------------------------------------------------------------------------------- /tests/parallel/docker/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.16 2 | 3 | ARG USERNAME 4 | ARG PASSWORD 5 | 6 | 7 | RUN apk add --update --no-cache sudo openssh python3 \ 8 | && cd /etc/ssh && ssh-keygen -A \ 9 | && echo "PubkeyAuthentication yes" >> /etc/ssh/sshd_config \ 10 | && echo "PermitRootLogin no" >> /etc/ssh/sshd_config \ 11 | && echo "PasswordAuthentication no" >> /etc/ssh/sshd_config \ 12 | && echo '%wheel ALL=(ALL) ALL' > /etc/sudoers.d/wheel 13 | 14 | RUN adduser -D $USERNAME -G wheel \ 15 | && echo $USERNAME:$PASSWORD | chpasswd \ 16 | && mkdir -p /home/$USERNAME/.ssh \ 17 | && chmod go-w /home/$USERNAME \ 18 | && chmod 700 /home/$USERNAME/.ssh \ 19 | && chown $USERNAME -R /home/$USERNAME/.ssh 20 | 21 | COPY --chmod=600 --chown=$USERNAME .ssh/id_ed25519.pub /home/$USERNAME/.ssh/authorized_keys 22 | 23 | EXPOSE 22 24 | 25 | CMD ["/usr/sbin/sshd", "-D"] -------------------------------------------------------------------------------- /tests/parallel/docker/README.md: -------------------------------------------------------------------------------- 1 | 2 | ## UP test servers 3 | 4 | ```sh 5 | DOCKER_BUILDKIT=1 docker-compose build 6 | docker-compose up -d 7 | ``` 8 | 9 | ## DOWN test servers 10 | ```sh 11 | docker-compose down 12 | docker rmi ansible-keepass-test-1 ansible-keepass-test-2 ansible-keepass-test-3 ansible-keepass-test-4 ansible-keepass-test-5 13 | ``` -------------------------------------------------------------------------------- /tests/parallel/docker/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | services: 3 | ansible-keepass-test-1: 4 | build: 5 | context: . 6 | args: 7 | USERNAME: user1 8 | PASSWORD: password1 9 | image: "ansible-keepass-test-1:latest" 10 | networks: 11 | ansible-net: 12 | ipv4_address: 172.24.2.1 13 | 14 | ansible-keepass-test-2: 15 | build: 16 | context: . 17 | args: 18 | USERNAME: user2 19 | PASSWORD: password2 20 | image: "ansible-keepass-test-2:latest" 21 | networks: 22 | ansible-net: 23 | ipv4_address: 172.24.2.2 24 | 25 | 26 | ansible-keepass-test-3: 27 | build: 28 | context: . 29 | args: 30 | USERNAME: user3 31 | PASSWORD: password3 32 | image: "ansible-keepass-test-3:latest" 33 | networks: 34 | ansible-net: 35 | ipv4_address: 172.24.2.3 36 | 37 | ansible-keepass-test-4: 38 | build: 39 | context: . 40 | args: 41 | USERNAME: user4 42 | PASSWORD: password4 43 | image: "ansible-keepass-test-4:latest" 44 | networks: 45 | ansible-net: 46 | ipv4_address: 172.24.2.4 47 | 48 | ansible-keepass-test-5: 49 | build: 50 | context: . 51 | args: 52 | USERNAME: user5 53 | PASSWORD: password5 54 | image: "ansible-keepass-test-5:latest" 55 | networks: 56 | ansible-net: 57 | ipv4_address: 172.24.2.5 58 | 59 | networks: 60 | ansible-net: 61 | driver: bridge 62 | ipam: 63 | driver: default 64 | config: 65 | - subnet: "172.24.2.0/16" -------------------------------------------------------------------------------- /tests/parallel/group_vars/SRV1: -------------------------------------------------------------------------------- 1 | ansible_user : "{{ lookup('viczem.keepass.keepass', 'srv-1', 'username') }}" 2 | ansible_become_pass : "{{ lookup('viczem.keepass.keepass', 'srv-1', 'password') }}" 3 | -------------------------------------------------------------------------------- /tests/parallel/group_vars/SRV2: -------------------------------------------------------------------------------- 1 | ansible_host : 172.24.2.2 2 | ansible_user : "{{ lookup('viczem.keepass.keepass', 'srv-2', 'username') }}" 3 | ansible_become_pass : "{{ lookup('viczem.keepass.keepass', 'srv-2', 'password') }}" 4 | -------------------------------------------------------------------------------- /tests/parallel/group_vars/SRV3: -------------------------------------------------------------------------------- 1 | ansible_host : 172.24.2.3 2 | ansible_user : "{{ lookup('viczem.keepass.keepass', 'srv-3', 'username') }}" 3 | ansible_become_pass : "{{ lookup('viczem.keepass.keepass', 'srv-3', 'password') }}" 4 | -------------------------------------------------------------------------------- /tests/parallel/group_vars/SRV4: -------------------------------------------------------------------------------- 1 | ansible_host : 172.24.2.4 2 | ansible_user : "{{ lookup('viczem.keepass.keepass', 'srv-4', 'username') }}" 3 | ansible_become_pass : "{{ lookup('viczem.keepass.keepass', 'srv-4', 'password') }}" 4 | -------------------------------------------------------------------------------- /tests/parallel/group_vars/SRV5: -------------------------------------------------------------------------------- 1 | ansible_host : 172.24.2.5 2 | ansible_user : "{{ lookup('viczem.keepass.keepass', 'srv-5', 'username') }}" 3 | ansible_become_pass : "{{ lookup('viczem.keepass.keepass', 'srv-5', 'password') }}" 4 | -------------------------------------------------------------------------------- /tests/parallel/group_vars/all: -------------------------------------------------------------------------------- 1 | keepass_dbx: ./ansible.kdbx 2 | keepass_psw: spamham 3 | keepass_ttl: 3 4 | 5 | ansible_ssh_private_key_file: ./docker/.ssh/id_ed25519 -------------------------------------------------------------------------------- /tests/parallel/inventory.ini: -------------------------------------------------------------------------------- 1 | [SRV1] 2 | srv-1 ansible_host=172.24.2.1 3 | 4 | [SRV2] 5 | srv-2 ansible_host=172.24.2.2 6 | 7 | [SRV3] 8 | srv-3 ansible_host=172.24.2.3 9 | 10 | [SRV4] 11 | srv-4 ansible_host=172.24.2.4 12 | 13 | [SRV5] 14 | srv-5 ansible_host=172.24.2.5 15 | -------------------------------------------------------------------------------- /tests/parallel/playbook.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Parallel 3 | hosts: all 4 | 5 | tasks: 6 | - ansible.builtin.ping: 7 | 8 | - name: pause to emulate long time operation (greater than keepass_ttl) 9 | pause: 10 | seconds: 5 11 | 12 | - ansible.builtin.ping: 13 | -------------------------------------------------------------------------------- /tests/parallel/run.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ansible all -m ping 3 | ansible-playbook playbook.yml -f5 --------------------------------------------------------------------------------