mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
backups: Fix issue with verifying SSH hosts with RSA key
- In current stable and testing, verifying SSH remote hosts using RSA is not working. After selecting the verified RSA fingerprint, paramiko fails to connect - A change introduced in paramiko 2.9 lead to failures when connecting to hosts that have a verified RSA host key[1][2][3]. To fix the issue, disabled_algorithms must be used to drop some of the other algorithms supported by the server to force paramiko behavior. A better solution to the problem was introduced in paramiko 3.2. Both these solutions require careful update to the code. Considering the utility paramiko provides, the regression annoyance, effort required for this fix, and the security implications (it is an completely independent SSH implementation), the library does not seem to be worth the effort in our case. - Switch to using sshpass command line utility instead of paramiko library. The only reason to use paramiko seems that 'ssh' command by default does not allow us to input password easily while paramiko does. - Another place where paramiko is being used is to check if a host is already verified in the known_hosts file. This has been trivially replaced with 'ssh-keygen -F'. - Exit codes provided by sshpass can replace the specific exception raised by paramiko. Links: 1) https://www.paramiko.org/changelog.html 2) https://github.com/paramiko/paramiko/issues/2017 3) https://github.com/paramiko/paramiko/issues/1984 Tests: - Add a remote backup repository with and without encryption. - Add remote backup repository with all three types of algorithms. - Add a remote repository again with wrong password. Authentication error is properly shown. - Add a remote backup repository and remove it. Host remains verified. Add a repository again. - Add a remote backup repository and remove it. Host remains verified. Change the fingerprint the /var/lib/plinth/.ssh/known_hosts file. Add a repository again. A proper error is shown that remote host could not be verified. - Add a remote backup repository and remove it. Host remains verified. Stop SSH server on the remote host. A generic error is shown that ssh command on remote host failed. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
parent
54538ed891
commit
c2007d0f6d
2
debian/control
vendored
2
debian/control
vendored
@ -38,7 +38,6 @@ Build-Depends:
|
||||
python3-markupsafe,
|
||||
python3-mypy,
|
||||
python3-pampy,
|
||||
python3-paramiko,
|
||||
python3-pexpect,
|
||||
python3-pip,
|
||||
python3-psutil,
|
||||
@ -114,7 +113,6 @@ Depends:
|
||||
python3-gi,
|
||||
python3-markupsafe,
|
||||
python3-pampy,
|
||||
python3-paramiko,
|
||||
python3-pexpect,
|
||||
python3-psutil,
|
||||
python3-requests,
|
||||
|
||||
@ -211,7 +211,6 @@ autodoc_mock_imports = [
|
||||
'gi',
|
||||
'markupsafe',
|
||||
'pam',
|
||||
'paramiko',
|
||||
'psutil',
|
||||
'pytest',
|
||||
'requests',
|
||||
|
||||
@ -6,8 +6,8 @@ import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
import paramiko
|
||||
from django.utils.text import get_valid_filename
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.utils.translation import gettext_noop
|
||||
@ -51,7 +51,8 @@ class BackupsApp(app_module.App):
|
||||
order=20)
|
||||
self.add(menu_item)
|
||||
|
||||
packages = Packages('packages-backups', ['borgbackup', 'sshfs'])
|
||||
packages = Packages('packages-backups',
|
||||
['borgbackup', 'sshfs', 'sshpass'])
|
||||
self.add(packages)
|
||||
|
||||
@staticmethod
|
||||
@ -143,9 +144,13 @@ def is_ssh_hostkey_verified(hostname):
|
||||
if not known_hosts_path.exists():
|
||||
return False
|
||||
|
||||
known_hosts = paramiko.hostkeys.HostKeys(str(known_hosts_path))
|
||||
host_keys = known_hosts.lookup(hostname)
|
||||
return host_keys is not None
|
||||
try:
|
||||
subprocess.run(
|
||||
['ssh-keygen', '-F', hostname, '-f',
|
||||
str(known_hosts_path)], check=True)
|
||||
return True
|
||||
except subprocess.CalledProcessError:
|
||||
return False
|
||||
|
||||
|
||||
def split_path(path):
|
||||
|
||||
@ -7,9 +7,9 @@ import io
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
from uuid import uuid1
|
||||
|
||||
import paramiko
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from plinth import cfg
|
||||
@ -493,28 +493,13 @@ class SshBorgRepository(BaseBorgRepository):
|
||||
password = self.credentials['ssh_password']
|
||||
|
||||
# Ensure remote directory exists, check contents
|
||||
# TODO Test with IPv6 connection
|
||||
with _ssh_connection(hostname, username, password) as ssh_client:
|
||||
with ssh_client.open_sftp() as sftp_client:
|
||||
try:
|
||||
sftp_client.listdir(dir_path)
|
||||
except FileNotFoundError:
|
||||
logger.info('Directory %s does not exist, creating.',
|
||||
dir_path)
|
||||
sftp_client.mkdir(dir_path)
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _ssh_connection(hostname, username, password):
|
||||
"""Context manager to create and close an SSH connection."""
|
||||
ssh_client = paramiko.SSHClient()
|
||||
ssh_client.load_host_keys(str(get_known_hosts_path()))
|
||||
|
||||
try:
|
||||
ssh_client.connect(hostname, username=username, password=password)
|
||||
yield ssh_client
|
||||
finally:
|
||||
ssh_client.close()
|
||||
env = {'SSHPASS': password}
|
||||
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)
|
||||
|
||||
|
||||
def get_repositories():
|
||||
|
||||
@ -5,10 +5,10 @@ Views for the backups app.
|
||||
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
from datetime import datetime
|
||||
from urllib.parse import unquote
|
||||
|
||||
import paramiko
|
||||
from django.contrib import messages
|
||||
from django.contrib.messages.views import SuccessMessageMixin
|
||||
from django.http import Http404, StreamingHttpResponse
|
||||
@ -427,13 +427,14 @@ def _save_repository(request, repository):
|
||||
repository.verified = True
|
||||
repository.save()
|
||||
return True
|
||||
except paramiko.BadHostKeyException:
|
||||
message = _('SSH host public key could not be verified.')
|
||||
except paramiko.AuthenticationException:
|
||||
message = _('Authentication to remote server failed.')
|
||||
except paramiko.SSHException as exception:
|
||||
message = _('Error establishing connection to server: {}').format(
|
||||
str(exception))
|
||||
except subprocess.CalledProcessError as exception:
|
||||
if exception.returncode in (6, 7):
|
||||
message = _('SSH host public key could not be verified.')
|
||||
elif exception.returncode == 5:
|
||||
message = _('Authentication to remote server failed.')
|
||||
else:
|
||||
message = _('Error establishing connection to server: {}').format(
|
||||
str(exception))
|
||||
except Exception as exception:
|
||||
message = str(exception)
|
||||
logger.exception('Error adding repository: %s', exception)
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user