Skip to content
  • Categories
  • Recent
  • Popular
  • Tags
  • Users
  • Groups
  • Documentation
  • Downloads
  • Report a bug
  • GitHub mirror
  • Paid support
Skins
  • Light
  • Brite
  • Cerulean
  • Cosmo
  • Flatly
  • Journal
  • Litera
  • Lumen
  • Lux
  • Materia
  • Minty
  • Morph
  • Pulse
  • Sandstone
  • Simplex
  • Sketchy
  • Spacelab
  • United
  • Yeti
  • Zephyr
  • Dark
  • Cyborg
  • Darkly
  • Quartz
  • Slate
  • Solar
  • Superhero
  • Vapor

  • Default (No Skin)
  • No Skin
Collapse
hMailServer Community Forum
hMailServer 6.3.8 is the current release, signed 2 November 2026. Windows and Linux, and an in-place upgrade from 5.3 or later, but for the original hMailServer's 5.7.Download 6.3.8 Documentation FAQ
  1. Support and discussion for hMailServer
  2. Running hMailServer
  3. Webmail, portal & Control Deck
  4. Signing in: passwords, second factor, passkeys, single sign-on, recovery, devices

Signing in: passwords, second factor, passkeys, single sign-on, recovery, devices

Scheduled Pinned Locked Moved Webmail, portal & Control Deck
securitywebmailportalcontrol-deckupgrade
1 Posts 1 Posters 137 Views 1 Watching
  • Oldest to Newest
  • Newest to Oldest
  • Most Votes
Reply
  • Reply as topic
