diff --git a/Duplicati/Library/RestAPI/Runner.cs b/Duplicati/Library/RestAPI/Runner.cs
index 6f91bf113..019451ca0 100644
--- a/Duplicati/Library/RestAPI/Runner.cs
+++ b/Duplicati/Library/RestAPI/Runner.cs
@@ -184,6 +184,104 @@ namespace Duplicati.Server
return new CustomRunnerTask(runner);
}
+ ///
+ /// Parses the raw option value into a .
+ /// For backwards compatibility, boolean true is treated as ,
+ /// boolean false as , and null/whitespace as .
+ ///
+ /// The raw option value.
+ /// The parsed mode.
+ public static StoreTaskConfigMode ParseStoreTaskConfigMode(string? rawValue)
+ {
+ if (string.IsNullOrWhiteSpace(rawValue))
+ return StoreTaskConfigMode.Auto;
+
+ // Handle boolean values for backwards compatibility, use a lambda capture to see if we hit the default case
+ var usedDefault = false;
+ var parsedBool = Utility.ParseBool(rawValue, () => { usedDefault = true; return false; });
+ if (!usedDefault)
+ return parsedBool ? StoreTaskConfigMode.Self : StoreTaskConfigMode.None;
+
+ return Utility.ParseEnum(rawValue, StoreTaskConfigMode.Auto);
+ }
+
+ ///
+ /// Determines whether the backup is configured to use encryption.
+ ///
+ /// The backup options dictionary.
+ /// True if encryption is enabled; otherwise, false.
+ private static bool IsBackupEncryptionEnabled(Dictionary options)
+ {
+ // In principle, this check should be enough
+ if (Utility.ParseBoolOption(options, "no-encryption"))
+ return false;
+
+ // But since we explicitly set the encryption module, we also check that this is set
+ if (string.IsNullOrWhiteSpace(options.GetValueOrDefault("encryption-module")))
+ return false;
+
+ // Also, we need a passphrase
+ if (string.IsNullOrWhiteSpace(options.GetValueOrDefault("passphrase")))
+ return false;
+
+ return true;
+ }
+
+ ///
+ /// Resolves the effective store-task-config mode based on encryption settings.
+ ///
+ /// The requested mode.
+ /// Whether backup encryption is enabled.
+ /// The effective mode to use.
+ private static StoreTaskConfigMode ResolveEffectiveMode(StoreTaskConfigMode mode, bool encryptionEnabled)
+ {
+ if (encryptionEnabled)
+ {
+ return mode switch
+ {
+ StoreTaskConfigMode.Auto => StoreTaskConfigMode.SelfWithForcedSecrets,
+ StoreTaskConfigMode.Self => StoreTaskConfigMode.SelfWithForcedSecrets,
+ StoreTaskConfigMode.All => StoreTaskConfigMode.AllWithForcedSecrets,
+ _ => mode
+ };
+ }
+ else
+ {
+ return mode switch
+ {
+ StoreTaskConfigMode.Auto => StoreTaskConfigMode.None,
+ StoreTaskConfigMode.Self => StoreTaskConfigMode.Self,
+ StoreTaskConfigMode.All => StoreTaskConfigMode.All,
+ _ => mode
+ };
+ }
+ }
+
+ ///
+ /// Prepares a backup for export, optionally removing sensitive information.
+ ///
+ /// The database connection.
+ /// The backup to export.
+ /// Whether to include secrets in the export.
+ /// The import/export structure.
+ private static Serializable.ImportExportStructure PrepareBackupForExport(Connection databaseConnection, IBackup backup, bool includeSecrets)
+ {
+ var exported = databaseConnection.PrepareBackupForExport(backup);
+ if (!includeSecrets && exported.Backup != null)
+ {
+ var clone = exported.Backup.Clone();
+ clone.RemoveSensitiveInformation();
+ exported = new Serializable.ImportExportStructure()
+ {
+ CreatedByVersion = exported.CreatedByVersion,
+ Backup = clone,
+ Schedule = exported.Schedule,
+ DisplayNames = exported.DisplayNames
+ };
+ }
+ return exported;
+ }
+
public static IRunnerData CreateTask(DuplicatiOperation operation, IBackup backup, IDictionary? extraOptions = null, string[]? filterStrings = null, string[]? extraArguments = null, int pageSize = 0, int pageOffset = 0, bool returnExtended = false, bool caseSensitiveSearch = false)
{
return new RunnerData()
@@ -670,7 +768,7 @@ namespace Duplicati.Server
ApplyAdditionalTargetUrls(backup, options);
// Pack in the system or task config for easy restore
- if (data.Operation == DuplicatiOperation.Backup && options.ContainsKey("store-task-config"))
+ if (data.Operation == DuplicatiOperation.Backup)
tempfolder = StoreTaskConfigAndGetTempFolder(databaseConnection, data, options);
var useOutOfProcess = databaseConnection.ApplicationSettings.UseOutOfProcessController;
@@ -882,39 +980,47 @@ namespace Duplicati.Server
if (data.Backup == null)
throw new ArgumentNullException(nameof(data.Backup));
- var all_tasks = string.Equals(options["store-task-config"], "all", StringComparison.OrdinalIgnoreCase) || string.Equals(options["store-task-config"], "*", StringComparison.OrdinalIgnoreCase);
- var this_task = Utility.ParseBool(options["store-task-config"], false);
+ var mode = ParseStoreTaskConfigMode(options.GetValueOrDefault("store-task-config"));
+ var encryptionEnabled = IsBackupEncryptionEnabled(options);
+ var effectiveMode = ResolveEffectiveMode(mode, encryptionEnabled);
options.Remove("store-task-config");
- TempFolder? tempfolder = null;
- if (all_tasks || this_task)
+ if (effectiveMode == StoreTaskConfigMode.None)
+ return null;
+
+ bool includeAllTasks = effectiveMode == StoreTaskConfigMode.All || effectiveMode == StoreTaskConfigMode.AllWithForcedSecrets;
+ bool includeSecrets = effectiveMode == StoreTaskConfigMode.SelfWithForcedSecrets || effectiveMode == StoreTaskConfigMode.AllWithForcedSecrets;
+
+ var tempfolder = new TempFolder();
+ var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json");
+ using (var tempfile = Library.Utility.TempFile.WrapExistingFile(temppath))
{
- tempfolder = new TempFolder();
- var temppath = System.IO.Path.Combine(tempfolder, "task-setup.json");
- using (var tempfile = Library.Utility.TempFile.WrapExistingFile(temppath))
+ IEnumerable? taskdata = null;
+ if (includeAllTasks)
{
- object? taskdata = null;
- if (all_tasks)
- taskdata = databaseConnection.Backups.Where(x => !x.IsTemporary).Select(x => databaseConnection.PrepareBackupForExport(databaseConnection.GetBackup(x.ID)!));
- else
- taskdata = new[] { databaseConnection.PrepareBackupForExport(data.Backup) };
-
- using (var fs = System.IO.File.OpenWrite(tempfile))
- using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8))
- Serializer.SerializeJson(sw, taskdata, true);
-
- tempfile.Protected = true;
-
- options.TryGetValue("control-files", out var controlfiles);
-
- if (string.IsNullOrWhiteSpace(controlfiles))
- controlfiles = tempfile;
- else
- controlfiles += System.IO.Path.PathSeparator + tempfile;
-
- options["control-files"] = controlfiles;
+ taskdata = databaseConnection.Backups
+ .Where(x => !x.IsTemporary)
+ .Select(x => PrepareBackupForExport(databaseConnection, databaseConnection.GetBackup(x.ID)!, includeSecrets));
}
+ else
+ {
+ taskdata = [PrepareBackupForExport(databaseConnection, data.Backup, includeSecrets)];
+ }
+
+ using (var fs = System.IO.File.OpenWrite(tempfile))
+ using (var sw = new System.IO.StreamWriter(fs, System.Text.Encoding.UTF8))
+ Serializer.SerializeJson(sw, taskdata, true);
+
+ options.TryGetValue("control-files", out var controlfiles);
+
+ // Append this to any other control files
+ options["control-files"] = string.IsNullOrWhiteSpace(controlfiles)
+ ? controlfiles = tempfile
+ : controlfiles += System.IO.Path.PathSeparator + tempfile;
+
+ // Don't delete the file now, leave it for when the folder is deleted
+ tempfile.Protected = true;
}
return tempfolder;
}
diff --git a/Duplicati/Library/RestAPI/StoreTaskConfigMode.cs b/Duplicati/Library/RestAPI/StoreTaskConfigMode.cs
new file mode 100644
index 000000000..5f225c113
--- /dev/null
+++ b/Duplicati/Library/RestAPI/StoreTaskConfigMode.cs
@@ -0,0 +1,60 @@
+// Copyright (C) 2026, The Duplicati Team
+// https://duplicati.com, hello@duplicati.com
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the "Software"),
+// to deal in the Software without restriction, including without limitation
+// the rights to use, copy, modify, merge, publish, distribute, sublicense,
+// and/or sell copies of the Software, and to permit persons to whom the
+// Software is furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in
+// all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+// DEALINGS IN THE SOFTWARE.
+
+namespace Duplicati.Server;
+
+///
+/// Defines how the task configuration should be stored in the backup.
+///
+public enum StoreTaskConfigMode
+{
+ ///
+ /// Automatically determine behavior based on encryption settings.
+ /// When encryption is enabled, behaves as .
+ /// When encryption is not enabled, behaves as .
+ ///
+ Auto,
+ ///
+ /// Include the current job's backup configuration.
+ /// When encryption is enabled, includes all secrets.
+ /// When encryption is not enabled, excludes secrets.
+ ///
+ Self,
+ ///
+ /// Include all job backup configurations.
+ /// When encryption is enabled, includes all secrets.
+ /// When encryption is not enabled, excludes secrets.
+ ///
+ All,
+ ///
+ /// Do not include any task configuration.
+ ///
+ None,
+ ///
+ /// Include the current job's backup configuration with all secrets included.
+ ///
+ SelfWithForcedSecrets,
+ ///
+ /// Include all job backup configurations with all secrets included.
+ ///
+ AllWithForcedSecrets
+}
+
diff --git a/Duplicati/Library/RestAPI/Strings.cs b/Duplicati/Library/RestAPI/Strings.cs
index 4ff1c0d26..867ba9506 100644
--- a/Duplicati/Library/RestAPI/Strings.cs
+++ b/Duplicati/Library/RestAPI/Strings.cs
@@ -129,6 +129,8 @@ Error message: {0}", error); }
public static string ConfigureHttpsHostnamesLong { get { return LC.L(@"Comma-separated list of hostnames to include in the HTTPS certificate (used with --configure-https)"); } }
public static string SuppressWelcomePageShort { get { return LC.L(@"Suppress the initial welcome page"); } }
public static string SuppressWelcomePageLong { get { return LC.L(@"Suppress the welcome page that is shown when first using the web interface"); } }
+ public static string StoretaskconfigShort { get { return LC.L(@"Store task configuration with backup"); } }
+ public static string StoretaskconfigLong { get { return LC.L(@$"Controls whether the backup configuration is stored as a control file in the backup. When encryption is enabled, {StoreTaskConfigMode.Auto} stores the current job with secrets. When encryption is not enabled, {StoreTaskConfigMode.Auto} does not store anything. When encryption is not enabled {StoreTaskConfigMode.Self} and {StoreTaskConfigMode.All} store the configuration without secrets. Note that configuration is only uploaded if the backup has changed, and a configuration change alone will not be treated as a new backup version."); } }
}
internal static class Scheduler
{
diff --git a/Duplicati/WebserverCore/Dto/SystemInfoDto.cs b/Duplicati/WebserverCore/Dto/SystemInfoDto.cs
index 32eac6fc2..4c408b77e 100644
--- a/Duplicati/WebserverCore/Dto/SystemInfoDto.cs
+++ b/Duplicati/WebserverCore/Dto/SystemInfoDto.cs
@@ -139,6 +139,11 @@ public sealed record SystemInfoDto
///
public required IEnumerable Options { get; init; }
+ ///
+ /// Gets or sets the server-only options.
+ ///
+ public required IEnumerable ServerOnlyOptions { get; init; }
+
///
/// Gets or sets the compression modules.
///
diff --git a/Duplicati/WebserverCore/DuplicatiWebserver.cs b/Duplicati/WebserverCore/DuplicatiWebserver.cs
index 7b641b40a..f5b0478fa 100644
--- a/Duplicati/WebserverCore/DuplicatiWebserver.cs
+++ b/Duplicati/WebserverCore/DuplicatiWebserver.cs
@@ -27,6 +27,7 @@ using System.Text.Json;
using System.Text.Json.Serialization;
using Duplicati.Library.Interface;
using Duplicati.Library.Utility;
+using Duplicati.Server;
using Duplicati.Server.Database;
using Duplicati.WebserverCore.Abstractions;
using Duplicati.WebserverCore.Dto.V2;
@@ -103,6 +104,21 @@ public class DuplicatiWebserver
private static readonly bool EnableSwagger = false;
#endif
+ ///
+ /// Gets server-only options that are consumed by the server and not passed to the command-line client.
+ ///
+ public static ICommandLineArgument[] ServerOnlyOptions =>
+ [
+ new CommandLineArgument(
+ "store-task-config",
+ CommandLineArgument.ArgumentType.Enumeration,
+ Server.Strings.Program.StoretaskconfigShort,
+ Server.Strings.Program.StoretaskconfigLong,
+ StoreTaskConfigMode.Auto.ToString(),
+ null,
+ Enum.GetNames(typeof(StoreTaskConfigMode)))
+ ];
+
///
/// The settings used for stating the server
///
diff --git a/Duplicati/WebserverCore/Services/SystemInfoProvider.cs b/Duplicati/WebserverCore/Services/SystemInfoProvider.cs
index 0a763f97e..182475a56 100644
--- a/Duplicati/WebserverCore/Services/SystemInfoProvider.cs
+++ b/Duplicati/WebserverCore/Services/SystemInfoProvider.cs
@@ -175,6 +175,11 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
///
public required Library.Interface.ICommandLineArgument[] Options { get; init; }
+ ///
+ /// Gets or sets the server-only options.
+ ///
+ public required Library.Interface.ICommandLineArgument[] ServerOnlyOptions { get; init; }
+
///
/// Gets or sets the compression modules.
///
@@ -283,6 +288,7 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
NewLine = Environment.NewLine,
CLRVersion = Environment.Version.ToString(),
Options = Server.Serializable.ServerSettings.Options,
+ ServerOnlyOptions = DuplicatiWebserver.ServerOnlyOptions,
CompressionModules = Server.Serializable.ServerSettings.CompressionModules,
EncryptionModules = Server.Serializable.ServerSettings.EncryptionModules,
BackendModules = Server.Serializable.ServerSettings.BackendModules,
@@ -372,6 +378,7 @@ public class SystemInfoProvider(IApplicationSettings applicationSettings, Connec
NewLine = systeminfo.NewLine,
CLRVersion = systeminfo.CLRVersion,
Options = systeminfo.Options,
+ ServerOnlyOptions = systeminfo.ServerOnlyOptions,
CompressionModules = systeminfo.CompressionModules,
EncryptionModules = systeminfo.EncryptionModules,
BackendModules = systeminfo.BackendModules,