Jelajahi Sumber

Add loading of systemd credentials even when running borgmatic outside of a systemd service (#1123).

Dan Helfman 2 bulan lalu
induk
melakukan
4f0162d5f2

+ 2 - 0
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.
 

+ 19 - 0
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

+ 20 - 4
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(

+ 20 - 9
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.
-
-The one exception is `borgmatic config validate`, which doesn't actually load
-any credentials and should continue working anywhere.
+<span class="minilink minilink-addedin">New in version 2.0.9</span> 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:
+
+```yaml
+systemd:
+    systemd_creds_command: /usr/local/bin/systemd-creds
+    encrypted_credentials_directory: /path/to/credstore.encrypted
+```
+
+<span class="minilink minilink-addedin">Prior to version 2.0.9</span> 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

+ 49 - 2
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():