Update the KeePassXC credential hook to support KeePassXC's secret service integration.

This commit is contained in:
Dan Helfman
2026-04-29 12:52:27 -07:00
parent 0e1659bd73
commit a508cabe3f
5 changed files with 110 additions and 7 deletions
+3
View File
@@ -3,6 +3,9 @@
"working_directory".
* #1301: Expand the "patterns_from" and "exclude_from" options to support paths containing tildes
and globs.
* Update the KeePassXC credential hook to support KeePassXC's secret service integration. See the
documentation for more information:
https://torsion.org/borgmatic/reference/configuration/credentials/keepassxc/
* Add the NEWS changelog file to release tarball (#1298).
* Enable reply by email on projects.torsion.org, so replies to notification emails get posted as
comments on tickets.
+10 -2
View File
@@ -3309,24 +3309,32 @@ properties:
description: |
Command to use instead of "keepassxc-cli".
example: /usr/local/bin/keepassxc-cli
secret_tool_command:
type: string
description: |
Command to use instead of "secret-tool".
example: /usr/local/bin/secret-tool
ask_for_password:
type: boolean
description: |
Whether keepassxc-cli should prompt the user for a password.
Disabling this is only really useful if you're unlocking
your KeePassXC database with a key file instead of a
password. Defaults to true.
password. Ignored when using KeePassXC's secret service
integration. Defaults to true.
example: false
key_file:
type: string
description: |
Path to a key file for unlocking the KeePassXC database.
Ignored when using KeePassXC's secret service integration.
example: /path/to/keyfile
yubikey:
type: string
description: |
YubiKey slot and optional serial number used to access the
KeePassXC database. The format is "<slot[:serial]>", where:
KeePassXC database. Ignored when using KeePassXC's secret
service integration. The format is "<slot[:serial]>", where:
* <slot> is the YubiKey slot number (e.g., `1` or `2`).
* <serial> (optional) is the YubiKey's serial number (e.g.,
`7370001`).
+17 -2
View File
@@ -7,19 +7,34 @@ import borgmatic.execute
logger = logging.getLogger(__name__)
SECRET_SERVICE_DATABASE_PATH = 'secret-service'
def load_credential(hook_config, config, credential_parameters):
'''
Given the hook configuration dict, the configuration dict, and a credential parameters tuple
containing a KeePassXC database path and an attribute name to load, run keepassxc-cli to fetch
the corresponding KeePassXC credential and return it.
the corresponding KeePassXC credential and return it. Or use secret-tool if the database path
is "secret-service", indicating that KeePassXC's secret service integration should be used
instead.
Raise ValueError if keepassxc-cli can't retrieve the credential.
Raise ValueError if keepassxc-cli or secret-tool can't retrieve the credential.
'''
try:
(database_path, attribute_name) = credential_parameters
except ValueError:
raise ValueError(f'Invalid KeePassXC credential parameters: {credential_parameters}')
if database_path == SECRET_SERVICE_DATABASE_PATH:
command = (
*shlex.split((hook_config or {}).get('secret_tool_command', 'secret-tool')),
*('lookup', 'Path', attribute_name),
)
return '\n'.join(borgmatic.execute.execute_command_and_capture_output(command)).rstrip(
os.linesep
)
expanded_database_path = os.path.expanduser(database_path)
if not os.path.exists(expanded_database_path):
@@ -43,14 +43,15 @@ postgresql_databases:
```
### Custom command
### Custom commands
You can also optionally override the `keepassxc-cli` command that borgmatic calls to load
passwords:
You can also optionally override the `keepassxc-cli` or `secret-tool` commands
that borgmatic call to load passwords:
```yaml
keepassxc:
keepassxc_cli_command: /usr/local/bin/keepassxc-cli
secret_tool_command: /usr/local/bin/secret-tool
```
Another example:
@@ -98,3 +99,30 @@ keepassxc:
The value here is the YubiKey slot number (e.g., `1` or `2`) and optional serial
number (e.g., `7370001`) used to access the KeePassXC database. Join the two
values with a colon, but omit the colon if you're leaving out the serial number.
### Secret service integration
<span class="minilink minilink-addedin">New in version 2.1.6</span> borgmatic
supports [KeePassXC's secret service
integration](https://keepassxc.org/docs/KeePassXC_UserGuide#_secret_service_integration)
that integrates with the [freedesktop secret service
API](https://specifications.freedesktop.org/secret-service/latest/) and allows
clients like borgmatic to access your passwords.
To use this feature from borgmatic, specify `secret-service` instead of a
KeePassXC database path when calling this credential hook. For instance:
```yaml
encryption_passphrase: "{credential keepassxc secret-service borgmatic}"
```
With this in place, borgmatic runs libsecret's `secret-tool` command to retrieve
the password titled "borgmatic" on demand. KeePassXC may then prompt you to
approve the password access, depending on how you've configured it.
One benefit of using the KeePassXC's secret service integration like this is
that you don't have to type a KeePassXC database passhprase (or use a keyfile)
whenever borgmatic accesses your passwords. Instead, you can configure
KeePassXC to prompt you to approve or deny each access. Or you can even
pre-approve password access to support automated borgmatic runs.
@@ -31,6 +31,55 @@ def test_load_credential_with_missing_database_raises():
)
def test_load_credential_with_secret_service_database_path_fetches_password_via_secret_tool():
flexmock(module.os.path).should_receive('expanduser').never()
flexmock(module.os.path).should_receive('exists').never()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output',
).with_args(
(
'secret-tool',
'lookup',
'Path',
'mypassword',
),
).and_yield('password').once()
assert (
module.load_credential(
hook_config={},
config={},
credential_parameters=('secret-service', 'mypassword'),
)
== 'password'
)
def test_load_credential_with_secret_service_database_path_and_secret_tool_command_calls_it():
flexmock(module.os.path).should_receive('expanduser').never()
flexmock(module.os.path).should_receive('exists').never()
flexmock(module.borgmatic.execute).should_receive(
'execute_command_and_capture_output',
).with_args(
(
'/usr/local/bin/secret-tool',
'--some-option',
'lookup',
'Path',
'mypassword',
),
).and_yield('password').once()
assert (
module.load_credential(
hook_config={'secret_tool_command': '/usr/local/bin/secret-tool --some-option'},
config={},
credential_parameters=('secret-service', 'mypassword'),
)
== 'password'
)
def test_load_credential_with_present_database_fetches_password_from_keepassxc():
flexmock(module.os.path).should_receive('expanduser').with_args('database.kdbx').and_return(
'database.kdbx',