Importing users and migrating from other servers
The importusers command creates user accounts in bulk from a file, along with a Disk VFS for every home directory or virtual folder that arrives as a path. It reads four formats:
--format | What it reads |
|---|---|
native | Syncplify's own CSV, in the classic five column layout or the named layout described below |
cerberus | The CSV export of Cerberus FTP Server |
sftpgo | The JSON backup of SFTPGo |
filezilla | A FileZilla Server configuration, of either generation (0.9.x and 1.x) |
Nothing has to be converted first: point the command at the file the other product wrote. Every format carries password hashes across as they are, so users keep the passwords they have and nobody ever handles a password in clear text.
Running the command
importusers is a verb of ss-webrest, like the commands on the command line reference. It opens the configuration database directly, so run it on a node of the installation, as root through sudo on Linux or from an elevated command prompt on Windows.
ss-webrest importusers --file users.csv --vsite <virtual site ID>| Flag | Meaning |
|---|---|
--file | The file to import. Required. --csv is the old name of this flag; it still works but is deprecated. |
--vsite | The ID of the virtual site to import into, not its friendly name. Required, except with --verify. The ID is shown on each card of the SuperAdmin Virtual sites page. |
--format | One of native (the default), cerberus, sftpgo or filezilla. |
--dryrun | Run every check and write nothing. |
--makevfs | Native format only: the classic vfs column holds a path to create a Disk VFS for, rather than the ID of an existing VFS. |
--cerberus-rounds | Cerberus format only: the PBKDF2 iteration count of the Cerberus installation. Default 5000. See below. |
--verify, --verifypass | Cerberus and FileZilla formats only: check that one known password reproduces its stored hash, and import nothing. |
What the run reports
Each account is reported by name as it is processed: imported, skipped because an account of that name already exists in the virtual site, or failed with the reason. Existing accounts are never modified. A summary line follows with the three counts, and the exit code is 0 only when nothing failed, so a script can tell how it went without reading the output.
Some accounts arrive with a warning printed against them. A warning means the account was created but something the source held could not be brought across in full: a setting with no equivalent here, a credential that is not carried, a limit whose unit the file does not state. Warned accounts are counted separately in the summary and are the ones to review afterwards.
A dry run performs the same checks as a real import, including the ones that need the database, such as whether an account or a referenced VFS exists, and reports what would happen.
What every format has in common
- Only users authenticated by Syncplify Server! itself are created, never LDAP or SSO accounts.
- A home directory or virtual folder that arrives as a path becomes a new Disk VFS named after the user. A VFS created this way is not encrypted at rest, and at rest encryption cannot be turned on afterwards: create such VFSs by hand first and reference them by ID.
- An account must end up with a way to sign in: a password, or an SSH public key with
PKIamong its authentication types. One that would have neither is refused by name, on a dry run too. - An allow list entry that cannot be translated exactly fails the account. An allow list that arrived incomplete would not be a restriction, it would be the absence of one. Trailing octet wildcards such as
192.168.1.*are converted to CIDR notation. - Paths are judged by the platform the server runs on. A Windows path on a Linux node, or a POSIX path on a Windows node, is refused with a message saying so; remap those in the file first.
- An account that fails after its VFSs were created has those VFSs removed again, so a corrected file can be run again without leaving orphans behind.
What happens to an imported password hash
An imported hash keeps the parameters the other product chose, because that is the only way it can verify, and those are almost always weaker than the PBKDF2 work factors Syncplify Server! uses. So the first time an imported user signs in successfully, over any protocol, the password is rehashed with the storage mode configured for the virtual site and the imported hash is discarded. Nothing has to be done for this to happen, and the user notices nothing.
The native CSV format
The native format has two layouts, and the command works out which one a file uses from its first row: a row that names columns selects the named layout, anything else is read as the classic layout.
The classic five column layout
Five positional columns and no header row: username, password, VFS, subsystems, permissions.
oneuser,"some password",2iSpTdEgRGPuYh0MVx2uWr5zxR4,"ssh2_scp,ssh2_sftp,ftps,ftpes,https","dirList,fileGet"The third column is the ID of an existing VFS, or, when --makevfs is given, an absolute path a Disk VFS is created for:
oneuser,"some password","/home/oneuser","ssh2_scp,ssh2_sftp,ftps,ftpes,https","dirList,fileGet"Usernames are lowercase with no spaces. Quote any field that contains a comma or a space; a password is taken exactly as written, spaces included. The last two columns are comma delimited lists of the values in the vocabulary below.
The named layout
Add a first row naming the columns, then supply any subset of them in any order. Column names are matched ignoring case, spaces, underscores and hyphens, and a column this version does not know is ignored rather than shifting the ones after it.
| Column | What it holds |
|---|---|
username | The account name. Required. user, userid and login are accepted spellings. |
password | A password in clear text. |
passwordhash | A password hash from another system, instead of password. Never both. |
mustchangepassword | true to require a new password at the first sign in. |
email | The email address. |
description | Free text. |
status | Enabled or Disabled. A disabled column holding true or false works too. |
subsystems | Comma delimited subsystems. |
authtypes | Comma delimited: Password, PKI, Keyboard-Interactive. Defaults to Password. |
allowlist | Comma delimited IP addresses and networks in CIDR notation. |
publickey1, publickey2, and so on | One SSH public key per column, in OpenSSH, RFC 4716 or PuTTY form. |
publickeyfile | A file of SSH public keys, one per line, as in authorized_keys. A relative path is resolved next to the CSV. |
homepath | An absolute path; a Disk VFS is created for it and becomes the home. |
homevfs | The ID of an existing VFS to use as the home. |
vfs, permissions | The home the classic way: an ID, or a path under --makevfs. permissions also applies to homepath and homevfs. |
vfoldername1, vfolderpath1 or vfoldervfs1, vfolderperms1 | A virtual folder besides the home, mounted under that name. Number them upward. |
username,password,email,status,subsystems,authtypes,allowlist,homepath,permissions
alice,"some password",alice@example.com,Enabled,"ssh2_sftp,https","Password,PKI",10.0.0.0/8,/home/alice,"dirList,fileGet,filePut,fileModify,fileEditMetadata"
bob,"another password",bob@example.com,Disabled,"ftps,ftpes",Password,192.168.1.0/24,/home/bob,"dirList,fileGet"homepath always means a path and homevfs always means a VFS ID, so neither needs --makevfs. A path may contain the {{username}} variable, exactly as in a parametric VFS; it is resolved when the user signs in.
Include PKI in authtypes for users who sign in with a key; otherwise the keys are stored but the account still expects a password.
Password hashes in the passwordhash column
The column takes a hash where a password is not known, written on one line with the salt and the hash in hexadecimal:
sha256$<salt-hex>$<hash-hex>
pbkdf2-sha256$<rounds>$<salt-hex>$<hash-hex>sha1, sha256 and sha512 are single pass digests over the salt followed by the password. pbkdf2-sha256 and pbkdf2-sha512 take the iteration count as the second field, so the file records everything needed to verify the hash and imports the same way in a year. For a product that concatenates the salt onto the password before running PBKDF2, instead of passing it as the salt parameter, use pbkdf2-sha256-prepend or pbkdf2-sha512-prepend.
Subsystems and permissions
Subsystems: ssh2_shell, ssh2_command, ssh2_scp, ssh2_sftp, ftp, ftps, ftpes, https, https_sharing.
Permissions: dirList, dirMake, dirRename, dirDelete, dirEditMetadata, fileGet, filePut, fileModify, fileRename, fileDelete, fileEditMetadata, symlink.
These are the values the user editor sets. An unknown value fails the row.
Cerberus FTP Server
Export the users from the Cerberus User Manager and import the CSV directly:
ss-webrest importusers --format cerberus --file cerberus-users.csv --vsite <virtual site ID> --cerberus-rounds 5000The export is read by column name, so an export from any Cerberus release imports, whatever order its columns come in.
Verify the iteration count first
Cerberus has stored passwords as PBKDF2 since their version 7.0, and the iteration count is not in the export: it is a server wide setting in their settings.xml. The default is 5000, and that is what the command assumes. If the count was ever raised, the wrong assumption is not detected at import time: every account imports, and every user is then rejected with the correct password.
Before importing, create a throwaway account in Cerberus with a password you choose, export, and verify it:
ss-webrest importusers --format cerberus --file cerberus-users.csv --verify migrationtest --verifypass "the password you chose"This reads the file, recomputes the hash and reports whether the assumed parameters reproduce it. It opens no database and needs no virtual site, so it cannot import anything. When the parameters do not match, it sweeps the plausible iteration counts and reports the one that does, together with the flag value to import with. Older exports whose passwords are salted SHA1, SHA256 or SHA512 digests are read as well; no iteration count is involved, and --verify says which kind the file holds.
WARNING
Do not skip this step. A wrong iteration count is discovered when users cannot sign in, not before.
What comes across
| Cerberus | Syncplify Server! |
|---|---|
| Username, email, description, disabled, password change required | The same |
| Allowed IP addresses | The allow list, with trailing octet wildcards converted to networks |
| Protocol FTP, FTPS, SFTP, HTTP or HTTPS | ftp; ftps and ftpes; ssh2_sftp; https. SSH shell, command and SCP are never granted |
| Virtual directories (DirName, DirPath, DirPermissions) | The first is the home, the others are virtual folders. %USER% in a path becomes {{username}} |
| Upload bit | filePut, fileModify and fileEditMetadata together, because resuming, appending and time stamping are part of an upload in practice |
| Share download or share upload bits | The https_sharing subsystem |
| Show hidden files, zip and unzip bits | Dropped, no equivalent |
| Auth SSH method | Password, public key, or both, with "password and public key" imported as requiring both |
Cerberus exports the path of a user's public key file on the Cerberus machine, never the key itself, so an account signing in with a key arrives without it. The command names those accounts at the end of the run; add their keys afterwards. Groups are not in the export and are not read.
SFTPGo
SFTPGo keeps its accounts in a data provider and exports them all as one JSON document, from the Maintenance section of its WebAdmin or through the dumpdata endpoint of its REST API. Import that document:
ss-webrest importusers --format sftpgo --file sftpgo-backup.json --vsite <virtual site ID>No verification pass exists for this format, because none is needed: SFTPGo writes every parameter of a hash inside the hash string. Their bcrypt, PBKDF2 and Argon2id hashes all come across. The Unix crypt and bare digest formats SFTPGo can hold but never writes itself are refused by name.
What comes across
| SFTPGo | Syncplify Server! |
|---|---|
| Username, email, description, status, require password change | The same |
| Public keys | SSH public keys, with PKI among the authentication types |
| Denied login methods, denied protocols | Whatever is not denied is granted: SSH becomes ssh2_sftp only, FTP becomes ftp, ftps and ftpes, HTTP becomes https and https_sharing. WebDAV has no equivalent |
| Allowed IP addresses | The allow list |
| Expiration date | Automatic disabling on that date, when it is still in the future |
| Home directory and virtual folders on local storage | The home and virtual folders, as new Disk VFSs |
| Permissions per path | Folder permissions, and permission overrides for paths inside a folder. chmod and chown have no equivalent |
What fails
An account whose home or virtual folder is not on local storage fails to import, by name and with the reason:
- Cloud and remote storage (S3, Google Cloud Storage, Azure Blob, SFTP, HTTP): the credentials are encrypted with SFTPGo's own key and are not in the dump in any usable form. Create the equivalent VFS here first, then import that account through a native CSV that references the VFS by ID.
- Their encrypted filesystem: the data on disk is ciphertext under a key held by SFTPGo. Decrypt it there before migrating.
Groups are not read. A setting an account inherits from a group is not on the account in the dump, so those accounts arrive with defaults and are worth reviewing.
FileZilla Server
Both configuration generations are read, and the command tells them apart by itself.
| FileZilla Server | Point --file at |
|---|---|
| 1.0 and later | users.xml in the configuration directory. A groups.xml beside it is read too. |
| 0.9.x | FileZilla Server.xml in the installation folder, which holds everything. |
A FileZilla Server 1.x Windows service normally keeps its configuration under C:\Windows\System32\config\systemprofile\AppData\Local\filezilla-server, or wherever its --config-dir option says. FileZilla Server 0.9.x kept its single XML in the installation folder, typically C:\Program Files (x86)\FileZilla Server.
ss-webrest importusers --format filezilla --file users.xml --vsite <virtual site ID>NOTE
The older format imports more faithfully. FileZilla 1.0 replaced ten per directory permission flags with four access modes, so a 0.9.x configuration says more about what each account may do. If both exist, import the 0.9.x one.
Passwords
| Scheme | Written by | Comes across |
|---|---|---|
| PBKDF2 with HMAC SHA256 | 1.0 and later | Yes |
| Salted SHA digest | 0.9.55 and later | Yes |
| Unsalted MD5 | 0.9.54 and earlier | No |
Syncplify Server! implements no MD5 anywhere in its password handling. An account whose password is still an MD5 hash imports without a password, is named in the report, and is then refused unless it has another way to sign in. FileZilla Server 1.x rewrites an MD5 hash as PBKDF2 the first time its owner signs in, so have those users sign in there once, export again, and their passwords come across intact.
A single 1.x users.xml may hold all three schemes at once, because FileZilla still verifies the old ones and rewrites them on sign in. That is the same arrangement described above, so a mixed file transfers as it is.
The legacy digests do not state which end of the password the salt goes on, and --verify settles it with one disposable account, exactly as for Cerberus:
ss-webrest importusers --format filezilla --file users.xml --verify migrationtest --verifypass "the password you chose"What comes across
- FileZilla Server serves FTP only, so every account arrives with
ftp,ftpsandftpes, never withssh2_sftp. A 0.9.x account marked ForceSsl loses plainftp, as it had it there. If the configuration came from FileZilla Pro Enterprise Server and its accounts used SFTP, add that subsystem afterwards; the command says so once at the start of the run. - 1.x mount points become the home (the one at
/) and virtual folders. Read only, read write and write only access modes map to the matching permissions; creating, deleting and renaming directories is granted only where the mount point allowed structure modification. - 0.9.x directories become the home (the one flagged as such) and virtual folders named after their aliases. A directory with no alias was unreachable in FileZilla too and is not imported. An account's own settings win over its group's, which fills in only what the account lacks, as FileZilla itself resolved them.
- Allowed IP addresses become the allow list.
- A 1.x account without the
enabledattribute is imported disabled and told so. An import may leave an account switched off by mistake, never switch one on. - Speed limits, session limits and open file limits are reported and not imported: the file does not state their unit, and a limit imported off by a factor of 1024 either throttles an account to uselessness or lifts the restriction, both silently. Set them on the account here if they still apply.
- 1.x group membership is not inherited. FileZilla does not document the element, and an unread setting is reported per account rather than guessed at.
What fails
- A disabled mount point. It is an explicit denial, usually of a subdirectory whose parent another mount point exposes. Skipping it would leave that path reachable.
- A per account deny list. There is a per user allow list and no per user deny list here, so the restriction would be lost. Ban those addresses on the virtual site's Shield, remove the entry in FileZilla, or create that account by hand.
- No mount point at
/(1.x), or no directory flagged as home (0.9.x). FileZilla synthesizes a root listing from the other mount names; there is no equivalent here to a home that is not a directory. - An address that cannot be translated, as in every format.
Before version 8.1.1
Syncplify Server! 6.2.50 through 8.1.0 ship the original command: the file flag is --csv, there is no --format, only the classic five column layout is read, and only password authentication comes across. --vsite, --makevfs and --dryrun work as described. The feature does not exist before 6.2.50.