Log in to reply
This topic has been deleted. Only users with topic management privileges can see it.
  • ProgressiverobotP
    ProgressiverobotP
    Progressiverobot
    hMailServer Team
    wrote on last edited by chris
    #1

    Written for 6.3.4. hMailServer 6.3.7 (17 October 2026) refuses a state-changing REST request that carries a credential - HTTP Basic included, which a browser keeps once it has answered the sign-in box - when the browser reports it as cross-site (Sec-Fetch-Site, or an Origin that is not this server's), and holds the password-reset request to the same rule, so another site cannot have a current browser drive it; the webmail and the Control Deck act on their own origin, and scripts, hmctl and the PowerShell module send no such header, so none of them is affected. SMTP, POP3 and ManageSieve now refuse a SASL PLAIN sign-in that asks to act as another account. See the 6.3.7 announcement. hMailServer 6.3.8 (2 November 2026) signs a name in to an account only when it is the account's stored address, compared in lower case after a domain alias and the default domain, on every protocol, the webmail, the REST API and COM, and for an OpenID Connect or SAML sign-in, so that the per-name lockout and the compromised-account hold can no longer be got round with another spelling of the name: a client that signs in with another spelling, in more than the case of the letters A to Z, or with white space at either end, may no longer sign in, since such a name fails as a name that does not exist fails. A failed account sign-in at the REST listener through a reverse proxy on the same machine, like a failed CalDAV, CardDAV or Autodiscover sign-in from loopback or an address in WebServicesTrustedProxies, is counted against the name alone (AccountLockoutThreshold, off by default), never towards a ban of the proxy's address. See the 6.3.8 announcement.

    This topic covers how people prove who they are to hMailServer 6.3.4: what 6.3.3 already had, and what 6.3.4 adds. The one thing to know first is that there are two front doors. The mail protocols - IMAP, POP3, SMTP submission, CalDAV and CardDAV - take a password: the account's own, an app password, or an OAuth2 bearer token where that is switched on. The two pages - the webmail at /portal and the Control Deck at / - take the password as well and, since 6.3.4, a passkey or a sign-in at the organisation's identity provider. Operators upgrading should read the note on passwords with characters above U+00FF first.

    What 6.3.3 already had

    Feature What it is
    A second factor for an account A TOTP secret on the mailbox. Once one exists, the account's password stops being a credential wherever a code cannot be presented alongside it - every mail client, and the webmail's sign-in - and app passwords are what those use
    App passwords One per device, shown once, revocable one at a time; they work for directory-linked accounts too and are untouched by password expiry
    A second factor for the administrator Enforced by the server on every surface - COM, REST, the Control Panel
    The password policy Length, mixed case, digits, symbols, a common-password list, history and maximum age
    LDAP and Active Directory Passwords checked against the directory; provisioning from it on a preview-then-apply
    OAuth2 bearer tokens on the protocols SASL XOAUTH2 and OAUTHBEARER validated against an identity provider's published keys

    There is no self-service enrolment of an authenticator app in the webmail: an administrator sets and clears the second factor, over COM and the Windows Control Panel. Create the app passwords first and enrol the secret second, or every configured client stops working with no way back in.

    The breach check (since 6.3.4). A password being chosen, never at logon, can be refused because it is in the published corpus of breached passwords; the check is off by default, because it sends five characters of the password's SHA-1 hash off the server, and its settings are in Hardening a public mail server.

    Four more changes in 6.3.4. The administrator password, which the Windows installer wrote as an unsalted MD5 digest, is written again as PBKDF2 at the first successful administrator sign-in (a file the service cannot write keeps the old value and says so once, HM7494). An account's TOTP secret is sealed at rest once the installation has a stored-secrets key. A mailbox encrypted at rest takes its own password only: no TOTP secret can be enrolled on it, and its app passwords are refused. And a room or a piece of equipment cannot be signed into by any credential.

    Upgrading to 6.3.4: passwords with characters above U+00FF

    Before 6.3.4 every password hash was made over a conversion that gave NUL bytes on Windows from the first character above U+00FF on, and ? for each such character on Linux, so a password of Cyrillic, Greek or CJK letters could be opened by any other password of its length. Hashes are now made over UTF-8, which leaves every ASCII password's hash as it was. The consequence, item 24 of the CHANGELOG's upgrade list: a password hash stored before 6.3.4 of a password with a character above U+00FF is refused, to its own password too, and the application log names the account. An administrator sets such a password again, and soon: until then SCRAM-SHA-256, whose client derives its key itself, still opens such a PBKDF2 hash to the NUL bytes (Windows) or question marks (Linux) it was made over, which the server cannot tell from the password; and on Linux such a hash also still opens to the password written with ? for those characters. Every ASCII and Latin-1 password signs in as before.

    The same release fixed SCRAM-SHA-256 having accepted the account's own password for an account with a second factor enrolled, and a password past PasswordPolicyMaximumAgeDays; each is now a forced failure of the exchange. SCRAM is always the account's own password, since an app password's hash is not one SCRAM can derive from, so such an account cannot sign in over SCRAM at all.

    Passkeys

    A passkey (schema 6077) is a WebAuthn credential held by the person's own device, such as a phone or a security key. The private half never leaves it; the server keeps the public half. Passkeys are offered on a stock installation, and nobody has one until they enrol it.

    Enrolling. The webmail's Security page has a Passkeys card, and the Control Deck has a Passkeys view for the administrator's own. Both ask for the owner's password again, with the one-time code in X-hMailServer-OTP where a second factor is enrolled, or for an account one of its recovery codes in X-hMailServer-Recovery-Code; a request that was itself authenticated with an app password is refused outright. At most ten per owner.

    POST   /api/v1/passkeys/options    what the browser needs to make one
    POST   /api/v1/passkeys            the credential it made
    GET    /api/v1/passkeys            the ones this credential holds
    DELETE /api/v1/passkeys/{id}       remove one
    

    Listing and removing are answered while passkeys_enabled is off as well.

    Signing in. POST /api/v1/passkeys/challenge hands out thirty-two random bytes, kept in memory for five minutes and spent on first use; the assertion goes to POST /api/v1/session, the one place this server mints a browser session, so the sign-in gets the same ceilings and a device-list entry marked passkey. Nobody types an address first. A passkey stands in for the one-time code: user verification - a PIN, a fingerprint or a face - is required at registration and on every assertion. It carries nobody past an inactive account or domain, the compromised-account hold or the domain's country policy.

    Behind several names. By default the relying party id is the Host header the page was served on. passkey_relying_party set to a registrable parent (example.com) lets a passkey made at mail.example.com work at webmail.example.com. With it set, passkey_allowed_origins must be set too, comma separated as the browser writes them (https://mail.example.com, https://webmail.example.com); until it is, no passkey is registered or taken.

    Not verified: the attestation statement, so an installation cannot say "only hardware keys of this make". Not in the backup: enrol the device again after a restore elsewhere. Registering and removing a passkey is on the audit trail.

    Single sign-on

    Since 6.3.4 a person can sign in at the organisation's identity provider instead, by OpenID Connect or by SAML 2.0 (for a provider such as ADFS that speaks only SAML). Once it is set up, both sign-in pages draw a button: Sign in with and the issuer's host name, or Sign in with SAML. It opens the webmail and the Control Deck and nothing else: IMAP, POP3 and SMTP still take the password or an app password. An identity provider that says somebody is an administrator is not obeyed; the Deck opens as the administrator only for an address listed in oidc_administrators or saml_administrators. There is no logout in either direction, so a person disabled at the provider keeps a session already started here until its own ceiling ends it, which is what the session policies below are for. The settings, the Entra ID, Okta and ADFS entries and the flows are in SCIM provisioning and single sign-on.

    Account recovery

    Three independent parts (schema 6076): a recovery address and ten one-time codes, which the person sets on the webmail's Security page, and the forgotten-password flow on the sign-in card. Setting either asks for the account's own password again - never an app password - and the second factor where there is one.

    What it needs. account_recovery_enabled is on by default and inert on a stock installation: links are written against the server's public base URL - confidential_base_url, falling back to quarantine_digest_base_url and then the import sign-in's import_oauth_redirect_base_url - and with none set POST /api/v1/password-reset answers 503. recovery_reset_link_minutes (30; 5 to 1440) is how long a reset link lives, and recovery_verify_link_hours (24; 1 to 168) how long the confirmation link lives.

    The recovery address counts for nothing until the link mailed to it is followed, on a page with a button rather than on the GET, which a mail scanner would follow first. It is never answered in full again, to anybody: [email protected] reads as [email protected]. The confirmation is metered at one a minute and six an hour for the address, and again for the asking account, answering 429 with retry_after.

    The ten one-time codes are made together, shown once and stored hashed. A code goes in X-hMailServer-Recovery-Code wherever an authenticator's code is asked for: POST /api/v1/session, POST /api/v1/me/password, POST /api/v1/me/app-passwords, PUT /api/v1/me/recovery and POST /api/v1/me/recovery/codes. The password is verified before the code is looked at, and the code is spent only once the whole attempt has been accepted. Every owner has a budget that needs no setting: five wrong codes in fifteen minutes, and for the next fifteen every code for that owner is refused - the right one too - with 429 and code second_factor_locked. And a code is taken once (RFC 6238, section 5.2): an accepted TOTP code is refused if it comes again within its window, without being counted as wrong. A script that sent the administrator's password and a code with every request signs in once instead, or uses an API key.

    Forgotten your password? on the sign-in card posts the address to POST /api/v1/password-reset, which takes no credentials and answers always the same - 202 and one sentence, in the same time - whether or not the address is a mailbox with a confirmed recovery address. Since 6.3.7 a request a browser reports as cross-site (Sec-Fetch-Site, or an Origin that is not this server's) is refused, so another site cannot have a current browser drive it. The limits are one request a minute and six an hour per address, and twenty an hour per client address; a loopback peer, such as a reverse proxy on the same machine, is held to thirty in any sixty seconds instead. The link goes to the confirmed recovery address and nowhere else, is good once, and dies after recovery_reset_link_minutes. Completing it applies the password policy, the reuse history and the breach check, ends every browser session of the account, tells the mailbox and the recovery address, and raises the account.password-reset alert. A used, expired or unknown token answers 410.

    The administrator can see whether an address is set and confirmed, its masked spelling and how many codes are unused - over REST, on the Deck's Recovery card and the Windows Control Panel's Recovery tab - and can clear either: DELETE /api/v1/accounts/{address}/recovery and DELETE /api/v1/accounts/{address}/recovery/codes. The administrator cannot read the address in full, read a code, set an address on somebody's behalf or generate codes for them. A recovery address is exactly as strong as the mailbox that receives it: a convenience, not a second factor, and an installation that cannot accept that sets account_recovery_enabled to 0. Recovery does not enrol a new authenticator either. Nor does a reset link open encrypted mail: for a mailbox encrypted at rest the link is refused before it is spent and the person is sent to their administrator, and for a zero-access mailbox the password it sets does not open the key, so at the next sign-in the page asks for the previous password or the recovery key.

    Devices and sessions

    Every client that has authenticated to an account is one entry of a list: the protocol (imap, pop3, smtp, web, dav, autodiscover or managesieve), the address it came from, the user agent where the protocol carries one, the credential it used, the app password's name where one was used, and when it was first and last seen. IMAP's ID command is deliberately not used, so two mail programs on one machine over IMAP are one entry. At most fifty entries are kept per account.

    Surface Read Revoke
    Webmail Settings → Security, the Devices card Revoke beside the entry; the box under the table adds the app password
    REST, the account GET /api/v1/me/devices DELETE /api/v1/me/devices/{id}, with ?credential=revoke and ?forget=true
    REST, an administrator GET /api/v1/accounts/{address}/devices DELETE /api/v1/accounts/{address}/devices/{id}
    COM (Windows) Account.GetDevices() Account.RevokeDevice(id, revokeCredential)
    Control Deck The Devices view Revoke, with the app password box
    hmctl hmctl device [email protected] list hmctl device [email protected] revoke ID [--credential] [--forget]
    PowerShell Get-HmDevice -Address [email protected] Revoke-HmDevice -Address [email protected] -DeviceId ID [-Credential] [-Forget]

    What revoking does, exactly. A browser session's token is invalidated, so the next request on that cookie is 401; a live IMAP, POP3 or SMTP connection is dropped where it stands; and with credential=revoke the app password is deactivated - not deleted, so the person can put it back if the phone turns up. CalDAV, CardDAV and ManageSieve clients have no connection to drop, so revoking one means revoking its app password. A revoke is not a ban: a client with a credential that still works signs in again and clears the mark. The account's own revoke is refused (403) to a request that came in on an app password, and an administrator's revoke is recorded in the audit trail. The list, hm_signins, is not in the backup.

    The session policies govern every signed-in browser - the webmail, the portal and the Deck - server-wide and per domain, and are re-read at every request. A mail program's connection is not a session in this sense.

    Policy Setting Unset means
    Idle timeout session_idle_seconds 1800 seconds; at most 2592000 (thirty days)
    Absolute lifetime session_lifetime_seconds 43200 seconds, twelve hours; at most 2592000
    One session per device session_one_per_device (a domain's: 0 the server's answer, 1 on, 2 off) Off
    Bound to the address it began on session_bind_to_address (0, 1 or 2 as above) Off - it ends a phone's session when it moves from wi-fi to mobile
    hmctl settings set --set session_idle_seconds=900 --set session_bind_to_address=true
    hmctl domain update example.com --set session_lifetime_seconds=28800 --set session_one_per_device=1
    

    A domain's value wins over the server's where it is set. Nothing changes on the day you upgrade: every policy reads 0, which means the ceilings the server has always had. The cookie, hmailsession, is HttpOnly and SameSite=Strict, and Secure whenever the browser came over HTTPS; a proxy that terminates HTTPS in front of the loopback listener should set X-Forwarded-Proto: https. Browser sessions live in the server's memory, so a restart ends every one, and a password change ends every session and drops every connection of the account.

    What none of this does

    • It does not reach the mail protocols. A phone's IMAP and SMTP still take the password or an app password, and an account that only ever signs in through its identity provider needs a password set before its person can mint an app password.
    • No password-less accounts: a passkey sits beside the password, and no policy forbids the password.

    Where to read more

    • Wiki: Sign-in and Identity, Connecting a Mail Client, Security Hardening.
    • Documents: PasskeysAndSingleSignOn.md, AccountRecovery.md, DevicesAndSessions.md, PasswordBreachCheck.md, and item 24 of the 6.3.4 section of the CHANGELOG.
    • The public documentation: 17.2 Modern authentication.
    • On this forum: Connecting a mail client for app passwords on a phone, Active Directory and LDAP sign-in, encryption at rest, Upgrading to 6.3.4, The Control Deck, The webmail portal at /portal and the 6.3.4 announcement.
    1 Reply Last reply
    0

    Hello! It looks like you're interested in this conversation, but you don't have an account yet.

    Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.

    With your input, this post could be even better 💗

    Register Login
    Reply
    • Reply as topic
    Log in to reply
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes


    hMailServer Community

    The community support forum for hMailServer — the free, open-source mail server for Windows and Linux — maintained by Progressive Robot Ltd. The software is AGPL-3.0 and stays that way; this is where it is discussed, debugged and documented.

    The software

    • Documentation
    • Downloads
    • hMailServer 6.3
    • Source on GitLab

    Getting help

    • Forum rules
    • About this forum
    • Support plans
    • Pay monthly support
    • Talk to Progressive Robot

    Legal

    • Privacy policy
    • Terms and conditions
    • Cookie policy

    Progressive Robot Ltd · 220 Chester Business Park, Heronsway, Chester, Cheshire CH4 9GB, United Kingdom · +44 (0)1244 911212 · [email protected] · VAT 506152326

    Powered by NodeBB

    • Login

    • Don't have an account? Register

    • Login or register to search.
    • First post
      Last post
    0
    • Categories
    • Recent
    • Popular
    • Tags
    • Users
    • Groups
    • Documentation
    • Downloads
    • Report a bug
    • GitHub mirror
    • Paid support