and again more work on backup-storages

Signed-off-by: Michael Kaufmann <d00p@froxlor.org>
This commit is contained in:
Michael Kaufmann
2023-09-05 11:03:39 +02:00
parent 9d2077ddee
commit 4fcf0606c7
13 changed files with 429 additions and 36 deletions

View File

@@ -81,6 +81,15 @@ return [
'checkPgpPublicKeySetting'
],
],
'backup_backup_tmp_dir' => [
'label' => lng('serversettings.backup_tmp_dir'),
'settinggroup' => 'backup',
'varname' => 'backup_tmp_dir',
'type' => 'text',
'string_type' => 'dir',
'default' => '/var/customers/backup/',
'save_method' => 'storeSettingField'
]
]
]
]

View File

@@ -55,13 +55,13 @@ if (($page == 'backups' || $page == 'overview')) {
'actions_links' => [
[
'href' => $linker->getLink(['section' => 'backups', 'page' => $page, 'action' => 'restore']),
'label' => lng('admin.backups_restore'),
'label' => lng('backup.backups_restore'),
'icon' => 'fa-solid fa-file-import',
'class' => 'btn-outline-secondary'
],
[
'href' => $linker->getLink(['section' => 'backups', 'page' => 'storages']),
'label' => lng('admin.backup_storages'),
'label' => lng('backup.backup_storages'),
'icon' => 'fa-solid fa-hard-drive',
'class' => 'btn-outline-secondary',
'visible' => $userinfo['change_serversettings'] == '1'
@@ -94,12 +94,12 @@ if (($page == 'backups' || $page == 'overview')) {
'actions_links' => [
[
'href' => $linker->getLink(['section' => 'backups', 'page' => 'backups']),
'label' => lng('admin.backups'),
'label' => lng('backup.backups'),
'icon' => 'fa-solid fa-reply'
],
[
'href' => $linker->getLink(['section' => 'backups', 'page' => $page, 'action' => 'add']),
'label' => lng('admin.backup_storage_add')
'label' => lng('backup.backup_storage.add')
]
]
]);

View File

@@ -47,6 +47,7 @@
"ext-gmp": "*",
"ext-gd": "*",
"ext-ftp": "*",
"ext-gnupg": "*",
"phpmailer/phpmailer": "~6.0",
"monolog/monolog": "^1.24",
"robthree/twofactorauth": "^1.6",
@@ -57,7 +58,8 @@
"symfony/console": "^5.4",
"pear/net_dns2": "^1.5",
"amnuts/opcache-gui": "^3.4",
"aws/aws-sdk-php": "^3.280"
"aws/aws-sdk-php": "^3.280",
"phpseclib/phpseclib": "~3.0"
},
"require-dev": {
"phpunit/phpunit": "^9",

229
composer.lock generated
View File

@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "b3c285b81c9729ba8e2bac6f3b586e9c",
"content-hash": "c64c2e8e5669531310620aa423ad2ecd",
"packages": [
{
"name": "amnuts/opcache-gui",
@@ -800,6 +800,123 @@
},
"time": "2023-08-25T10:54:48+00:00"
},
{
"name": "paragonie/constant_time_encoding",
"version": "v2.6.3",
"source": {
"type": "git",
"url": "https://github.com/paragonie/constant_time_encoding.git",
"reference": "58c3f47f650c94ec05a151692652a868995d2938"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/constant_time_encoding/zipball/58c3f47f650c94ec05a151692652a868995d2938",
"reference": "58c3f47f650c94ec05a151692652a868995d2938",
"shasum": ""
},
"require": {
"php": "^7|^8"
},
"require-dev": {
"phpunit/phpunit": "^6|^7|^8|^9",
"vimeo/psalm": "^1|^2|^3|^4"
},
"type": "library",
"autoload": {
"psr-4": {
"ParagonIE\\ConstantTime\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com",
"role": "Maintainer"
},
{
"name": "Steve 'Sc00bz' Thomas",
"email": "steve@tobtu.com",
"homepage": "https://www.tobtu.com",
"role": "Original Developer"
}
],
"description": "Constant-time Implementations of RFC 4648 Encoding (Base-64, Base-32, Base-16)",
"keywords": [
"base16",
"base32",
"base32_decode",
"base32_encode",
"base64",
"base64_decode",
"base64_encode",
"bin2hex",
"encoding",
"hex",
"hex2bin",
"rfc4648"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/constant_time_encoding/issues",
"source": "https://github.com/paragonie/constant_time_encoding"
},
"time": "2022-06-14T06:56:20+00:00"
},
{
"name": "paragonie/random_compat",
"version": "v9.99.100",
"source": {
"type": "git",
"url": "https://github.com/paragonie/random_compat.git",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/paragonie/random_compat/zipball/996434e5492cb4c3edcb9168db6fbb1359ef965a",
"reference": "996434e5492cb4c3edcb9168db6fbb1359ef965a",
"shasum": ""
},
"require": {
"php": ">= 7"
},
"require-dev": {
"phpunit/phpunit": "4.*|5.*",
"vimeo/psalm": "^1"
},
"suggest": {
"ext-libsodium": "Provides a modern crypto API that can be used to generate random bytes."
},
"type": "library",
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Paragon Initiative Enterprises",
"email": "security@paragonie.com",
"homepage": "https://paragonie.com"
}
],
"description": "PHP 5.x polyfill for random_bytes() and random_int() from PHP 7",
"keywords": [
"csprng",
"polyfill",
"pseudorandom",
"random"
],
"support": {
"email": "info@paragonie.com",
"issues": "https://github.com/paragonie/random_compat/issues",
"source": "https://github.com/paragonie/random_compat"
},
"time": "2020-10-15T08:29:30+00:00"
},
{
"name": "pear/net_dns2",
"version": "v1.5.3",
@@ -931,6 +1048,116 @@
],
"time": "2023-08-29T08:26:30+00:00"
},
{
"name": "phpseclib/phpseclib",
"version": "3.0.21",
"source": {
"type": "git",
"url": "https://github.com/phpseclib/phpseclib.git",
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
"reference": "4580645d3fc05c189024eb3b834c6c1e4f0f30a1",
"shasum": ""
},
"require": {
"paragonie/constant_time_encoding": "^1|^2",
"paragonie/random_compat": "^1.4|^2.0|^9.99.99",
"php": ">=5.6.1"
},
"require-dev": {
"phpunit/phpunit": "*"
},
"suggest": {
"ext-dom": "Install the DOM extension to load XML formatted public keys.",
"ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.",
"ext-libsodium": "SSH2/SFTP can make use of some algorithms provided by the libsodium-php extension.",
"ext-mcrypt": "Install the Mcrypt extension in order to speed up a few other cryptographic operations.",
"ext-openssl": "Install the OpenSSL extension in order to speed up a wide variety of cryptographic operations."
},
"type": "library",
"autoload": {
"files": [
"phpseclib/bootstrap.php"
],
"psr-4": {
"phpseclib3\\": "phpseclib/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Jim Wigginton",
"email": "terrafrost@php.net",
"role": "Lead Developer"
},
{
"name": "Patrick Monnerat",
"email": "pm@datasphere.ch",
"role": "Developer"
},
{
"name": "Andreas Fischer",
"email": "bantu@phpbb.com",
"role": "Developer"
},
{
"name": "Hans-Jürgen Petrich",
"email": "petrich@tronic-media.com",
"role": "Developer"
},
{
"name": "Graham Campbell",
"email": "graham@alt-three.com",
"role": "Developer"
}
],
"description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.",
"homepage": "http://phpseclib.sourceforge.net",
"keywords": [
"BigInteger",
"aes",
"asn.1",
"asn1",
"blowfish",
"crypto",
"cryptography",
"encryption",
"rsa",
"security",
"sftp",
"signature",
"signing",
"ssh",
"twofish",
"x.509",
"x509"
],
"support": {
"issues": "https://github.com/phpseclib/phpseclib/issues",
"source": "https://github.com/phpseclib/phpseclib/tree/3.0.21"
},
"funding": [
{
"url": "https://github.com/terrafrost",
"type": "github"
},
{
"url": "https://www.patreon.com/phpseclib",
"type": "patreon"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpseclib/phpseclib",
"type": "tidelift"
}
],
"time": "2023-07-09T15:24:48+00:00"
},
{
"name": "psr/container",
"version": "1.1.2",

View File

@@ -698,7 +698,7 @@ opcache.validate_timestamps'),
('system', 'distribution', ''),
('system', 'update_channel', 'stable'),
('system', 'updatecheck_data', ''),
('system', 'update_notify_last', '2.0.20'),
('system', 'update_notify_last', '2.1.0-dev1'),
('system', 'traffictool', 'goaccess'),
('system', 'req_limit_per_interval', 60),
('system', 'req_limit_interval', 60),
@@ -751,7 +751,7 @@ opcache.validate_timestamps'),
('panel', 'logo_overridetheme', '0'),
('panel', 'logo_overridecustom', '0'),
('panel', 'settings_mode', '0'),
('panel', 'version', '2.0.20'),
('panel', 'version', '2.1.0-dev1'),
('panel', 'db_version', '202305240');

View File

@@ -35,6 +35,9 @@ use Froxlor\Settings;
use Froxlor\UI\Response;
use Froxlor\Validate\Validate;
use PDO;
use phpseclib3\Crypt\Common\PublicKey;
use phpseclib3\Crypt\PublicKeyLoader;
use phpseclib3\Exception\NoKeyLoadedException;
/**
* @since 2.1.0
@@ -73,7 +76,7 @@ class BackupStorages extends ApiCommand implements ResourceEntity
$this->logger()->logAction(FroxlorLogger::ADM_ACTION, LOG_INFO, "[API] list backup storages");
$query_fields = [];
$result_stmt = Database::prepare("
SELECT * FROM `" . TABLE_PANEL_BACKUP_STORAGES . "` ". $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()
SELECT * FROM `" . TABLE_PANEL_BACKUP_STORAGES . "` " . $this->getSearchWhere($query_fields) . $this->getOrderBy() . $this->getLimit()
);
Database::pexecute($result_stmt, $query_fields, true, true);
$result = [];
@@ -133,7 +136,7 @@ class BackupStorages extends ApiCommand implements ResourceEntity
* @param string $pgp_public_key
* optional, pgp public key for backup storage
* @param string $retention
* optional, retention for backup storage (default 3)
* optional, retention for backup storage (default {backup.default_retention})
*
* @access admin
* @return string json-encoded array
@@ -177,10 +180,36 @@ class BackupStorages extends ApiCommand implements ResourceEntity
$username = $this->getParam('username', $optional_flags['username']);
$password = $this->getParam('password', $optional_flags['password']);
$pgp_public_key = $this->getParam('pgp_public_key', true, null);
$retention = $this->getParam('retention', true, 3);
$retention = $this->getParam('retention', true, Settings::Get('backup.default_retention'));
// validation
$destination_path = FileDir::makeCorrectDir(Validate::validate($destination_path, 'destination_path', Validate::REGEX_DIR, '', [], true));
if ($type != 'local') {
if (!Validate::validateUrl($hostname)) {
Response::standardError('invalidhostname', '', true);
}
// check whether password is an ssh public key
$pwd_is_ssh_key = false;
try {
$key = PublicKeyLoader::loadPublicKey($password);
if ($key instanceof PublicKey) {
$pwd_is_ssh_key = true;
}
} catch (NoKeyLoadedException $e) {
/* nothing to do */
}
if (!$pwd_is_ssh_key) {
// normal password
$password = Validate::validate($password, 'password', '', '', [], true);
}
}
if ($type == 's3') {
$region = Validate::validate($region, 'region', '', '', [], true);
$bucket = Validate::validate($bucket, 'bucket', '/(?!(^xn--|.+-s3alias$))^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/', '', [], true);
}
if ($retention <= 0) {
$retention = Settings::Get('backup.default_retention');
}
// TODO: add more validation
// pgp public key validation
@@ -303,7 +332,7 @@ class BackupStorages extends ApiCommand implements ResourceEntity
* @param string $pgp_public_key
* optional, pgp public key for backup storage
* @param string $retention
* optional, retention for backup storage (default 3)
* optional, retention for backup storage (default {backup.default_retention})
*
* @access admin
* @return string json-encoded array
@@ -342,8 +371,24 @@ class BackupStorages extends ApiCommand implements ResourceEntity
if (empty($username)) {
throw new Exception("Field 'username' cannot be empty", 406);
}
// password change
if (!empty($password)) {
// check whether password is an ssh public key
$pwd_is_ssh_key = false;
try {
$key = PublicKeyLoader::loadPublicKey($password);
if ($key instanceof PublicKey) {
$pwd_is_ssh_key = true;
}
} catch (NoKeyLoadedException $e) {
/* nothing to do */
}
if (!$pwd_is_ssh_key) {
// normal password
$password = Validate::validate($password, 'password', '', '', [], true);
}
}
}
if ($type == 's3') {
if (empty($region)) {
throw new Exception("Field 'region' cannot be empty", 406);
@@ -355,6 +400,16 @@ class BackupStorages extends ApiCommand implements ResourceEntity
// validation
$destination_path = FileDir::makeCorrectDir(Validate::validate($destination_path, 'destination_path', Validate::REGEX_DIR, '', [], true));
if ($type != 'local' && !Validate::validateUrl($hostname)) {
Response::standardError('invalidhostname', '', true);
}
if ($type == 's3') {
$region = Validate::validate($region, 'region', '', '', [], true);
$bucket = Validate::validate($bucket, 'bucket', '/(?!(^xn--|.+-s3alias$))^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/', '', [], true);
}
if ($retention <= 0) {
$retention = Settings::Get('backup.default_retention');
}
// TODO: add more validation
// pgp public key validation

View File

@@ -35,13 +35,16 @@ class S3 extends Storage
* @param string $filename
* @param string $tmp_source_directory
* @return string
* @throws \Exception
*/
protected function putFile(string $filename, string $tmp_source_directory): string
{
$source = FileDir::makeCorrectFile($tmp_source_directory . "/" . $filename);
$target = FileDir::makeCorrectFile($this->getDestinationDirectory() . "/" . $filename);
$this->s3_client->putObject([
'Bucket' => $this->sData['storage']['bucket'],
'Key' => $filename,
'SourceFile' => FileDir::makeCorrectFile($tmp_source_directory . '/' . $filename),
'Key' => $target,
'SourceFile' => $source,
]);
}

View File

@@ -2,15 +2,34 @@
namespace Froxlor\Backup\Storages;
use Exception;
use Froxlor\FileDir;
use phpseclib3\Net\SFTP as secSFTP;
class Sftp extends Storage
{
private secSFTP $sftp_client;
/**
* @return bool
*/
public function init(): bool
{
// TODO: Implement init() method.
$hostname = $this->sData['storage']['hostname'] ?? '';
$username = $this->sData['storage']['username'] ?? '';
$password = $this->sData['storage']['password'] ?? '';
if (!empty($hostname) && !empty($username) && !empty($password)) {
$tmp = explode(":", $hostname);
$hostname = $tmp[0];
$port = $tmp[1] ?? 22;
$this->sftp_client = new secSFTP($hostname, $port);
if ($this->sftp_client->isConnected()) {
// @todo login by either user/passwd or user/ssh-key
return true;
}
return false;
}
throw new Exception('Empty hostname for FTP backup storage');
}
/**
@@ -20,19 +39,27 @@ class Sftp extends Storage
* @param string $filename
* @param string $tmp_source_directory
* @return string
* @throws Exception
*/
protected function putFile(string $filename, string $tmp_source_directory): string
{
return "";
$source = FileDir::makeCorrectFile($tmp_source_directory . "/" . $filename);
$target = FileDir::makeCorrectFile($this->getDestinationDirectory() . "/" . $filename);
$this->sftp_client->put($target, $source, secSFTP::SOURCE_LOCAL_FILE);
}
/**
* @param string $filename
* @return bool
* @throws Exception
*/
protected function rmFile(string $filename): bool
{
// TODO: Implement removeOld() method.
$target = FileDir::makeCorrectFile($this->getDestinationDirectory() . "/" . $filename);
if ($this->sftp_client->file_exists($target)) {
return $this->sftp_client->delete($target);
}
return false;
}
/**
@@ -40,6 +67,7 @@ class Sftp extends Storage
*/
public function shutdown(): bool
{
$this->sftp_client->disconnect();
return true;
}
}

View File

@@ -27,12 +27,12 @@ use Froxlor\Settings;
return [
'backup_storage_add' => [
'title' => lng('backups.backup_storage_add'),
'title' => lng('backup.backup_storage.add'),
'image' => 'fa-solid fa-file-archive',
'self_overview' => ['section' => 'backups', 'page' => 'storages'],
'sections' => [
'section_a' => [
'title' => lng('backup.backup_storage_create'),
'title' => lng('backup.backup_storage.create'),
'fields' => [
'description' => [
'label' => lng('backup.backup_storage.description'),
@@ -63,7 +63,8 @@ return [
],
'destination_path' => [
'label' => lng('backup.backup_storage.destination_path'),
'type' => 'text'
'type' => 'text',
'mandatory' => true,
],
'hostname' => [
'label' => lng('backup.backup_storage.hostname'),
@@ -75,7 +76,7 @@ return [
],
'password' => [
'label' => lng('backup.backup_storage.password'),
'type' => 'password',
'type' => 'textarea',
'autocomplete' => 'off',
],
'pgp_public_key' => [
@@ -87,6 +88,7 @@ return [
'label' => lng('backup.backup_storage.retention'),
'type' => 'number',
'min' => 0,
'value' => Settings::Get('backup.default_retention')
]
]
]

View File

@@ -25,12 +25,12 @@
return [
'backup_storage_edit' => [
'title' => lng('backups.backup_storage_edit'),
'title' => lng('backup.backup_storage.edit'),
'image' => 'fa-solid fa-file-archive',
'self_overview' => ['section' => 'backups', 'page' => 'storages'],
'sections' => [
'section_a' => [
'title' => lng('backup.backup_storage_edit'),
'title' => lng('backup.backup_storage.edit'),
'fields' => [
'description' => [
'label' => lng('backup.backup_storage.description'),
@@ -64,7 +64,8 @@ return [
'destination_path' => [
'label' => lng('backup.backup_storage.destination_path'),
'type' => 'text',
'value' => $result['destination_path']
'value' => $result['destination_path'],
'mandatory' => true,
],
'hostname' => [
'label' => lng('backup.backup_storage.hostname'),
@@ -77,8 +78,9 @@ return [
'value' => $result['username']
],
'password' => [
'label' => lng('backup.backup_storage.password') . '&nbsp;(' . lng('panel.emptyfornochanges') . ')',
'type' => 'password',
'label' => lng('backup.backup_storage.password.title'),
'desc' => lng('backup.backup_storage.password.description') . '<br>(' . lng('panel.emptyfornochanges') . ')',
'type' => 'textarea',
'autocomplete' => 'off'
],
'pgp_public_key' => [

View File

@@ -34,7 +34,7 @@ use Froxlor\UI\Listing;
return [
'backup_storages_list' => [
'title' => lng('backup.backup_storages.list'),
'title' => lng('backup.backup_storage.list'),
'icon' => 'fa-solid fa-file-archive',
'self_overview' => ['section' => 'backups', 'page' => 'storages'],
'default_sorting' => ['description' => 'asc'],
@@ -45,42 +45,42 @@ return [
'sortable' => true,
],
'description' => [
'label' => lng('description'),
'label' => lng('backup.backup_storage.description'),
'field' => 'description',
'sortable' => true,
],
'type' => [
'label' => lng('type'),
'label' => lng('backup.backup_storage.type'),
'field' => 'type',
'sortable' => true,
],
'region' => [
'label' => lng('region'),
'label' => lng('backup.backup_storage.region'),
'field' => 'region',
'sortable' => true,
],
'bucket' => [
'label' => lng('bucket'),
'label' => lng('backup.backup_storage.bucket'),
'field' => 'bucket',
'sortable' => true,
],
'destination_path' => [
'label' => lng('destination_path'),
'label' => lng('backup.backup_storage.destination_path.title'),
'field' => 'destination_path',
'sortable' => true,
],
'hostname' => [
'label' => lng('hostname'),
'label' => lng('backup.backup_storage.hostname'),
'field' => 'hostname',
'sortable' => true,
],
'username' => [
'label' => lng('username'),
'label' => lng('backup.backup_storage.username'),
'field' => 'username',
'sortable' => true,
],
'retention' => [
'label' => lng('retention'),
'label' => lng('backup.backup_storage.retention'),
'field' => 'retention',
'sortable' => true,
],

View File

@@ -2296,4 +2296,39 @@ Vielen Dank, Ihr Administrator',
'config_note' => 'Damit froxlor mit dem Backend vernünftig kommunizieren kann, musst du dieses noch konfigurieren.',
'config_now' => 'Jetzt konfigurieren'
],
'backup' => [
'backup' => 'Backup',
'backups' => 'Backups',
'backups_restore' => 'Wiederherstellen',
'backup_storages' => 'Speichermedien verwalten',
'size' => 'Größe',
'created_at' => 'Erstellt',
'backup_storage' => [
'add' => 'Speichermedium hinzufügen',
'create' => 'Neues Speichermedium erstellen',
'edit' => 'Speichermedien bearbeiten',
'list' => 'Speichermedien',
'description' => 'Beschreibung',
'type' => 'Typ',
'type_local' => 'Lokales Dateisystem',
'type_ftp' => 'FTP Server',
'type_sftp' => 'SFTP auf entfernten Server',
'type_rsync' => 'Rsync zu entfernten Server',
'type_s3' => 'S3 Server',
'region' => 'S3 Region',
'bucket' => 'S3 Bucket',
'destination_path' => [
'title' => 'Ziel-Pfad',
'description' => 'Absoluter Pfad, wenn Speicher-Typ ist "lokal", sonst relativ zum Heimatverzeichnisses des Benutzers',
],
'hostname' => 'Ziel Hostname',
'username' => 'Benutzername',
'password' => [
'title' => 'Passwort oder SSH Public-key',
'description' => 'Es kann entweder ein Passwort oder ein vollständiger SSH Public-Key angegeben werden'
],
'pgp_public_key' => 'PGP Public-key',
'retention' => 'Backup-Aufbewahrung in Tagen',
],
],
];

View File

@@ -2432,7 +2432,37 @@ Yours sincerely, your administrator',
],
'backup' => [
'backup' => 'Backup',
'backups' => 'Backups',
'backups_restore' => 'Restore backups',
'backup_storages' => 'Configure storages',
'size' => 'Size',
'created_at' => 'Created at',
'backup_storage' => [
'add' => 'Add backup storage',
'create' => 'Create new backup storage',
'edit' => 'Edit backup storage',
'list' => 'Backup storages',
'description' => 'Storage description',
'type' => 'Storage type',
'type_local' => 'local filesystem',
'type_ftp' => 'FTP server',
'type_sftp' => 'SFTP to remote server',
'type_rsync' => 'Rsync to remote server',
'type_s3' => 'S3 server',
'region' => 'S3 region',
'bucket' => 'S3 bucket',
'destination_path' => [
'title' => 'Destination path',
'description' => 'Absolute path if storage type is "local", else relative to home-directory of given user',
],
'hostname' => 'Target hostname',
'username' => 'Username',
'password' => [
'title' => 'Password or ssh public-key',
'description' => 'Can be either a password or a complete SSH public key'
],
'pgp_public_key' => 'PGP public key',
'retention' => 'Backup retention in days',
],
],
];