mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-09-19 04:59:01 +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-markupsafe,
|
||||||
python3-mypy,
|
python3-mypy,
|
||||||
python3-pampy,
|
python3-pampy,
|
||||||
python3-paramiko,
|
|
||||||
python3-pexpect,
|
python3-pexpect,
|
||||||
python3-pip,
|
python3-pip,
|
||||||
python3-psutil,
|
python3-psutil,
|
||||||
@ -114,7 +113,6 @@ Depends:
|
|||||||
python3-gi,
|
python3-gi,
|
||||||
python3-markupsafe,
|
python3-markupsafe,
|
||||||
python3-pampy,
|
python3-pampy,
|
||||||
python3-paramiko,
|
|
||||||
python3-pexpect,
|
python3-pexpect,
|
||||||
python3-psutil,
|
python3-psutil,
|
||||||
python3-requests,
|
python3-requests,
|
||||||
|
|||||||
@ -211,7 +211,6 @@ autodoc_mock_imports = [
|
|||||||
'gi',
|
'gi',
|
||||||
'markupsafe',
|
'markupsafe',
|
||||||
'pam',
|
'pam',
|
||||||
'paramiko',
|
|
||||||
'psutil',
|
'psutil',
|
||||||
'pytest',
|
'pytest',
|
||||||
'requests',
|
'requests',
|
||||||
|
|||||||
@ -6,8 +6,8 @@ import logging
|
|||||||
import os
|
import os
|
||||||
import pathlib
|
import pathlib
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
|
|
||||||
import paramiko
|
|
||||||
from django.utils.text import get_valid_filename
|
from django.utils.text import get_valid_filename
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.utils.translation import gettext_noop
|
from django.utils.translation import gettext_noop
|
||||||
@ -51,7 +51,8 @@ class BackupsApp(app_module.App):
|
|||||||
order=20)
|
order=20)
|
||||||
self.add(menu_item)
|
self.add(menu_item)
|
||||||
|
|
||||||
packages = Packages('packages-backups', ['borgbackup', 'sshfs'])
|
packages = Packages('packages-backups',
|
||||||
|
['borgbackup', 'sshfs', 'sshpass'])
|
||||||
self.add(packages)
|
self.add(packages)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -143,9 +144,13 @@ def is_ssh_hostkey_verified(hostname):
|
|||||||
if not known_hosts_path.exists():
|
if not known_hosts_path.exists():
|
||||||
return False
|
return False
|
||||||
|
|
||||||
known_hosts = paramiko.hostkeys.HostKeys(str(known_hosts_path))
|
try:
|
||||||
host_keys = known_hosts.lookup(hostname)
|
subprocess.run(
|
||||||
return host_keys is not None
|
['ssh-keygen', '-F', hostname, '-f',
|
||||||
|
str(known_hosts_path)], check=True)
|
||||||
|
return True
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def split_path(path):
|
def split_path(path):
|
||||||
|
|||||||
@ -7,9 +7,9 @@ import io
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import subprocess
|
||||||
from uuid import uuid1
|
from uuid import uuid1
|
||||||
|
|
||||||
import paramiko
|
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from plinth import cfg
|
from plinth import cfg
|
||||||
@ -493,28 +493,13 @@ class SshBorgRepository(BaseBorgRepository):
|
|||||||
password = self.credentials['ssh_password']
|
password = self.credentials['ssh_password']
|
||||||
|
|
||||||
# Ensure remote directory exists, check contents
|
# Ensure remote directory exists, check contents
|
||||||
# TODO Test with IPv6 connection
|
env = {'SSHPASS': password}
|
||||||
with _ssh_connection(hostname, username, password) as ssh_client:
|
known_hosts_path = str(get_known_hosts_path())
|
||||||
with ssh_client.open_sftp() as sftp_client:
|
subprocess.run([
|
||||||
try:
|
'sshpass', '-e', 'ssh', '-o',
|
||||||
sftp_client.listdir(dir_path)
|
f'UserKnownHostsFile={known_hosts_path}', f'{username}@{hostname}',
|
||||||
except FileNotFoundError:
|
'mkdir', '-p', dir_path
|
||||||
logger.info('Directory %s does not exist, creating.',
|
], check=True, env=env)
|
||||||
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()
|
|
||||||
|
|
||||||
|
|
||||||
def get_repositories():
|
def get_repositories():
|
||||||
|
|||||||
@ -5,10 +5,10 @@ Views for the backups app.
|
|||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from urllib.parse import unquote
|
from urllib.parse import unquote
|
||||||
|
|
||||||
import paramiko
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.messages.views import SuccessMessageMixin
|
from django.contrib.messages.views import SuccessMessageMixin
|
||||||
from django.http import Http404, StreamingHttpResponse
|
from django.http import Http404, StreamingHttpResponse
|
||||||
@ -427,13 +427,14 @@ def _save_repository(request, repository):
|
|||||||
repository.verified = True
|
repository.verified = True
|
||||||
repository.save()
|
repository.save()
|
||||||
return True
|
return True
|
||||||
except paramiko.BadHostKeyException:
|
except subprocess.CalledProcessError as exception:
|
||||||
message = _('SSH host public key could not be verified.')
|
if exception.returncode in (6, 7):
|
||||||
except paramiko.AuthenticationException:
|
message = _('SSH host public key could not be verified.')
|
||||||
message = _('Authentication to remote server failed.')
|
elif exception.returncode == 5:
|
||||||
except paramiko.SSHException as exception:
|
message = _('Authentication to remote server failed.')
|
||||||
message = _('Error establishing connection to server: {}').format(
|
else:
|
||||||
str(exception))
|
message = _('Error establishing connection to server: {}').format(
|
||||||
|
str(exception))
|
||||||
except Exception as exception:
|
except Exception as exception:
|
||||||
message = str(exception)
|
message = str(exception)
|
||||||
logger.exception('Error adding repository: %s', exception)
|
logger.exception('Error adding repository: %s', exception)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user