diff --git a/NEWS b/NEWS
index 8ffa96d6..dc5fe202 100644
--- a/NEWS
+++ b/NEWS
@@ -1,4 +1,6 @@
2.0.9.dev0
+ * #1123: Add loading of systemd credentials even when running borgmatic outside of a systemd
+ service.
* #1149: Add support for Python 3.14.
* #1149: Include automated tests in the source dist tarball uploaded to PyPI.
diff --git a/borgmatic/config/schema.yaml b/borgmatic/config/schema.yaml
index 36da3d5e..f65b2f96 100644
--- a/borgmatic/config/schema.yaml
+++ b/borgmatic/config/schema.yaml
@@ -3002,6 +3002,25 @@ properties:
description: |
Configuration for integration with Linux LVM (Logical Volume
Manager).
+ systemd:
+ type: object
+ additionalProperties: false
+ properties:
+ systemd_creds_command:
+ type: string
+ description: |
+ Command to use instead of "systemd-creds". Only used as a
+ fallback when borgmatic is run outside of a systemd service.
+ example: /usr/local/bin/systemd-creds
+ encrypted_credentials_directory:
+ type: string
+ description: |
+ Directory containing encrypted credentials for
+ "systemd-creds" to use instead of
+ "/etc/credstore.encrypted".
+ example: /path/to/credstore.encrypted
+ description: |
+ Configuration for integration with systemd credentials.
container:
type: object
additionalProperties: false
diff --git a/borgmatic/hooks/credential/systemd.py b/borgmatic/hooks/credential/systemd.py
index 4a9dd9ed..7213f396 100644
--- a/borgmatic/hooks/credential/systemd.py
+++ b/borgmatic/hooks/credential/systemd.py
@@ -1,6 +1,9 @@
import logging
import os
import re
+import shlex
+
+import borgmatic.execute
logger = logging.getLogger(__name__)
@@ -24,15 +27,28 @@ def load_credential(hook_config, config, credential_parameters):
raise ValueError(f'Cannot load invalid credential name: "{name}"')
+ if not CREDENTIAL_NAME_PATTERN.match(credential_name):
+ raise ValueError(f'Cannot load invalid credential name "{credential_name}"')
+
credentials_directory = os.environ.get('CREDENTIALS_DIRECTORY')
if not credentials_directory:
- raise ValueError(
- f'Cannot load credential "{credential_name}" because the systemd CREDENTIALS_DIRECTORY environment variable is not set',
+ logger.debug(
+ f'Falling back to loading credential "{credential_name}" via systemd-creds because the systemd CREDENTIALS_DIRECTORY environment variable is not set'
)
- if not CREDENTIAL_NAME_PATTERN.match(credential_name):
- raise ValueError(f'Cannot load invalid credential name "{credential_name}"')
+ command = (
+ *shlex.split((hook_config or {}).get('systemd_creds_command', 'systemd-creds')),
+ 'decrypt',
+ os.path.join(
+ (hook_config or {}).get(
+ 'encrypted_credentials_directory', '/etc/credstore.encrypted'
+ ),
+ credential_name,
+ ),
+ )
+
+ return borgmatic.execute.execute_command_and_capture_output(command).rstrip(os.linesep)
try:
with open(
diff --git a/docs/how-to/provide-your-passwords.md b/docs/how-to/provide-your-passwords.md
index 7ce3c8c3..2350a1b2 100644
--- a/docs/how-to/provide-your-passwords.md
+++ b/docs/how-to/provide-your-passwords.md
@@ -127,15 +127,26 @@ encryption_passcommand: cat ${CREDENTIALS_DIRECTORY}/borgmatic_backupserver1
Adjust `borgmatic_backupserver1` according to the name of the credential and the
directory set in the service file.
-Be aware that when using this systemd `{credential ...}` feature, you may no
-longer be able to run certain borgmatic actions outside of the systemd service,
-as the credentials are only available from within the context of that service.
-So for instance, `borgmatic list` necessarily relies on the
-`encryption_passphrase` in order to access the Borg repository, but `list`
-shouldn't need to load any credentials for your database or monitoring hooks.
+New in version 2.0.9 When using
+the systemd `{credential ...}` feature, borgmatic loads systemd credentials even
+when run outside of a systemd service. This works by falling back to calling
+`systemd-creds decrypt` instead of reading credentials directly. To customize
+this behavior, you can override the `systemd-creds` command and/or the
+credential store directory it uses:
-The one exception is `borgmatic config validate`, which doesn't actually load
-any credentials and should continue working anywhere.
+```yaml
+systemd:
+ systemd_creds_command: /usr/local/bin/systemd-creds
+ encrypted_credentials_directory: /path/to/credstore.encrypted
+```
+
+Prior to version 2.0.9 The
+systemd `{credential ...}` feature did not work when run outside of a systemd
+service. But depending on the borgmatic action invoked and the configuration
+option where `{credential ...}` was used, you could sometimes get away without
+working systemd credentials for certain actions. For instance, `borgmatic list`
+doesn't connect to any databases or monitoring services, and `borgmatic config
+validate` doesn't use credentials as all.
### Container secrets
diff --git a/tests/unit/hooks/credential/test_systemd.py b/tests/unit/hooks/credential/test_systemd.py
index 80cb3a33..71dbbb9b 100644
--- a/tests/unit/hooks/credential/test_systemd.py
+++ b/tests/unit/hooks/credential/test_systemd.py
@@ -19,13 +19,60 @@ def test_load_credential_with_invalid_credential_parameters_raises(credential_pa
)
-def test_load_credential_without_credentials_directory_raises():
+def test_load_credential_without_credentials_directory_falls_back_to_systemd_creds_command():
flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
None,
)
+ flexmock(module.borgmatic.execute).should_receive(
+ 'execute_command_and_capture_output'
+ ).with_args(('systemd-creds', 'decrypt', '/etc/credstore.encrypted/mycredential')).and_return(
+ 'password'
+ ).once()
- with pytest.raises(ValueError):
+ assert (
module.load_credential(hook_config={}, config={}, credential_parameters=('mycredential',))
+ == 'password'
+ )
+
+
+def test_load_credential_without_credentials_directory_calls_custom_systemd_creds_command():
+ flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
+ None,
+ )
+ flexmock(module.borgmatic.execute).should_receive(
+ 'execute_command_and_capture_output'
+ ).with_args(
+ ('/path/to/systemd-creds', '--flag', 'decrypt', '/etc/credstore.encrypted/mycredential')
+ ).and_return('password').once()
+
+ assert (
+ module.load_credential(
+ hook_config={'systemd_creds_command': '/path/to/systemd-creds --flag'},
+ config={},
+ credential_parameters=('mycredential',),
+ )
+ == 'password'
+ )
+
+
+def test_load_credential_without_credentials_directory_uses_custom_encrypted_credentials_directory():
+ flexmock(module.os.environ).should_receive('get').with_args('CREDENTIALS_DIRECTORY').and_return(
+ None,
+ )
+ flexmock(module.borgmatic.execute).should_receive(
+ 'execute_command_and_capture_output'
+ ).with_args(('systemd-creds', 'decrypt', '/my/credstore.encrypted/mycredential')).and_return(
+ 'password'
+ ).once()
+
+ assert (
+ module.load_credential(
+ hook_config={'encrypted_credentials_directory': '/my/credstore.encrypted'},
+ config={},
+ credential_parameters=('mycredential',),
+ )
+ == 'password'
+ )
def test_load_credential_with_invalid_credential_name_raises():