├── meta
└── runtime.yml
├── .flake8
├── tests
├── keepass-keyfile-only
│ ├── run.sh
│ ├── hosts.ini
│ ├── ansible.kdbx
│ ├── ansible.keyx
│ └── playbook.yml
├── keepass-password-only
│ ├── run.sh
│ ├── hosts.ini
│ ├── ansible.kdbx
│ └── playbook.yml
├── parallel
│ ├── ansible.cfg
│ ├── run.sh
│ ├── ansible.kdbx
│ ├── docker
│ │ ├── .ssh
│ │ │ ├── id_ed25519.pub
│ │ │ └── id_ed25519
│ │ ├── README.md
│ │ ├── Dockerfile
│ │ └── docker-compose.yml
│ ├── group_vars
│ │ ├── all
│ │ ├── SRV1
│ │ ├── SRV2
│ │ ├── SRV3
│ │ ├── SRV4
│ │ └── SRV5
│ ├── inventory.ini
│ ├── playbook.yml
│ └── clear.sh
└── keepass-password-keyfile
│ ├── run.sh
│ ├── ansible.kdbx
│ ├── hosts.ini
│ ├── ansible.keyx
│ └── playbook.yml
├── docs
├── examples
│ ├── example.kdbx
│ ├── README.md
│ ├── group_vars
│ │ └── all
│ └── example-playbook.yml
└── contributing
│ └── README.md
├── LICENSE
├── .gitignore
├── galaxy.yml
├── README.md
└── plugins
├── modules
└── attachment.py
└── lookup
└── keepass.py
/meta/runtime.yml:
--------------------------------------------------------------------------------
1 | requires_ansible: ">=2.10"
2 |
3 |
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | max-line-length = 88
3 | extend-ignore = E203
--------------------------------------------------------------------------------
/tests/keepass-keyfile-only/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | ansible-playbook -i hosts.ini -vvvv playbook.yml
--------------------------------------------------------------------------------
/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/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ansible all -m ping
3 | ansible-playbook playbook.yml -f5
--------------------------------------------------------------------------------
/tests/keepass-password-keyfile/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | ansible-playbook -i hosts.ini -vvvv playbook.yml
--------------------------------------------------------------------------------
/docs/examples/example.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viczem/ansible-keepass/HEAD/docs/examples/example.kdbx
--------------------------------------------------------------------------------
/tests/parallel/ansible.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viczem/ansible-keepass/HEAD/tests/parallel/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-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/ansible.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viczem/ansible-keepass/HEAD/tests/keepass-keyfile-only/ansible.kdbx
--------------------------------------------------------------------------------
/tests/keepass-password-only/ansible.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viczem/ansible-keepass/HEAD/tests/keepass-password-only/ansible.kdbx
--------------------------------------------------------------------------------
/tests/keepass-password-keyfile/ansible.kdbx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/viczem/ansible-keepass/HEAD/tests/keepass-password-keyfile/ansible.kdbx
--------------------------------------------------------------------------------
/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/parallel/docker/.ssh/id_ed25519.pub:
--------------------------------------------------------------------------------
1 | ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIJbTfhu0Q+szM97jE0rDTY7vQOFCv4Q6fDPS+o02pSC0 example@example.com
2 |
--------------------------------------------------------------------------------
/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/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 |
--------------------------------------------------------------------------------
/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`
--------------------------------------------------------------------------------
/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/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/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/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-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/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/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-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-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/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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"]
--------------------------------------------------------------------------------
/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.
--------------------------------------------------------------------------------
/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"
--------------------------------------------------------------------------------
/.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
--------------------------------------------------------------------------------
/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
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/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).
--------------------------------------------------------------------------------
/plugins/modules/attachment.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 | # -*- coding: utf-8 -*-
3 |
4 | # Copyright: (c) 2022, Jimisola Laursen
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 |
--------------------------------------------------------------------------------
/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"(?