challenge from passcommand can end up hidden #961

Closed
opened 2024-12-30 17:27:12 +00:00 by markzabaro · 8 comments

What I'm trying to do and why

I'm just starting with borg and borgmatic. The setup I'm trying is local borgmatic -> remote authorized key with borg serve as forced-command -> remote borg repo, set up with repokey encryption.

I'm trying to use encryption_passcommand, pointed at my password manager, because i feel uneasy about leaving my repo passphrase around (in plaintext) in env vars or files on disk, in terms of defense-in-depth/lateral movement risk. I'm not sure how justified that feeling is (putting a repokey passphrase behind a different password at best adds an extra hurdle to someone who's only compromised my server), nor how practical this approach is (requiring interactive passcommand is at odds with the usual endgame of automating backups). I may well have an X-Y problem.

I'm using borg list/borgmatic repo-list -a '*' as my roundtrip test for remote borg and remote borgmatic. borgmatic repo-list and borgmatic repo-list -a '*' hang indefinitely, without any prompt for action from me. If I enter the password to my password manager, the command continues and gives me the list of archives in the repo.

My impression is that it is trying to get my repo passphrase for a 2nd time (though this invocation already had it), and not showing the password prompt from my password manager.

Steps to reproduce

Given the configs below, the test command is:

borgmatic -v 2 repo-list -a '*'

my borgmatic config, at /home/localuser/.config/borgmatic/config.yaml

source_directories:
    - /local/abspath/needing/backups
repositories:
    - path: 'ssh://remoteusername@remotehost:22/remote/abspath/to/repo'
      label: nas
remote_path: /usr/local/bin/borg
exclude_patterns:
    - '/local/abspath/needing/backups/.something'
encryption_passcommand: 'keepassxc-cli show -s -a Password /local/abspath/to/passwords.kdbx "repo identifier"'
ssh_command: 'ssh -o"PasswordAuthentication no" -i /home/localuser/.ssh/somedirectory/id_ed25519'
archive_name_format: '{hostname}-{now:%Y-%m-%d}'
keep_daily: 7
keep_weekly: 4
keep_monthly: 6

locally:

borgmatic --version
1.9.5

borg --version
borg 1.4.0

remote:

borg --version
borg 1.4.0

cat ~/.ssh/authorized_keys
restrict,command="/usr/local/bin/borg serve --append-only --restrict-to-path /remote/abspath/to/repo" ssh-ed25519 hunter2 

Actual behavior

The command hangs indefinitely:

borgmatic -v 2 repo-list -a '*'
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg --version --debug --show-rc
/home/localuser/.config/borgmatic/config.yaml: Borg 1.4.0
nas: Running actions for repository
/home/localuser/.config/borgmatic/config.yaml: No commands to run for pre-actions hook
nas: Listing repository
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo
Enter password to unlock /local/abspath/to/passwords.kdbx: 
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo
using builtin fallback logging configuration
33 self tests completed in 0.10 seconds
SSH command line: ['ssh', '-oPasswordAuthentication no', '-i', '/home/localuser/.ssh/somedirectory/id_ed25519', '-p', '22', 'remoteusername@remotehost', '/usr/local/bin/borg', 'serve', '--debug']
Remote: using builtin fallback logging configuration
Remote: 33 self tests completed in 0.14 seconds
Remote: using builtin fallback logging configuration
Remote: Initialized logging system for JSON-based protocol
Remote: Resolving repository path b'/remote/abspath/to/repo'
Remote: Resolved repository path to '/remote/abspath/to/repo'
Remote: Verified integrity of /remote/abspath/to/repo/index.196

I waited about 3 hours before I started messing with it.

If I type some stuff and press enter, borgmatic dies with stacktrace.

borgmatic -v 2 repo-list -a '*'
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg --version --debug --show-rc
/home/localuser/.config/borgmatic/config.yaml: Borg 1.4.0
nas: Running actions for repository
/home/localuser/.config/borgmatic/config.yaml: No commands to run for pre-actions hook
nas: Listing repository
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo
Enter password to unlock /local/abspath/to/passwords.kdbx: 
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo
using builtin fallback logging configuration
33 self tests completed in 0.10 seconds
SSH command line: ['ssh', '-oPasswordAuthentication no', '-i', '/home/localuser/.ssh/somedirectory/id_ed25519', '-p', '22', 'remoteusername@remotehost', '/usr/local/bin/borg', 'serve', '--debug']
Remote: using builtin fallback logging configuration
Remote: 33 self tests completed in 0.14 seconds
Remote: using builtin fallback logging configuration
Remote: Initialized logging system for JSON-based protocol
Remote: Resolving repository path b'/remote/abspath/to/repo'
Remote: Resolved repository path to '/remote/abspath/to/repo'
Remote: Verified integrity of /remote/abspath/to/repo/index.196
Enter password to unlock /local/abspath/to/passwords.kdbx:
Error while reading the database: Invalid credentials were provided, please try again.
If this reoccurs, then your database file may be corrupt. (HMAC mismatch)
RemoteRepository: 230 B bytes sent, 2.27 kB bytes received, 5 messages sent
passcommand supplied in BORG_PASSCOMMAND failed: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1.
Traceback (most recent call last):
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 524, in env_passcommand
    passphrase = subprocess.check_output(shlex.split(passcommand), text=True, env=env)
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/subprocess.py", line 466, in check_output
    return run(*popenargs, stdout=PIPE, timeout=timeout, check=True,
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/lib/python3.12/subprocess.py", line 571, in run
    raise CalledProcessError(retcode, process.args,
subprocess.CalledProcessError: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1.
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5391, in main
    exit_code = archiver.run(args)
                ^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5309, in run
    rc = func(args)
         ^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 178, in wrapper
    kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility)
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/helpers/manifest.py", line 190, in load
    key = key_factory(repository, cdata)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 165, in key_factory
    return identify_key(manifest_data).detect(repository, manifest_data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 668, in detect
    passphrase = Passphrase.env_passphrase()
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 510, in env_passphrase
    passphrase = cls.env_passcommand()
                 ^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 526, in env_passcommand
    raise PasscommandFailure(e)
borg.crypto.key.PasscommandFailure: passcommand supplied in BORG_PASSCOMMAND failed: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1.
nas: Error running actions for repository
Command 'borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo' returned non-zero exit status 51.
/home/localuser/.config/borgmatic/config.yaml: An error occurred

summary:
/home/localuser/.config/borgmatic/config.yaml: Loading configuration file
/home/localuser/.config/borgmatic/config.yaml: An error occurred
nas: Error running actions for repository
...
Traceback (most recent call last):
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5391, in main
    exit_code = archiver.run(args)
                ^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5309, in run
    rc = func(args)
         ^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 178, in wrapper
    kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility)
                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/helpers/manifest.py", line 190, in load
    key = key_factory(repository, cdata)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 165, in key_factory
    return identify_key(manifest_data).detect(repository, manifest_data)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 668, in detect
    passphrase = Passphrase.env_passphrase()
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 510, in env_passphrase
    passphrase = cls.env_passcommand()
                 ^^^^^^^^^^^^^^^^^^^^^
  File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 526, in env_passcommand
    raise PasscommandFailure(e)
borg.crypto.key.PasscommandFailure: passcommand supplied in BORG_PASSCOMMAND failed: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1.
Command 'borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo' returned non-zero exit status 51.

Need some help? https://torsion.org/borgmatic/#issues

You can see in the above that the first line after where it hangs is my password manager asking for a password.

If I re-run, entering the password for my password database, the command completes normally, listing archives in the repo:

borgmatic -v 2 repo-list -a '*'
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg --version --debug --show-rc
/home/localuser/.config/borgmatic/config.yaml: Borg 1.4.0
nas: Running actions for repository
/home/localuser/.config/borgmatic/config.yaml: No commands to run for pre-actions hook
nas: Listing repository
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo
Enter password to unlock /local/abspath/to/passwords.kdbx: 
BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo
using builtin fallback logging configuration
33 self tests completed in 0.11 seconds
SSH command line: ['ssh', '-oPasswordAuthentication no', '-i', '/home/localuser/.ssh/somedirectory/id_ed25519', '-p', '22', 'remoteusername@remotehost', '/usr/local/bin/borg', 'serve', '--debug']
Remote: using builtin fallback logging configuration
Remote: 33 self tests completed in 0.14 seconds
Remote: using builtin fallback logging configuration
Remote: Initialized logging system for JSON-based protocol
Remote: Resolving repository path b'/remote/abspath/to/repo'
Remote: Resolved repository path to '/remote/abspath/to/repo'
Remote: Verified integrity of /remote/abspath/to/repo/index.196
Enter password to unlock /local/abspath/to/passwords.kdbx:
TAM-verified manifest
security: read previous location 'ssh://remoteusername@remotehost:22/remote/abspath/to/repo'
security: read manifest timestamp '2024-12-29T19:22:06.768658'
security: determined newest manifest timestamp as 2024-12-29T19:22:06.768658
security: repository checks ok, allowing access
test                                 Sat, 2024-12-28 20:22:50 [1faa64bce9f3041b6db2f2f41b503c5b67c647ee57fb2026d60246bca3f29933]
test2                                Sat, 2024-12-28 20:25:20 [08f5fccd944676ac868e113e74cb4bebf108d88786559c39847de53ec1f04547]
personal-20241224                    Sun, 2024-12-29 14:37:57 [1347c5e160484c81c51b20caa921c4bb3e377f72857001c5b5ef43bafbb4e2aa]
localhostname-2024-12-29              Sun, 2024-12-29 20:15:02 [e1c83a0ee9ddd5c76f9a8331e57e728b3eb105952d2e604e5924b2296418d053]
RemoteRepository: 230 B bytes sent, 2.75 kB bytes received, 5 messages sent
terminating with success status, rc 0
/home/localuser/.config/borgmatic/config.yaml: No commands to run for post-actions hook

summary:
/home/localuser/.config/borgmatic/config.yaml: Loading configuration file
/home/localuser/.config/borgmatic/config.yaml: Successfully ran configuration file

The above plaintext doesn't show terminal colors, but the colors of the two password prompts are different:

TEAL  : BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo
WHITE : Enter password to unlock /local/abspath/to/passwords.kdbx: 
TEAL  : BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo

but:

PURPLE: Remote: Verified integrity of /remote/abspath/to/repo/index.196
PURPLE: Enter password to unlock /local/abspath/to/passwords.kdbx:
PURPLE: TAM-verified manifest

That color difference is more noticeable with verbosity off:

borgmatic repo-list -a '*'
PURPLE: nas: Listing repository
WHITE : Enter password to unlock /local/abspath/to/passwords.kdbx: 
- - - - - - >8 where it waits 8< - - - - - - -
PURPLE: Enter password to unlock /local/abspath/to/passwords.kdbx:
PURPLE: test                                 Sat, 2024-12-28 20:22:50 [1faa64bce9f3041b6db2f2f41b503c5b67c647ee57fb2026d60246bca3f29933]
PURPLE: test2                                Sat, 2024-12-28 20:25:20 [08f5fccd944676ac868e113e74cb4bebf108d88786559c39847de53ec1f04547]
PURPLE: personal-20241224                    Sun, 2024-12-29 14:37:57 [1347c5e160484c81c51b20caa921c4bb3e377f72857001c5b5ef43bafbb4e2aa]
PURPLE: localhostname-2024-12-29             Sun, 2024-12-29 20:15:02 [e1c83a0ee9ddd5c76f9a8331e57e728b3eb105952d2e604e5924b2296418d053]

Expected behavior

The password challenge issued from my (local) password manager executable:

  • (1) should be printed,
  • (2) preferably in the relevant color/always the same color

Ideally, the borgmatic invocation would reuse the repo passphrase it's already retrieved and not need to issue a 2nd challenge. But I'm not sure if it's supposed to work this way.

Other notes / implementation ideas

No response

borgmatic version

1.9.5

borgmatic installation method

pip install

Borg version

borg 1.4.0

Python version

Python 3.12.3

Database version (if applicable)

n/a

Operating system and version

Ubuntu 24.04.1 LTS

### What I'm trying to do and why I'm just starting with borg and borgmatic. The setup I'm trying is local borgmatic -> remote authorized key with `borg serve` as forced-command -> remote borg repo, set up with repokey encryption. I'm trying to use `encryption_passcommand`, pointed at my password manager, because i feel uneasy about leaving my repo passphrase around (in plaintext) in env vars or files on disk, in terms of defense-in-depth/lateral movement risk. I'm not sure how justified that feeling is (putting a repokey passphrase behind a different password at best adds an extra hurdle to someone who's only compromised my server), nor how practical this approach is (requiring interactive passcommand is at odds with the usual endgame of automating backups). I may well have an X-Y problem. I'm using `borg list`/`borgmatic repo-list -a '*'` as my roundtrip test for remote borg and remote borgmatic. `borgmatic repo-list` and `borgmatic repo-list -a '*'` hang indefinitely, without any prompt for action from me. If I enter the password to my password manager, the command continues and gives me the list of archives in the repo. My impression is that it is trying to get my repo passphrase for a 2nd time (though this invocation already had it), and not showing the password prompt from my password manager. ### Steps to reproduce Given the configs below, the test command is: ``` borgmatic -v 2 repo-list -a '*' ``` my borgmatic config, at `/home/localuser/.config/borgmatic/config.yaml` ``` source_directories: - /local/abspath/needing/backups repositories: - path: 'ssh://remoteusername@remotehost:22/remote/abspath/to/repo' label: nas remote_path: /usr/local/bin/borg exclude_patterns: - '/local/abspath/needing/backups/.something' encryption_passcommand: 'keepassxc-cli show -s -a Password /local/abspath/to/passwords.kdbx "repo identifier"' ssh_command: 'ssh -o"PasswordAuthentication no" -i /home/localuser/.ssh/somedirectory/id_ed25519' archive_name_format: '{hostname}-{now:%Y-%m-%d}' keep_daily: 7 keep_weekly: 4 keep_monthly: 6 ``` locally: ``` borgmatic --version 1.9.5 borg --version borg 1.4.0 ``` remote: ``` borg --version borg 1.4.0 cat ~/.ssh/authorized_keys restrict,command="/usr/local/bin/borg serve --append-only --restrict-to-path /remote/abspath/to/repo" ssh-ed25519 hunter2 ``` ### Actual behavior The command hangs indefinitely: ``` borgmatic -v 2 repo-list -a '*' BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg --version --debug --show-rc /home/localuser/.config/borgmatic/config.yaml: Borg 1.4.0 nas: Running actions for repository /home/localuser/.config/borgmatic/config.yaml: No commands to run for pre-actions hook nas: Listing repository BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo Enter password to unlock /local/abspath/to/passwords.kdbx: BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo using builtin fallback logging configuration 33 self tests completed in 0.10 seconds SSH command line: ['ssh', '-oPasswordAuthentication no', '-i', '/home/localuser/.ssh/somedirectory/id_ed25519', '-p', '22', 'remoteusername@remotehost', '/usr/local/bin/borg', 'serve', '--debug'] Remote: using builtin fallback logging configuration Remote: 33 self tests completed in 0.14 seconds Remote: using builtin fallback logging configuration Remote: Initialized logging system for JSON-based protocol Remote: Resolving repository path b'/remote/abspath/to/repo' Remote: Resolved repository path to '/remote/abspath/to/repo' Remote: Verified integrity of /remote/abspath/to/repo/index.196 ``` I waited about 3 hours before I started messing with it. If I type some stuff and press enter, borgmatic dies with stacktrace. ``` borgmatic -v 2 repo-list -a '*' BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg --version --debug --show-rc /home/localuser/.config/borgmatic/config.yaml: Borg 1.4.0 nas: Running actions for repository /home/localuser/.config/borgmatic/config.yaml: No commands to run for pre-actions hook nas: Listing repository BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo Enter password to unlock /local/abspath/to/passwords.kdbx: BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo using builtin fallback logging configuration 33 self tests completed in 0.10 seconds SSH command line: ['ssh', '-oPasswordAuthentication no', '-i', '/home/localuser/.ssh/somedirectory/id_ed25519', '-p', '22', 'remoteusername@remotehost', '/usr/local/bin/borg', 'serve', '--debug'] Remote: using builtin fallback logging configuration Remote: 33 self tests completed in 0.14 seconds Remote: using builtin fallback logging configuration Remote: Initialized logging system for JSON-based protocol Remote: Resolving repository path b'/remote/abspath/to/repo' Remote: Resolved repository path to '/remote/abspath/to/repo' Remote: Verified integrity of /remote/abspath/to/repo/index.196 Enter password to unlock /local/abspath/to/passwords.kdbx: Error while reading the database: Invalid credentials were provided, please try again. If this reoccurs, then your database file may be corrupt. (HMAC mismatch) RemoteRepository: 230 B bytes sent, 2.27 kB bytes received, 5 messages sent passcommand supplied in BORG_PASSCOMMAND failed: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1. Traceback (most recent call last): File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 524, in env_passcommand passphrase = subprocess.check_output(shlex.split(passcommand), text=True, env=env) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/subprocess.py", line 466, in check_output return run(*popenargs, stdout=PIPE, timeout=timeout, check=True, ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/lib/python3.12/subprocess.py", line 571, in run raise CalledProcessError(retcode, process.args, subprocess.CalledProcessError: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1. During handling of the above exception, another exception occurred: Traceback (most recent call last): File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5391, in main exit_code = archiver.run(args) ^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5309, in run rc = func(args) ^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 178, in wrapper kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/helpers/manifest.py", line 190, in load key = key_factory(repository, cdata) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 165, in key_factory return identify_key(manifest_data).detect(repository, manifest_data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 668, in detect passphrase = Passphrase.env_passphrase() ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 510, in env_passphrase passphrase = cls.env_passcommand() ^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 526, in env_passcommand raise PasscommandFailure(e) borg.crypto.key.PasscommandFailure: passcommand supplied in BORG_PASSCOMMAND failed: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1. nas: Error running actions for repository Command 'borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo' returned non-zero exit status 51. /home/localuser/.config/borgmatic/config.yaml: An error occurred summary: /home/localuser/.config/borgmatic/config.yaml: Loading configuration file /home/localuser/.config/borgmatic/config.yaml: An error occurred nas: Error running actions for repository ... Traceback (most recent call last): File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5391, in main exit_code = archiver.run(args) ^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 5309, in run rc = func(args) ^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/archiver.py", line 178, in wrapper kwargs['manifest'], kwargs['key'] = Manifest.load(repository, compatibility) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/helpers/manifest.py", line 190, in load key = key_factory(repository, cdata) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 165, in key_factory return identify_key(manifest_data).detect(repository, manifest_data) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 668, in detect passphrase = Passphrase.env_passphrase() ^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 510, in env_passphrase passphrase = cls.env_passcommand() ^^^^^^^^^^^^^^^^^^^^^ File "/home/localuser/.local/share/pipx/venvs/borgbackup/lib/python3.12/site-packages/borg/crypto/key.py", line 526, in env_passcommand raise PasscommandFailure(e) borg.crypto.key.PasscommandFailure: passcommand supplied in BORG_PASSCOMMAND failed: Command '['keepassxc-cli', 'show', '-s', '-a', 'Password', '/local/abspath/to/passwords.kdbx', 'repo identifier']' returned non-zero exit status 1. Command 'borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo' returned non-zero exit status 51. Need some help? https://torsion.org/borgmatic/#issues ``` You can see in the above that the first line after where it hangs is my password manager asking for a password. If I re-run, entering the password for my password database, the command completes normally, listing archives in the repo: ``` borgmatic -v 2 repo-list -a '*' BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg --version --debug --show-rc /home/localuser/.config/borgmatic/config.yaml: Borg 1.4.0 nas: Running actions for repository /home/localuser/.config/borgmatic/config.yaml: No commands to run for pre-actions hook nas: Listing repository BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo Enter password to unlock /local/abspath/to/passwords.kdbx: BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo using builtin fallback logging configuration 33 self tests completed in 0.11 seconds SSH command line: ['ssh', '-oPasswordAuthentication no', '-i', '/home/localuser/.ssh/somedirectory/id_ed25519', '-p', '22', 'remoteusername@remotehost', '/usr/local/bin/borg', 'serve', '--debug'] Remote: using builtin fallback logging configuration Remote: 33 self tests completed in 0.14 seconds Remote: using builtin fallback logging configuration Remote: Initialized logging system for JSON-based protocol Remote: Resolving repository path b'/remote/abspath/to/repo' Remote: Resolved repository path to '/remote/abspath/to/repo' Remote: Verified integrity of /remote/abspath/to/repo/index.196 Enter password to unlock /local/abspath/to/passwords.kdbx: TAM-verified manifest security: read previous location 'ssh://remoteusername@remotehost:22/remote/abspath/to/repo' security: read manifest timestamp '2024-12-29T19:22:06.768658' security: determined newest manifest timestamp as 2024-12-29T19:22:06.768658 security: repository checks ok, allowing access test Sat, 2024-12-28 20:22:50 [1faa64bce9f3041b6db2f2f41b503c5b67c647ee57fb2026d60246bca3f29933] test2 Sat, 2024-12-28 20:25:20 [08f5fccd944676ac868e113e74cb4bebf108d88786559c39847de53ec1f04547] personal-20241224 Sun, 2024-12-29 14:37:57 [1347c5e160484c81c51b20caa921c4bb3e377f72857001c5b5ef43bafbb4e2aa] localhostname-2024-12-29 Sun, 2024-12-29 20:15:02 [e1c83a0ee9ddd5c76f9a8331e57e728b3eb105952d2e604e5924b2296418d053] RemoteRepository: 230 B bytes sent, 2.75 kB bytes received, 5 messages sent terminating with success status, rc 0 /home/localuser/.config/borgmatic/config.yaml: No commands to run for post-actions hook summary: /home/localuser/.config/borgmatic/config.yaml: Loading configuration file /home/localuser/.config/borgmatic/config.yaml: Successfully ran configuration file ``` The above plaintext doesn't show terminal colors, but the colors of the two password prompts are different: ``` TEAL : BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --remote-path /usr/local/bin/borg --json ssh://remoteusername@remotehost:22/remote/abspath/to/repo WHITE : Enter password to unlock /local/abspath/to/passwords.kdbx: TEAL : BORG_PASSCOMMAND=*** BORG_RSH=*** BORG_RELOCATED_REPO_ACCESS_IS_OK=*** BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=*** BORG_EXIT_CODES=*** borg list --debug --show-rc --remote-path /usr/local/bin/borg ssh://remoteusername@remotehost:22/remote/abspath/to/repo ``` but: ``` PURPLE: Remote: Verified integrity of /remote/abspath/to/repo/index.196 PURPLE: Enter password to unlock /local/abspath/to/passwords.kdbx: PURPLE: TAM-verified manifest ``` That color difference is more noticeable with verbosity off: ``` borgmatic repo-list -a '*' PURPLE: nas: Listing repository WHITE : Enter password to unlock /local/abspath/to/passwords.kdbx: - - - - - - >8 where it waits 8< - - - - - - - PURPLE: Enter password to unlock /local/abspath/to/passwords.kdbx: PURPLE: test Sat, 2024-12-28 20:22:50 [1faa64bce9f3041b6db2f2f41b503c5b67c647ee57fb2026d60246bca3f29933] PURPLE: test2 Sat, 2024-12-28 20:25:20 [08f5fccd944676ac868e113e74cb4bebf108d88786559c39847de53ec1f04547] PURPLE: personal-20241224 Sun, 2024-12-29 14:37:57 [1347c5e160484c81c51b20caa921c4bb3e377f72857001c5b5ef43bafbb4e2aa] PURPLE: localhostname-2024-12-29 Sun, 2024-12-29 20:15:02 [e1c83a0ee9ddd5c76f9a8331e57e728b3eb105952d2e604e5924b2296418d053] ``` ### Expected behavior The password challenge issued from my (local) password manager executable: - (1) should be printed, - (2) preferably in the relevant color/always the same color Ideally, the borgmatic invocation would reuse the repo passphrase it's already retrieved and not need to issue a 2nd challenge. But I'm not sure if it's supposed to work this way. ### Other notes / implementation ideas _No response_ ### borgmatic version 1.9.5 ### borgmatic installation method pip install ### Borg version borg 1.4.0 ### Python version Python 3.12.3 ### Database version (if applicable) n/a ### Operating system and version Ubuntu 24.04.1 LTS
Owner

I'm just starting with borg and borgmatic.

Thanks for giving borgmatic a shot!

I'm not sure how justified that feeling is (putting a repokey passphrase behind a different password at best adds an extra hurdle to someone who's only compromised my server), nor how practical this approach is (requiring interactive passcommand is at odds with the usual endgame of automating backups).

Yeah, it's really a question of the risk level you're comfortable with. And of course the tradeoff between security and convenience.

My impression is that it is trying to get my repo passphrase for a 2nd time (though this invocation already had it), and not showing the password prompt from my password manager.

Any given invocation of a borgmatic action may call Borg multiple times. And in this particular case, it's calling borg repo-list once with --json in order to check for too-aggressive archive flags and once without --json to satisfy the user's ask to actually list the repo. Also, borgmatic doesn't actually do anything with your configured passcommand itself; that gets passed to Borg, which is responsible for calling the passcommand and prompting the user. The call order looks something like this:

  • borgmatic repo-list ...
    • borg repo-list --json with BORG_PASSCOMMAND environment variable
      • Borg calls your configured passcommand
    • borg repo-list with BORG_PASSCOMMAND environment variable
      • Borg calls your configured passcommand

So what I think is going on here with the hang is that borgmatic's second call to borg repo-list is collecting Borg's output, so as to log each line at the appropriate colored log level. And that includes Borg's call to your passcommand. However, a side effect of collecting the output is that anything that doesn't log a complete line—like the interactive prompt of your passcommand—doesn't show up. Which makes it appear that borgmatic is hanging since that prompt is invisible.

One solution I originally thought of here is to disable the output collection for logging when a passcommand is configured. That works for your repo-list case, but not for other use cases. For instance, borgmatic restore for database restoration currently calls borg extract and collects its output directly as part of the streaming database restore. So it wouldn't be possible to disable the output collection when a passcommand is set without breaking database restoration streaming.

Ideally, the borgmatic invocation would reuse the repo passphrase it's already retrieved and not need to issue a 2nd challenge. But I'm not sure if it's supposed to work this way.

This suggests another potential solution. borgmatic could potentially not delegate passcommand invocation to Borg like it does now and instead call the passcommand itself. Then, borgmatic would have to somehow provide the passphrase to each individual invocation of Borg in a secure manner. The natural way would be using an environment variable, but my guess is that anyone who's bothering to use a passcommand probably wouldn't want their plaintext passphrase stuffed unceremoneously into an environment variable. Borg does however support passing in a passphrase by file descriptor, so that may be a sufficiently secure way to provide the passphrase to Borg.

The upshot is the call order in our example would look something like:

  • borgmatic repo-list ...
    • borgmatic calls your configured passcommand to get the passphrase
    • borg repo-list --json with BORG_PASSPHRASE_FD (file descriptor) environment variable
    • borg repo-list with BORG_PASSPHRASE_FD (file descriptor) environment variable

... meaning that there would be: 1. only a single passcommand prompt regardless of how many times borgmatic runs Borg, and 2. the prompt would actually be visible.

Please let me know if you have thoughts on any of this.

> I'm just starting with borg and borgmatic. Thanks for giving borgmatic a shot! > I'm not sure how justified that feeling is (putting a repokey passphrase behind a different password at best adds an extra hurdle to someone who's only compromised my server), nor how practical this approach is (requiring interactive passcommand is at odds with the usual endgame of automating backups). Yeah, it's really a question of the risk level you're comfortable with. And of course the tradeoff between security and convenience. > My impression is that it is trying to get my repo passphrase for a 2nd time (though this invocation already had it), and not showing the password prompt from my password manager. Any given invocation of a borgmatic action may call Borg multiple times. And in this particular case, it's calling `borg repo-list` once with `--json` in order to check for too-aggressive archive flags and once without `--json` to satisfy the user's ask to actually list the repo. Also, borgmatic doesn't actually do anything with your configured passcommand itself; that gets passed to Borg, which is responsible for calling the passcommand and prompting the user. The call order looks something like this: * `borgmatic repo-list ...` * `borg repo-list --json` with `BORG_PASSCOMMAND` environment variable * Borg calls your configured passcommand * `borg repo-list` with `BORG_PASSCOMMAND` environment variable * Borg calls your configured passcommand So what I think is going on here with the hang is that borgmatic's second call to `borg repo-list` is collecting Borg's output, so as to log each line at the appropriate colored log level. And that includes Borg's call to your passcommand. However, a side effect of collecting the output is that anything that doesn't log a complete line—like the interactive prompt of your passcommand—doesn't show up. Which makes it appear that borgmatic is hanging since that prompt is invisible. One solution I originally thought of here is to disable the output collection for logging when a passcommand is configured. That works for your `repo-list` case, but not for other use cases. For instance, `borgmatic restore` for database restoration currently calls `borg extract` and collects its output directly as part of the streaming database restore. So it wouldn't be possible to disable the output collection when a passcommand is set without breaking database restoration streaming. > Ideally, the borgmatic invocation would reuse the repo passphrase it's already retrieved and not need to issue a 2nd challenge. But I'm not sure if it's supposed to work this way. This suggests another potential solution. borgmatic could potentially *not* delegate passcommand invocation to Borg like it does now and instead call the passcommand itself. Then, borgmatic would have to somehow provide the passphrase to each individual invocation of Borg in a secure manner. The natural way would be using an environment variable, but my guess is that anyone who's bothering to use a passcommand probably wouldn't want their plaintext passphrase stuffed unceremoneously into an environment variable. Borg does however support passing in a passphrase by file descriptor, so that may be a sufficiently secure way to provide the passphrase to Borg. The upshot is the call order in our example would look something like: * `borgmatic repo-list ...` * borgmatic calls your configured passcommand to get the passphrase * `borg repo-list --json` with `BORG_PASSPHRASE_FD` (file descriptor) environment variable * `borg repo-list` with `BORG_PASSPHRASE_FD` (file descriptor) environment variable ... meaning that there would be: 1. only a single passcommand prompt regardless of how many times borgmatic runs Borg, and 2. the prompt would actually be visible. Please let me know if you have thoughts on any of this.
witten added the
waiting for response
label 2025-01-18 03:17:41 +00:00
Author

Hi, thanks for the speedy reply (during the holidays), and sorry I didn't reply sooner.

Any given invocation of a borgmatic action may call Borg multiple times. And in this particular case, it's calling borg repo-list once with --json in order to check for too-aggressive archive flags and once without --json to satisfy the user's ask to actually list the repo. Also, borgmatic doesn't actually do anything with your configured passcommand itself; that gets passed to Borg, which is responsible for calling the passcommand and prompting the user.

Ah, makes sense.

So what I think is going on here with the hang is that borgmatic's second call to borg repo-list is collecting Borg's output, so as to log each line at the appropriate colored log level. And that includes Borg's call to your passcommand. However, a side effect of collecting the output is that anything that doesn't log a complete line—like the interactive prompt of your passcommand—doesn't show up. Which makes it appear that borgmatic is hanging since that prompt is invisible.

One solution I originally thought of here is to disable the output collection for logging when a passcommand is configured. That works for your repo-list case, but not for other use cases. [example of how borgmatic restore calls borg extract]

Ohhhkaaaay. Yeah, as a beginner I haven't used/interacted much with the richer functionality, like backing up databases. If I understand correctly, the blocker is more or less that the various data sources don't really produce output/logging/errors in any one way more portable than STDIN/STDERR/exit code, so borgmatic needs to only rely on these, and in so doing it consumes these, whether they are presenting data or prompts. Makes sense.

This suggests another potential solution. borgmatic could potentially not delegate passcommand invocation to Borg like it does now and instead call the passcommand itself. Then, borgmatic would have to somehow provide the passphrase to each individual invocation of Borg in a secure manner. The natural way would be using an environment variable, but my guess is that anyone who's bothering to use a passcommand probably wouldn't want their plaintext passphrase stuffed unceremoneously into an environment variable. Borg does however support passing in a passphrase by file descriptor, so that may be a sufficiently secure way to provide the passphrase to Borg.

The upshot is the call order in our example would look something like:

  • borgmatic repo-list ...
    • borgmatic calls your configured passcommand to get the passphrase
    • borg repo-list --json with BORG_PASSPHRASE_FD (file descriptor) environment variable

This is why it took me so long to reply.... I had seen mention of BORG_PASSPHRASE_FD when getting started, didn't really understand the use case, and shelved it as "I'd like to know but I will never bother to find out.". It took me a bit to google the right terms to develop an impression around it. As a responsible forum user, I'll give a bit of a summary of those notes below (apologies if it's spammy).

I agree with your reasoning, that an env var would raise alarm bells for some folks.

For my use case, the proposal sounds good. As you say, hoisting passcommand execution into borgmatic solves multi-prompting as well as swallowed password prompts.

I couldn't think of any downside. It adds a little bit of complexity to borgmatic, and it's hard to predict how many people would make use of it.


Notes on using file descriptors for ephemeral handling of secrets:

Best TLDR is https://security.stackexchange.com/a/190075 , mainly this quote

Given a choice, there is a very clear order of disminishing risks of data leakage: pipes are safe, environment variables are usually safe if the subprocess doesn't spawn other subprocesses, command line arguments should not be treated as confiential.

Places where these can leak:

commandline arguments:

  • They are always visible to the same user; on some systems, even to other users.
  • e.g., via tools like ps, {,h}top, and /proc/$pid/cmdline
  • command line args are more likely to end up getting logged
    secrets via env var
  • /proc/$pid/environ
  • on my (ubuntu) box, /proc/$pid is dr-xr-xr-x and
    secrets via FD
  • usually private to the process

On my Ubuntu 24.04.1 LTS host:

perl -e 'sleep 60*60;' arg1 arg2 arg3 &

ls -ald /proc/51104{,/{cmdline,environ}}
dr-xr-xr-x 9 myuname mygname 0 Jan 21 14:36 /proc/51104
-r--r--r-- 1 myuname mygname 0 Jan 21 14:36 /proc/51104/cmdline
-r-------- 1 myuname mygname 0 Jan 21 14:36 /proc/51104/environ

cat /proc/65380/cmdline | od -t c
0000000   p   e   r   l  \0   -   e  \0   s   l   e   e   p       1   0
0000020   _   0   0   0   ;  \0   a   r   g   1  \0   a   r   g   2  \0
0000040   a   r   g   3  \0
0000045

https://lackingrhoticity.blogspot.com/2015/05/passing-fds-handles-between-processes.html

In contrast, Unix has no equivalent to [the Windows API] DuplicateHandle(). A Unix process's FD table is private to the process. Consequently, on Unix it is much rarer for a process to have dealings with another process's FD numbers.

There are standard ways for a process to share its FDs with another process over a unix domain socket, with sample code published in one of those big white Stevens books. They need to be initiated by the owning process, and they seem rather fussy https://stackoverflow.com/a/28005250 . Some comments on https://unix.stackexchange.com/a/429028 look informative.

Since 2020 (linux 5.6), the pidfd_getfd syscall exists, for a process to duplicate an FD from another process, but it requires advanced access (PTRACE_MODE_ATTACH_REALCREDS, see https://www.man7.org/linux/man-pages/man2/ptrace.2.html ). sample code at https://stackoverflow.com/a/72135834 .

I took some fun wrong turns that might be related:

  • gpg also allows you to give password via FD, with --passphrase-fd n
  • Thinking about how ephemeral secrets are handled in linuxy platforms, i wondered how ssh-agent does it. Didn't get very far, but https://stackoverflow.com/a/72135834
  • some of the SO threads talk about passwords in memory being vulnerable to dumping with gdb
  • The memfd_secret syscall sounds like a riot:
    • "The memfd_secret() system call is designed to allow a user-space process to create a range of memory that is inaccessible to anybody else - kernel included"
    • "Once a region for a memfd_secret() memory mapping is allocated, the user can't accidentally pass it into the kernel to be transmitted somewhere."
    • "The way memfd_secret() allocates and locks the memory may impact overall system performance, therefore the system call is disabled by default and only available if the system administrator turned it on using "secretmem.enable=y" kernel parameter."
    • "To prevent potential data leaks of memory regions backed by memfd_secret() from a hybernation image, hybernation is prevented when there are active memfd_secret() users."
  • this line of inquiry also leads down a winding path toward memfd_create/fcntl
Hi, thanks for the speedy reply (during the holidays), and sorry I didn't reply sooner. > Any given invocation of a borgmatic action may call Borg multiple times. And in this particular case, it's calling `borg repo-list` once with `--json` in order to check for too-aggressive archive flags and once without `--json` to satisfy the user's ask to actually list the repo. Also, borgmatic doesn't actually do anything with your configured passcommand itself; that gets passed to Borg, which is responsible for calling the passcommand and prompting the user. Ah, makes sense. > So what I think is going on here with the hang is that borgmatic's second call to `borg repo-list` is collecting Borg's output, so as to log each line at the appropriate colored log level. And that includes Borg's call to your passcommand. However, a side effect of collecting the output is that anything that doesn't log a complete line—like the interactive prompt of your passcommand—doesn't show up. Which makes it appear that borgmatic is hanging since that prompt is invisible. > One solution I originally thought of here is to disable the output collection for logging when a passcommand is configured. That works for your `repo-list` case, but not for other use cases. \[example of how `borgmatic restore` calls `borg extract`] Ohhhkaaaay. Yeah, as a beginner I haven't used/interacted much with the richer functionality, like backing up databases. If I understand correctly, the blocker is more or less that the various data sources don't really produce output/logging/errors in any one way more portable than STDIN/STDERR/exit code, so borgmatic needs to only rely on these, and in so doing it consumes these, whether they are presenting data or prompts. Makes sense. > This suggests another potential solution. borgmatic could potentially _not_ delegate passcommand invocation to Borg like it does now and instead call the passcommand itself. Then, borgmatic would have to somehow provide the passphrase to each individual invocation of Borg in a secure manner. The natural way would be using an environment variable, but my guess is that anyone who's bothering to use a passcommand probably wouldn't want their plaintext passphrase stuffed unceremoneously into an environment variable. Borg does however support passing in a passphrase by file descriptor, so that may be a sufficiently secure way to provide the passphrase to Borg. > > The upshot is the call order in our example would look something like: > >- `borgmatic repo-list ...` > - borgmatic calls your configured passcommand to get the passphrase > - `borg repo-list --json` with `BORG_PASSPHRASE_FD` (file descriptor) environment variable This is why it took me so long to reply.... I had seen mention of `BORG_PASSPHRASE_FD` when getting started, didn't really understand the use case, and shelved it as "I'd like to know but I will never bother to find out.". It took me a bit to google the right terms to develop an impression around it. As a responsible forum user, I'll give a bit of a summary of those notes below (apologies if it's spammy). I agree with your reasoning, that an env var would raise alarm bells for some folks. For my use case, the proposal sounds good. As you say, hoisting passcommand execution into `borgmatic` solves multi-prompting as well as swallowed password prompts. I couldn't think of any downside. It adds a little bit of complexity to borgmatic, and it's hard to predict how many people would make use of it. ---- Notes on using file descriptors for ephemeral handling of secrets: Best TLDR is https://security.stackexchange.com/a/190075 , mainly this quote > Given a choice, there is a very clear order of disminishing risks of data leakage: pipes are safe, environment variables are usually safe if the subprocess doesn't spawn other subprocesses, command line arguments should not be treated as confiential. Places where these can leak: commandline arguments: - They are always visible to the same user; on some systems, even to other users. - e.g., via tools like `ps`, `{,h}top`, and `/proc/$pid/cmdline` - command line args are more likely to end up getting logged secrets via env var - `/proc/$pid/environ` - on my (ubuntu) box, /proc/$pid is `dr-xr-xr-x` and secrets via FD - usually private to the process On my Ubuntu 24.04.1 LTS host: ``` perl -e 'sleep 60*60;' arg1 arg2 arg3 & ls -ald /proc/51104{,/{cmdline,environ}} dr-xr-xr-x 9 myuname mygname 0 Jan 21 14:36 /proc/51104 -r--r--r-- 1 myuname mygname 0 Jan 21 14:36 /proc/51104/cmdline -r-------- 1 myuname mygname 0 Jan 21 14:36 /proc/51104/environ cat /proc/65380/cmdline | od -t c 0000000 p e r l \0 - e \0 s l e e p 1 0 0000020 _ 0 0 0 ; \0 a r g 1 \0 a r g 2 \0 0000040 a r g 3 \0 0000045 ``` https://lackingrhoticity.blogspot.com/2015/05/passing-fds-handles-between-processes.html > In contrast, Unix has no equivalent to \[the Windows API] `DuplicateHandle()`. A Unix process's FD table is private to the process. Consequently, on Unix it is much rarer for a process to have dealings with another process's FD numbers. There are standard ways for a process to share its FDs with another process over a unix domain socket, with sample code published in one of those big white Stevens books. They need to be initiated by the owning process, and they seem rather fussy https://stackoverflow.com/a/28005250 . Some comments on https://unix.stackexchange.com/a/429028 look informative. Since 2020 (linux 5.6), the [`pidfd_getfd` syscall](https://man7.org/linux/man-pages/man2/pidfd_getfd.2.html) exists, for a process to duplicate an FD from another process, but it requires advanced access (`PTRACE_MODE_ATTACH_REALCREDS`, see https://www.man7.org/linux/man-pages/man2/ptrace.2.html ). sample code at https://stackoverflow.com/a/72135834 . I took some fun wrong turns that might be related: - `gpg` also allows you to give password via FD, with `--passphrase-fd n` - Thinking about how ephemeral secrets are handled in linuxy platforms, i wondered how ssh-agent does it. Didn't get very far, but https://stackoverflow.com/a/72135834 - some of the SO threads talk about passwords in memory being vulnerable to dumping with `gdb` - The [`memfd_secret`](https://www.man7.org/linux/man-pages//man2/memfd_secret.2.html) syscall sounds like a riot: - "The **memfd_secret**() system call is designed to allow a user-space process to create a range of memory that is inaccessible to anybody else - kernel included" - "Once a region for a memfd_secret() memory mapping is allocated, the user can't accidentally pass it into the kernel to be transmitted somewhere." - "The way memfd_secret() allocates and locks the memory may impact overall system performance, therefore the system call is disabled by default and only available if the system administrator turned it on using "secretmem.enable=y" kernel parameter." - "To prevent potential data leaks of memory regions backed by memfd_secret() from a hybernation image, hybernation is prevented when there are active memfd_secret() users." - this line of inquiry also leads down a winding path toward `memfd_create`/`fcntl`
witten removed the
waiting for response
label 2025-01-24 19:04:40 +00:00
Owner

Thanks for digging into the security aspects of this. I haven't looked into some of the fancier options like memfd_secret(), but my instinct right now is that the anonymous pipe FD option is probably a good balance of convenience and security. Of course, my instinct may be wrong. There's also the consideration that borgmatic users are not all Linux users, as I've discovered! Anyway, I'll consider the resolution of this point an (important) implementation detail of this ticket.

Additional note, mostly for myself or whoever implements this: #966 recently came up with a generalized way to load credentials within borgmatic's config file, which on its face has some vague adjacency to this ticket even if the specific approach is very different. At the risk of over-engineering, it makes me think that it may make sense to introduce the notion of borgmatic credential "providers" or "hooks." For instance, there could be one Borg passphrase provider, prompting for the passphrase and passing it to Borg as described in this ticket. And then there could be another provider for loading systemd credentials instead as described in #966. Additional credential providers/hooks could be added in the future as needed.

Thanks for digging into the security aspects of this. I haven't looked into some of the fancier options like `memfd_secret()`, but my instinct right now is that the anonymous pipe FD option is probably a good balance of convenience and security. Of course, my instinct may be wrong. There's also the consideration that borgmatic users are not all Linux users, as I've discovered! Anyway, I'll consider the resolution of this point an (important) implementation detail of this ticket. Additional note, mostly for myself or whoever implements this: #966 recently came up with a generalized way to load credentials within borgmatic's config file, which on its face has some vague adjacency to this ticket even if the specific approach is very different. At the risk of over-engineering, it makes me think that it *may* make sense to introduce the notion of borgmatic credential "providers" or "hooks." For instance, there could be one Borg passphrase provider, prompting for the passphrase and passing it to Borg as described in this ticket. And then there could be another provider for loading systemd credentials instead as described in #966. Additional credential providers/hooks could be added in the future as needed.
witten referenced this issue from a commit 2025-01-30 17:49:29 +00:00
Owner

I have an initial implementation going! #984

I have an initial implementation going! #984
witten referenced this issue from a commit 2025-01-30 18:20:40 +00:00
Author

Gee whiz that was fast.

Gee whiz that was fast.
Owner

This is implemented in main and will be part of the next release! I've tested it with keepassxc-cli and it seems to work great.. It only prompts for a passphrase to unlock the kdbx file once now even though Borg is called multiple times. And no more apparent hangs. To keep things (relatively) simple, I ended up going with the file descriptor approach for passing the passphrase to Borg when a passcommand is used. We can always revisit in the future if needed.

This is implemented in main and will be part of the next release! I've tested it with `keepassxc-cli` and it seems to work great.. It only prompts for a passphrase to unlock the kdbx file once now even though Borg is called multiple times. And no more apparent hangs. To keep things (relatively) simple, I ended up going with the file descriptor approach for passing the passphrase to Borg when a passcommand is used. We can always revisit in the future if needed.
Owner

Gee whiz that was fast.

That's funny.. From my end it seemed like it took way longer than it should've. 😄

> Gee whiz that was fast. That's funny.. From my end it seemed like it took way longer than it should've. 😄
witten referenced this issue from a commit 2025-01-31 18:27:36 +00:00
Owner

Released in borgmatic 1.9.9!

Released in borgmatic 1.9.9!
Sign in to join this conversation.
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: borgmatic-collective/borgmatic#961
No description provided.