mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-08-19 12:36:06 +00:00
backups: Use SSH key instead of password
- After copying the SSH client public key to the remote host, replace the SSH password credential with keyfile. - Also use SSH key when checking that remote directory exists. Tests: - Add remote backup location "tester@localhost:~backups". Test various operations like create backup, download backup, unmount and mount. Confirm that SSH password is no longer present in plinth sqlite database. Signed-off-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
f689e1b3cf
commit
3558a26b2f
@ -4,7 +4,7 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
from pathlib import Path
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
@ -132,14 +132,21 @@ def restore_from_upload(path, app_ids=None):
|
||||
create_subvolume=False, backup_file=path)
|
||||
|
||||
|
||||
def get_known_hosts_path():
|
||||
def get_known_hosts_path() -> Path:
|
||||
"""Return the path to the known hosts file."""
|
||||
return pathlib.Path(cfg.data_dir) / '.ssh' / 'known_hosts'
|
||||
return Path(cfg.data_dir) / '.ssh' / 'known_hosts'
|
||||
|
||||
|
||||
def get_ssh_client_auth_key_paths() -> tuple[Path, Path]:
|
||||
"""Return the paths to the SSH client public key and private key."""
|
||||
key_path = Path(cfg.data_dir) / '.ssh' / 'id_ed25519'
|
||||
pubkey_path = key_path.with_suffix('.pub')
|
||||
return pubkey_path, key_path
|
||||
|
||||
|
||||
def generate_ssh_client_auth_key():
|
||||
"""Generate SSH client authentication keypair, if needed."""
|
||||
key_path = pathlib.Path(cfg.data_dir) / '.ssh' / 'id_ed25519'
|
||||
_, key_path = get_ssh_client_auth_key_paths()
|
||||
if not key_path.exists():
|
||||
logger.info('Generating SSH client key %s for FreedomBox service',
|
||||
key_path)
|
||||
@ -153,7 +160,7 @@ def generate_ssh_client_auth_key():
|
||||
|
||||
def get_ssh_client_public_key() -> str:
|
||||
"""Get SSH client public key for FreedomBox service."""
|
||||
pubkey_path = pathlib.Path(cfg.data_dir) / '.ssh' / 'id_ed25519.pub'
|
||||
pubkey_path, _ = get_ssh_client_auth_key_paths()
|
||||
with pubkey_path.open('r') as pubkey_file:
|
||||
pubkey = pubkey_file.read()
|
||||
|
||||
@ -166,7 +173,7 @@ def copy_ssh_client_public_key(hostname: str, username: str,
|
||||
|
||||
Returns whether the copy was successful, and any error message.
|
||||
"""
|
||||
pubkey_path = pathlib.Path(cfg.data_dir) / '.ssh' / 'id_ed25519.pub'
|
||||
pubkey_path, _ = get_ssh_client_auth_key_paths()
|
||||
env = os.environ.copy()
|
||||
env['SSHPASS'] = password
|
||||
process = subprocess.run([
|
||||
|
||||
@ -13,7 +13,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from plinth import cfg
|
||||
from plinth.utils import format_lazy
|
||||
|
||||
from . import (_backup_handler, api, errors, get_known_hosts_path, privileged,
|
||||
from . import (_backup_handler, api, errors, get_known_hosts_path,
|
||||
get_ssh_client_auth_key_paths, privileged,
|
||||
restore_archive_handler, split_path, store)
|
||||
from .schedule import Schedule
|
||||
|
||||
@ -344,6 +345,11 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
"""Return whether remote path is mounted locally."""
|
||||
return privileged.is_mounted(self._mountpoint)
|
||||
|
||||
def replace_ssh_password_with_keyfile(self, keyfile_path: str):
|
||||
"""Add SSH keyfile credential and delete stored password."""
|
||||
self.credentials['ssh_keyfile'] = keyfile_path
|
||||
self.credentials.pop('ssh_password', None)
|
||||
|
||||
def initialize(self):
|
||||
"""Initialize the repository after mounting the target directory."""
|
||||
self._ensure_remote_directory()
|
||||
@ -405,16 +411,14 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
if dir_path[0] == '~':
|
||||
dir_path = '.' + dir_path[1:]
|
||||
|
||||
password = self.credentials['ssh_password']
|
||||
|
||||
# Ensure remote directory exists, check contents
|
||||
env = {'SSHPASS': password}
|
||||
_, key_path = get_ssh_client_auth_key_paths()
|
||||
known_hosts_path = str(get_known_hosts_path())
|
||||
subprocess.run([
|
||||
'sshpass', '-e', 'ssh', '-o',
|
||||
f'UserKnownHostsFile={known_hosts_path}', f'{username}@{hostname}',
|
||||
'mkdir', '-p', dir_path
|
||||
], check=True, env=env)
|
||||
'ssh', '-i',
|
||||
str(key_path), '-o', f'UserKnownHostsFile={known_hosts_path}',
|
||||
f'{username}@{hostname}', 'mkdir', '-p', dir_path
|
||||
], check=True)
|
||||
|
||||
|
||||
def get_repositories():
|
||||
|
||||
@ -26,7 +26,8 @@ from plinth.views import AppView
|
||||
|
||||
from . import (SESSION_PATH_VARIABLE, api, copy_ssh_client_public_key, errors,
|
||||
forms, generate_ssh_client_auth_key, get_known_hosts_path,
|
||||
get_ssh_client_public_key, is_ssh_hostkey_verified, privileged)
|
||||
get_ssh_client_auth_key_paths, get_ssh_client_public_key,
|
||||
is_ssh_hostkey_verified, privileged)
|
||||
from .decorators import delete_tmp_backup_file
|
||||
from .repository import (BorgRepository, SshBorgRepository, get_instance,
|
||||
get_repositories)
|
||||
@ -446,11 +447,15 @@ class VerifySshHostkeyView(FormView):
|
||||
logger.info(
|
||||
"Copied SSH client public key to remote host's authorized "
|
||||
"keys.")
|
||||
_pubkey_path, key_path = get_ssh_client_auth_key_paths()
|
||||
repo.replace_ssh_password_with_keyfile(str(key_path))
|
||||
if _save_repository(self.request, repo):
|
||||
return redirect(reverse_lazy('backups:index'))
|
||||
else:
|
||||
logger.warning('Failed to copy SSH client public key: %s', message)
|
||||
messages.error(self.request, message)
|
||||
messages.error(
|
||||
self.request,
|
||||
_('Failed to copy SSH client public key: %s') % message)
|
||||
# Remove the repository so that the user can have another go at
|
||||
# creating it.
|
||||
try:
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user