diff --git a/examples/npbackup.linux.conf.dist b/examples/npbackup.linux.conf.dist index f035c68..45c19f6 100644 --- a/examples/npbackup.linux.conf.dist +++ b/examples/npbackup.linux.conf.dist @@ -28,6 +28,7 @@ backup: repo: repository: password: + password_command: # Backup age, in minutes, which is the minimum time between two backups minimum_backup_age: 1440 upload_speed: 0 # in KiB, use 0 for unlimited upload speed diff --git a/examples/npbackup.windows.conf.dist b/examples/npbackup.windows.conf.dist index 0f74447..61b7b49 100644 --- a/examples/npbackup.windows.conf.dist +++ b/examples/npbackup.windows.conf.dist @@ -28,6 +28,7 @@ backup: repo: repository: password: + password_command: # Backup age, in minutes, which is the minimum time between two backups minimum_backup_age: 1440 upload_speed: 0 # in KiB, use 0 for unlimited upload speed diff --git a/npbackup/configuration.py b/npbackup/configuration.py index 94439a5..62d419e 100644 --- a/npbackup/configuration.py +++ b/npbackup/configuration.py @@ -7,7 +7,7 @@ __intname__ = "npbackup.configuration" __author__ = "Orsiris de Jong" __copyright__ = "Copyright (C) 2022-2023 NetInvent" __license__ = "GPL-3.0-only" -__build__ = "2023041201" +__build__ = "2023050301" __version__ = "1.7.0 for npbackup 2.2.0+" from typing import Tuple, Optional, List @@ -54,9 +54,11 @@ except ImportError: logger = getLogger(__name__) +# NPF-SEC-00003: Avoid password command divulgation ENCRYPTED_OPTIONS = [ {"section": "repo", "name": "repository", "type": str}, {"section": "repo", "name": "password", "type": str}, + {"section": "repo", "name": "password_command", "type": str}, {"section": "prometheus", "name": "http_username", "type": str}, {"section": "prometheus", "name": "http_password", "type": str}, {"section": "options", "name": "auto_upgrade_server_username", "type": str}, @@ -82,6 +84,7 @@ empty_config_dict = { "repo": { "repository": "", "password": "", + "password_command": "", "minimum_backup_age": 1440, "upload_speed": 0, "download_speed": 0, diff --git a/npbackup/core/runner.py b/npbackup/core/runner.py index 1c7d404..43f84fa 100644 --- a/npbackup/core/runner.py +++ b/npbackup/core/runner.py @@ -111,6 +111,8 @@ class NPBackupRunner: """ Wraps ResticRunner into a class that is usable by NPBackup """ + # NPF-SEC-00002: password commands, pre_exec and post_exec commands will be executed with npbackup privileges + # This can lead to a problem when the config file can be written by users other than npbackup def __init__(self, config_dict): self.config_dict = config_dict @@ -214,12 +216,27 @@ class NPBackupRunner: logger.error("Repo cannot be empty") can_run = False try: - password = self.config_dict["repo"]["password"] - if not password: - raise KeyError + password = self.config_dict["repo"]["password"] except (KeyError, AttributeError): logger.error("Repo password cannot be empty") can_run = False + if not password or password == "": + try: + password_command = self.config_dict["repo"]["password_command"] + if password_command and password_command != "": + exit_code, output = command_runner(password_command, shell=True, timeout=30) + if exit_code != 0 or output == "": + logger.error("Password command failed to produce output:\n{}".format(output)) + can_run = False + else: + password = output + else: + logger.error("No password nor password command given. Repo password cannot be empty") + can_run = False + except KeyError: + logger.error("No password nor password command given. Repo password cannot be empty") + can_run = False + print(password) self.is_ready = can_run if not can_run: return None diff --git a/npbackup/gui/config.py b/npbackup/gui/config.py index 7697c25..5ae7090 100644 --- a/npbackup/gui/config.py +++ b/npbackup/gui/config.py @@ -66,12 +66,14 @@ def config_gui(config_dict: dict, config_file: str): try: value = config_dict[section][entry] # Don't show sensible info unless unencrypted requested + # TODO: Refactor this to use ENCRYPTED_OPTIONS from configuration if not unencrypted: if entry in [ "http_username", "http_password", "repository", "password", + "password_command", "auto_upgrade_server_username", "auto_upgrade_server_password", ]: @@ -251,6 +253,10 @@ def config_gui(config_dict: dict, config_file: str): sg.Text(_t("config_gui.backup_repo_password"), size=(30, 1)), sg.Input(key="repo---password", size=(50, 1)), ], + [ + sg.Text(_t("config.gui.backup_repo_password_command"), size=(30, 1)), + sg.Input(key="repo---password_command", size=(50, 1)) + ], [ sg.Text(_t("config_gui.upload_speed"), size=(30, 1)), sg.Input(key="repo---upload_speed", size=(50, 1)), @@ -483,7 +489,7 @@ def config_gui(config_dict: dict, config_file: str): if event in (sg.WIN_CLOSED, "cancel"): break if event == "accept": - if not values["repo---password"]: + if not values["repo---password"] and not values["repo---password_command"]: sg.PopupError(_t("config_gui.repo_password_cannot_be_empty")) continue config_dict = update_config_dict(values, config_dict) diff --git a/npbackup/translations/config_gui.en.yml b/npbackup/translations/config_gui.en.yml index 3d2b282..b415235 100644 --- a/npbackup/translations/config_gui.en.yml +++ b/npbackup/translations/config_gui.en.yml @@ -53,7 +53,7 @@ en: configuration_saved: Configuration saved cannot_save_configuration: Could not save configuration. See logs for further info - repo_password_cannot_be_empty: Password cannot be empty + repo_password_cannot_be_empty: Repo password or password command cannot be empty enter_backup_admin_password: Backup admin password wrong_password: Wrong password password_updated_please_save: Password updated. Please save configuration diff --git a/npbackup/translations/config_gui.fr.yml b/npbackup/translations/config_gui.fr.yml index 8e73045..41be20e 100644 --- a/npbackup/translations/config_gui.fr.yml +++ b/npbackup/translations/config_gui.fr.yml @@ -53,7 +53,7 @@ fr: configuration_saved: Configuration sauvegardée cannot_save_configuration: Impossible d'enregistrer la configuration. Veuillez consulter les journaux pour plus de détails - repo_password_cannot_be_empty: Le mot de passe du dépot ne peut être vide + repo_password_cannot_be_empty: Le mot de passe du dépot ou la commande de mot de passe ne peut être vide enter_backup_admin_password: Mot de passe admin de sauvegarde wrong_password: Mot de passe érroné password_updated_please_save: Mot de passe mis à jour. Veuillez enregistrer la configuraiton