// 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. using System.CommandLine; using System.CommandLine.NamingConventionBinder; using Duplicati.Library.AutoUpdater; using Duplicati.Library.Common.IO; using Duplicati.Library.Logging; namespace Duplicati.CommandLine.ConfigureTool.Commands; /// /// Command for securing the permissions on the Duplicati data folder. /// public static class SecureDataFolderCommand { /// /// Creates the 'secure-datafolder' command. /// public static Command CreateSecureDataFolderCommand() { var cmd = new Command("secure-datafolder", OperatingSystem.IsWindows() ? "Restrict the permissions on the data folder so only the current user, SYSTEM and Administrators can access it" : "Restrict the permissions on the data folder so only root and the current user can access it") { new Option("--data-folder", "Path to the Duplicati data folder (defaults to standard location)"), new Option("--apply", "Apply the restricted permissions without prompting. By default a warning is shown and the user must confirm."), }; cmd.Handler = CommandHandler.Create(HandleSecureDataFolder); return cmd; } /// /// Gets the data folder path, either from the option or using the default. /// private static string GetDataFolder(string? dataFolderOption) { if (!string.IsNullOrWhiteSpace(dataFolderOption)) return Path.GetFullPath(dataFolderOption); return DataFolderManager.GetDataFolder(DataFolderManager.AccessMode.ProbeOnly); } /// /// Captures log messages generated in the library and forwards them to the console. /// private static IDisposable StartConsoleLogScope() => Log.StartScope(entry => { if (entry.Level == LogMessageType.Information) Console.WriteLine(entry.FormattedMessage); else Console.WriteLine($"{entry.Level}: {entry.FormattedMessage}"); }, entry => entry.Level >= LogMessageType.Information); /// /// Handles the 'secure-datafolder' command. /// Checks the current permissions on the data folder and, if they are not already restricted, /// applies the restricted permissions (current user, SYSTEM and Administrators only). /// private static int HandleSecureDataFolder(string? dataFolder, bool apply) { var dataFolderPath = GetDataFolder(dataFolder); using var _ = StartConsoleLogScope(); Console.WriteLine($"Using data folder: {dataFolderPath}"); if (!Directory.Exists(dataFolderPath)) { Console.WriteLine($"The data folder does not exist: {dataFolderPath}"); Console.WriteLine("Create the folder first by starting the server or by creating it manually, then re-run this command."); return 1; } // Check if the permissions are already set as expected var alreadySecure = SystemIO.IO_OS.DirectoryHasPermissionUserRWOnly(dataFolderPath, out var detail); if (alreadySecure) { Console.WriteLine("The data folder already has restricted permissions (only the current user, SYSTEM and Administrators can access it)."); Console.WriteLine("No changes are needed."); return 0; } Console.WriteLine("The data folder does not have restricted permissions."); Console.WriteLine($"Current state: {detail}"); Console.WriteLine(); Console.WriteLine("Warning: if someone created this folder, running this command may inadvertently give unwanted access to this system."); Console.WriteLine("Before proceeding, verify that the data folder was created by Duplicati and not by an attacker,"); Console.WriteLine("as restricting the permissions will grant the current user, SYSTEM and Administrators full control over the folder."); Console.WriteLine(); if (!apply) { Console.Write("Apply restricted permissions? [y/N] "); var response = Console.ReadLine(); if (!string.Equals(response?.Trim(), "y", StringComparison.OrdinalIgnoreCase) && !string.Equals(response?.Trim(), "yes", StringComparison.OrdinalIgnoreCase)) { Console.WriteLine("Aborted. No changes were made."); return 1; } } try { SystemIO.IO_OS.DirectorySetPermissionUserRWOnly(dataFolderPath); if (!SystemIO.IO_OS.DirectoryHasPermissionUserRWOnly(dataFolderPath, out var checkDetail)) { Console.WriteLine($"Warning: failed to verify that permissions were applied correctly: {checkDetail}"); return 1; } Console.WriteLine("Restricted permissions applied to the data folder."); Console.WriteLine("Only the current user, SYSTEM and Administrators can now access the folder."); return 0; } catch (Exception ex) { Console.WriteLine($"Failed to set permissions on the data folder: {ex.Message}"); return 1; } } }