From 073fab9a5dcae64b28ec06233b515b9bcda9949e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 25 Apr 2026 12:51:50 +0000 Subject: [PATCH] aio-interface: handle SSH key authorization error explicitly in remote backup setup --- Containers/borgbackup/backupscript.sh | 28 +++++++++++++- Containers/borgbackup/start.sh | 2 + php/public/index.php | 1 + php/src/Data/DataConst.php | 4 ++ php/src/Docker/DockerActionManager.php | 4 ++ php/templates/containers.twig | 51 +++++++++++++++++--------- tests/QA/004-initial-backup.md | 6 ++- tests/QA/010-restore-instance.md | 4 +- 8 files changed, 77 insertions(+), 23 deletions(-) diff --git a/Containers/borgbackup/backupscript.sh b/Containers/borgbackup/backupscript.sh index e0c7ae8a..2aa083f8 100644 --- a/Containers/borgbackup/backupscript.sh +++ b/Containers/borgbackup/backupscript.sh @@ -14,6 +14,30 @@ get_expiration_time() { DURATION_HOUR=$((DURATION / 3600)) DURATION_READABLE=$(printf "%02d hours %02d minutes %02d seconds" $DURATION_HOUR $DURATION_MIN $DURATION_SEC) } +# Run "borg info" and handle the exit code. +# If the exit code indicates a connection failure (80 = ConnectionClosed, +# 81 = ConnectionClosedWithHint) and a remote repo is configured, the SSH +# auth error signal file is created so the mastercontainer can show a +# targeted error message. Returns the original borg exit code. +borg_info() { + borg info > /dev/null + local _exit=$? + if [ -n "$BORG_REMOTE_REPO" ] && { [ "$_exit" -eq 80 ] || [ "$_exit" -eq 81 ]; }; then + touch "$SSH_AUTH_ERROR_FILE" + fi + return $_exit +} + +# Signal file written when an SSH authentication failure is detected so the +# mastercontainer can show a targeted error without needing to scan container logs. +# Borg exit codes 80 (ConnectionClosed) and 81 (ConnectionClosedWithHint) indicate +# connection failures that occur before the Borg protocol is established, which covers +# SSH authentication errors and host-key verification failures. +# These codes are available because BORG_EXIT_CODES=modern is set in start.sh. +SSH_AUTH_ERROR_FILE="/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg_ssh_auth_error" + +# Start with a clean state for every run +rm -f "$SSH_AUTH_ERROR_FILE" # Test if all volumes aren't empty VOLUME_DIRS="$(find /nextcloud_aio_volumes -mindepth 1 -maxdepth 1 -type d)" @@ -123,7 +147,7 @@ if [ "$BORG_MODE" = backup ]; then fi # Initialize the repository if can't get info from target - if ! borg info > /dev/null; then + if ! borg_info; then # Don't initialize if already initialized if [ -f "/nextcloud_aio_volumes/nextcloud_aio_mastercontainer/data/borg.config" ]; then if [ -n "$BORG_REMOTE_REPO" ]; then @@ -584,7 +608,7 @@ fi # Do the backup test if [ "$BORG_MODE" = test ]; then if [ -n "$BORG_REMOTE_REPO" ]; then - if ! borg info > /dev/null; then + if ! borg_info; then echo "Borg could not get info from the remote repo." echo "See the above borg info output for details." exit 1 diff --git a/Containers/borgbackup/start.sh b/Containers/borgbackup/start.sh index bb7a8a6a..1ff706e5 100644 --- a/Containers/borgbackup/start.sh +++ b/Containers/borgbackup/start.sh @@ -18,6 +18,8 @@ else fi export BORG_UNKNOWN_UNENCRYPTED_REPO_ACCESS_IS_OK=yes export BORG_RELOCATED_REPO_ACCESS_IS_OK=yes +# Use specific exit codes (80/81 for connection failures) instead of the legacy generic exit code 2 +export BORG_EXIT_CODES=modern if [ -n "$BORG_REMOTE_REPO" ]; then export BORG_REPO="$BORG_REMOTE_REPO" diff --git a/php/public/index.php b/php/public/index.php index 5d706c2d..c8289831 100644 --- a/php/public/index.php +++ b/php/public/index.php @@ -142,6 +142,7 @@ $app->get('/containers', function (Request $request, Response $response, array $ 'has_backup_run_once' => $configurationManager->hasBackupRunOnce(), 'is_backup_container_running' => $dockerActionManager->isBackupContainerRunning(), 'backup_exit_code' => $dockerActionManager->GetBackupcontainerExitCode(), + 'is_ssh_auth_error' => $dockerActionManager->isBorgBackupSshAuthError(), 'is_instance_restore_attempt' => $configurationManager->instanceRestoreAttempt, 'borg_backup_mode' => $configurationManager->backupMode, 'was_start_button_clicked' => $configurationManager->wasStartButtonClicked, diff --git a/php/src/Data/DataConst.php b/php/src/Data/DataConst.php index 10d5342b..04b928e4 100644 --- a/php/src/Data/DataConst.php +++ b/php/src/Data/DataConst.php @@ -68,6 +68,10 @@ class DataConst { return (string)realpath(__DIR__ . '/../../containers.json'); } + public static function GetBorgSshAuthErrorFile() : string { + return self::GetDataDirectory() . '/borg_ssh_auth_error'; + } + public static function GetAioVersionFile() : string { return (string)realpath(__DIR__ . '/../../templates/includes/aio-version.twig'); } diff --git a/php/src/Docker/DockerActionManager.php b/php/src/Docker/DockerActionManager.php index 940814fe..6bc4c178 100644 --- a/php/src/Docker/DockerActionManager.php +++ b/php/src/Docker/DockerActionManager.php @@ -895,6 +895,10 @@ readonly class DockerActionManager { } } + public function isBorgBackupSshAuthError(): bool { + return file_exists(DataConst::GetBorgSshAuthErrorFile()); + } + public function GetBackupcontainerExitCode(): int { $containerName = 'nextcloud-aio-borgbackup'; $url = $this->BuildApiUrl(sprintf('containers/%s/json', urlencode($containerName))); diff --git a/php/templates/containers.twig b/php/templates/containers.twig index adfe3161..ff5be027 100644 --- a/php/templates/containers.twig +++ b/php/templates/containers.twig @@ -191,10 +191,17 @@ {% if not hasBackupLocation or borg_backup_mode not in ['test', 'check', ''] or backup_exit_code > 0 %} {% if borg_remote_repo and backup_exit_code > 0 %} -
- You may still need to authorize this pubkey on your borg remote:
{{ borg_public_key }}
- To try again, resubmit your location and rerun the test.
-
+ ⚠️ SSH key not authorized on the remote server. You must add the following SSH public key to the authorized_keys file on your remote backup server before the restore test can succeed:
{{ borg_public_key }}
+ Once you have added the key on the remote server, resubmit your location and rerun the test.
+
+ You may still need to authorize this pubkey on your borg remote:
{{ borg_public_key }}
+ To try again, resubmit your location and rerun the test.
+
@@ -420,21 +427,29 @@ {% if has_backup_run_once == false %}
The initial backup was not successful.
- {% if borg_remote_repo %} -
- You may still need to authorize this pubkey on your borg remote:
{{ borg_public_key }}
- To try again, click Create backup.
-
+ ⚠️ SSH key not authorized on the remote server. You must add the following SSH public key to the authorized_keys file on your remote backup server before the backup can succeed:
{{ borg_public_key }}
+ Once you have added the key on the remote server, click Create backup to try again.
+
+ You may want to reset the backup location which allows you to enter a new one afterwards. +
++ If the configured backup host location {{ borg_backup_host_location }} + {% if borg_remote_repo %} + or the remote repo {{ borg_remote_repo }} + {% endif %} + is wrong or if you want to reset the backup location due to other reasons, you can do so by clicking on the button below. +
+ {% endif %} - -You may change the backup path again since the initial backup was not successful. After submitting the new value, you need to click on Create Backup to test the new value.
- {% endif %} {% elseif backup_exit_code == 0 %} {% if borg_backup_mode == "backup" %} diff --git a/tests/QA/004-initial-backup.md b/tests/QA/004-initial-backup.md index b5b60ed5..43bb6aad 100644 --- a/tests/QA/004-initial-backup.md +++ b/tests/QA/004-initial-backup.md @@ -23,8 +23,10 @@ - [ ] Both a local backup location and a remote repo URL should not be accepted at the same time - [ ] The page should now reload - [ ] Now click on `Create backup` - - [ ] After the first failed backup attempt with a remote repo, the SSH public key for borg should be shown so it can be authorized on the remote server - - [ ] After authorizing the server on the remote, scroll down and click on `Create backup` again to create another backup. This time it should succeed. + - [ ] After the first failed backup attempt with a remote repo, the page should show **"The initial backup was not successful."** and one of two things depending on why it failed: + - [ ] **SSH auth error** (exit codes 80/81 – connection closed before Borg protocol established): a prominent ⚠️ **"SSH key not authorized on the remote server."** warning should appear with the public key displayed. After adding the key to `~/.ssh/authorized_keys` on the remote server, click **Create backup** again to retry. + - [ ] **Other error** (wrong path, unreachable host, etc.): instead of the ⚠️ warning, a **"Reset backup location"** button should appear (with a confirmation prompt) that allows resetting the configured location so a new one can be entered. Note: there are no longer inline text inputs to re-enter the location at this point. + - [ ] After authorizing the SSH key on the remote, scroll down and click on `Create backup` again to create another backup. This time it should succeed. - [ ] The initial Nextcloud credentials on top of the page that are visible when the containers are running should now be hidden in a details tag - [ ] After a while and a few automatic reloads (as long as the side is focused), you should be redirected to the usual page and seen in the Backup and restore section that the last backup was successful. - [ ] Below that you should see a details tag that allows to reveal all backup options diff --git a/tests/QA/010-restore-instance.md b/tests/QA/010-restore-instance.md index d7561a9f..d99c36eb 100644 --- a/tests/QA/010-restore-instance.md +++ b/tests/QA/010-restore-instance.md @@ -22,7 +22,9 @@ For the below to work, you need a backup archive of an AIO instance and the loca - [ ] Enter an invalid remote repo URL (e.g. `user` without `@` and `:`) which should send an error - [ ] Enter a valid remote borg repo URL and the correct backup password: - [ ] Should reload and should hide all options except the option to test the path and password - - [ ] After the first failed connection attempt, the SSH public key for borg should be shown so it can be authorized on the remote server + - [ ] After the first failed connection attempt, the behavior depends on the failure reason: + - [ ] **SSH auth error** (exit codes 80/81 – connection closed before Borg protocol established): a prominent ⚠️ **"SSH key not authorized on the remote server."** warning should appear with the SSH public key displayed and instructions to add it to `~/.ssh/authorized_keys` on the remote server. After adding the key, scroll down and click on the test button again. + - [ ] **Other error** (wrong path, unreachable host, etc.): a generic message should appear noting the public key that may still need to be authorized on the remote. - [ ] After authorizing the key on the remote server, scroll down and click on the test button again. This time it should succeed and show the options to check the integrity and list backup archives - [ ] After the test you should see the options to check the integrity of the backup and a list of backup archives that you can choose from to restore your instance - [ ] Clicking on either option should show a window prompt that lets you cancel the operation