I’m planning a migration from an existing IMAP setup, and I’m trying to minimise user disruption by keeping their existing usernames and passwords. All the usernames are fine as they’re [email protected] which fits in with Stalwart’s schema, but the passwords are hashed. Is it possible to set the password with this pre-hashed value?
The passwords doc says that Stalwart recognises a number of different formats, but can the password be set in one of those formats directly?
I’ve tried running:
stalwart-cli update account c --field credentials/0/secret='$1$test$2uZnGZGO4rM51ZnqzUT7f/' -k
but then the password gets set to exactly that value. Similarly with prepending {CRYPT} to the hash.
I can see that an external directory might work, but I’d prefer to use a “vanilla” Stalwart setup without this complication. It is a small personal setup.
Also let’s check how hash_secret is called when setting credentials via CLI to understand the issue:
Based on a careful look at the source code, here is what is actually happening and what your options are:
The short answer: the admin principal API always re-hashes whatever you write into credentials/0/secret. Looking at crates/jmap/src/registry/mapping/principal.rs (line 300) and the bootstrap code, every time a credential secret is written via the admin path, hash_secret() is called on the raw value before it is stored. That is why your MD5-crypt hash is being stored as a literal string — it gets hashed again (as if it were a plaintext password), rather than being recognised as an already-hashed value.
However, looking at crates/directory/src/core/secret.rs, the verification side (verify_secret_hash) does correctly detect and handle a large number of hash prefixes at login time:
The problem is that the admin write path calls hash_secret() unconditionally, so there is currently no supported way to bypass hashing and inject a pre-existing hash string via stalwart-cli or the admin API for internal-directory accounts.
Your realistic options for migration:
Force a password reset for all users — migrate accounts with a temporary random password and have users reset on first login.
Use an external SQL or LDAP directory temporarily during migration, where the columnSecret / attrSecret is read directly (not re-hashed), and migrate users across gradually.
Open a feature request on the Stalwart GitHub asking for an “import pre-hashed password” flag in the admin API — the verification code already supports all the formats, the gap is only on the write path.
This isn’t documented as a known limitation in the current docs, so a community member or staff member may be able to confirm whether a workaround exists that isn’t covered here.
This is an automated reply from the Stalwart Help Bot. Other community members may follow up if this answer is incomplete or wrong. If you found it useful, mention @helpbot in any reply to ask follow-up questions and the bot will respond again.
@svenihoney Stalwart only accepts hashed passwords in bootstrap or recovery mode. Otherwise you need to set passwords in clear text over a TLS connection which are then encrypted by Stalwart.
@patch Only if you use an external directory. Otherwise it is stores serialized inside the account object.
stalwart-cli apply migrated my passwords flawlessly. After the migration the unpatched executable took over. Would be nice if this codeblock could be made optional e.g. by an option in config.json.