Version in base suite: 25.9.3+deb13u1 Base version: freedombox_25.9.3+deb13u1 Target version: freedombox_25.9.3+deb13u2 Base file: /srv/ftp-master.debian.org/ftp/pool/main/f/freedombox/freedombox_25.9.3+deb13u1.dsc Target file: /srv/ftp-master.debian.org/policy/pool/main/f/freedombox/freedombox_25.9.3+deb13u2.dsc debian/changelog | 8 ++++++ plinth/action_utils.py | 10 ++++++++ plinth/modules/ssh/privileged.py | 48 +++++++++++++++++++++++---------------- 3 files changed, 47 insertions(+), 19 deletions(-) dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp9o2as59g/freedombox_25.9.3+deb13u1.dsc: no acceptable signature found dpkg-source: warning: cannot verify inline signature for /srv/release.debian.org/tmp/tmp9o2as59g/freedombox_25.9.3+deb13u2.dsc: no acceptable signature found diff -Nru freedombox-25.9.3+deb13u1/debian/changelog freedombox-25.9.3+deb13u2/debian/changelog --- freedombox-25.9.3+deb13u1/debian/changelog 2025-12-18 13:48:51.000000000 +0000 +++ freedombox-25.9.3+deb13u2/debian/changelog 2026-11-01 01:46:31.000000000 +0000 @@ -1,3 +1,11 @@ +freedombox (25.9.3+deb13u2) trixie-security; urgency=medium + + [ Sunil Mohan Adapa ] + * ssh: Use pathlib during SSH key operations + * ssh: Read/write authorized keys file using user account privileges + + -- James Valleroy Sat, 31 Oct 2026 21:46:31 -0400 + freedombox (25.9.3+deb13u1) trixie; urgency=medium [ Sunil Mohan Adapa ] diff -Nru freedombox-25.9.3+deb13u1/plinth/action_utils.py freedombox-25.9.3+deb13u2/plinth/action_utils.py --- freedombox-25.9.3+deb13u1/plinth/action_utils.py 2025-12-18 13:48:51.000000000 +0000 +++ freedombox-25.9.3+deb13u2/plinth/action_utils.py 2026-11-01 01:46:31.000000000 +0000 @@ -779,3 +779,13 @@ shutil.move(source, destination) shutil.chown(destination, user, group) destination.chmod(permissions) + + +def run_as_user(command, username, **kwargs): + """Run a command as another user. + + Uses 'runuser' which is similar to 'su'. Creates PAM session unlike + setpriv. Sets real/effective uid/gid and resets the environment. + """ + command = ['runuser', '--user', username, '--'] + command + return subprocess.run(command, **kwargs) diff -Nru freedombox-25.9.3+deb13u1/plinth/modules/ssh/privileged.py freedombox-25.9.3+deb13u2/plinth/modules/ssh/privileged.py --- freedombox-25.9.3+deb13u1/plinth/modules/ssh/privileged.py 2025-12-18 13:48:51.000000000 +0000 +++ freedombox-25.9.3+deb13u2/plinth/modules/ssh/privileged.py 2026-11-01 01:46:31.000000000 +0000 @@ -2,7 +2,6 @@ """Configuration helper for SSH server.""" import grp -import os import pathlib import pwd import shutil @@ -73,45 +72,56 @@ return config_file.exists() -def get_user_homedir(username): +def _get_user_homedir(username: str) -> pathlib.Path: """Return the home dir of a user by looking up in password database.""" try: - return pwd.getpwnam(username).pw_dir + return pathlib.Path(pwd.getpwnam(username).pw_dir) except KeyError: raise ValueError('Username not found') @privileged def get_keys(user: str) -> str: - """Get SSH authorized keys.""" - path = os.path.join(get_user_homedir(user), '.ssh', 'authorized_keys') + """Get SSH authorized keys. + + Read the file under user account privileges instead of root as symlinks + result in user reading files that are not otherwise permitted. + """ + path = _get_user_homedir(user) / '.ssh' / 'authorized_keys' try: - with open(path, 'r', encoding='utf-8') as file_handle: - return file_handle.read() - except FileNotFoundError: + process = action_utils.run_as_user(['cat', str(path)], username=user, + capture_output=True) + except Exception: return '' + return process.stdout.decode() + @privileged def set_keys(user: str, keys: str, auth_user: str, auth_password: secret_str): - """Set SSH authorized keys.""" + """Set SSH authorized keys. + + Write the file under user account privileges instead of root as symlinks + result in user writing to files that are not otherwise permitted. + """ must_be_admin = user != auth_user _validate_user(auth_user, auth_password, must_be_admin=must_be_admin) - ssh_folder = os.path.join(get_user_homedir(user), '.ssh') - key_file_path = os.path.join(ssh_folder, 'authorized_keys') - + # Ensure home directory exists. subprocess.check_call(['mkhomedir_helper', user]) - if not os.path.exists(ssh_folder): - os.makedirs(ssh_folder) + # Ensure that .ssh folder exists + ssh_folder = _get_user_homedir(user) / '.ssh' + if not ssh_folder.exists(): + ssh_folder.mkdir() shutil.chown(ssh_folder, user, 'users') - with open(key_file_path, 'w', encoding='utf-8') as file_handle: - file_handle.write(keys) - - shutil.chown(key_file_path, user, 'users') - os.chmod(key_file_path, stat.S_IRUSR | stat.S_IWUSR) + # Write with proper ownership/permissions + key_file_path = ssh_folder / 'authorized_keys' + action_utils.run_as_user(['tee', str(key_file_path)], username=user, + input=keys.encode(encoding='utf-8'), umask=0o600, + check=True) + key_file_path.chmod(stat.S_IRUSR | stat.S_IWUSR) def _load_augeas():