Skip to content

The Service Account

Christopher Holloway edited this page Oct 21, 2026 · 10 revisions

The service account

Source of truth: hmailserver/docs/ServiceAccount.md, kept in the repository and copied here. Copied on 27 September 2026 from the document as the tag v6.3.4 (51e2dbda0) has it, and again on 5 October 2026 from the document as the tag v6.3.6 (e3c448aa6) has it (ServiceAccount.md at v6.3.6): the live update under this account and the administrator's install from the Control Panel (AdminUpdate, HM7465 to HM7469), the service's login on SQL Server (hMailServer.exe --grant-database-login), a new installation whose database step did not complete, what an administrator's command writes and which links it goes through, /GrantServiceAccess's exit code 7, the program folder's folders made private (hMailServer.exe /ProtectFolders, with --report-only written before the verb), and a command run as root on Linux. Copied again on 18 October 2026 from the document as the tag v6.3.7 (31d656c0c), the release, tagged and published on 17 October 2026, has it (ServiceAccount.md at v6.3.7): /ProtectFolders, which setup runs at every installation and upgrade, hardens the program folder, Bin and ControlPanel too, which until 6.3.7 took their permissions from the folder they are in, so that on an installation at a drive's root any account that could sign in could change the programs - with the steps to take before upgrading such an installation, and what is left for a CHECK line to name; a command run as root on Linux writes no report but the stored-secrets code's HM7496 and HM7497, which go to standard error, where v6.3.6's text says every report goes there and this copy has said so since 5 October 2026; and a server on 6.3.4 or 6.3.5 under this account cannot install 6.3.6 or a later release by its live update (From 6.3.4 and 6.3.5), as this copy said from 17 October 2026. The document is new in 6.3.4, and for Windows: the Linux packages have always run the server as a user of its own. Security Hardening §17.3 is the short version. Re-copied on 20 October 2026 from the document as master has it from batch50, 0c0b6bae7 (ServiceAccount.md at 0c0b6bae7): in Reinstalling into a new folder, step 2 changes the settings file's other paths into the old installation - PostgreSQLSslRootCert and TlsCaFile in [Database], UpdateTrustRootsFile and UpdateLogPublicKeyFile in [Settings], and a path in [SettingsOverride] - to copies where nobody but the administrators may change them, and deletes the lines the site did not set in [Directories], among the live update's five trust settings, in [SettingsOverride], in [LDAP] and in the sending limits, and step 4 keeps the old folder private to SYSTEM and the Administrators until it is deleted; and the live update's internal tests name the signed installers that must pass the publisher pin there as 6.3.1 and 6.3.3 to 6.3.5, where they said 6.3.1 to 6.3.5. Re-copied on 21 October 2026 from the document as master has it from batch51, 5e49abf06 (ServiceAccount.md at 5e49abf06), which changes it in two places: Reinstalling into a new folder says that where nobody but the administrators could have changed the old folder's programs, Relocating an Installation moves the whole installation instead, its programs copied with it, its step 6 running /ProtectFolders on the copy and its step 11 running setup at the new folder; and the table of what writes hMailServer.INI under the virtual account says that the application log's line on the file read-only to the service is written at a reinitialise where application logging is on, with the settings whose line there is older than the stored value counted and named, and dropped at the service's start, which reconciles the settings store before the server has read its logging settings, and that an edit is reported once, as HM5804, where it said that the line was written once per start and an edit named once. Its links to the repository's other documents point at them as 5e49abf06 has them, or at this wiki's copy where there is one.

The Windows service reads mail from anybody on the internet, in C++, and for as long as this project has existed it has done that as LocalSystem, the most privileged account on the machine. A flaw reachable through SMTP, IMAP or POP3 was a flaw with full control of the computer. Since 25 September 2026 a new installation runs the service as NT SERVICE\hMailServer instead: the service's own virtual account, which Windows makes from the service's name - no password, no account to create - and which reaches only the folders it is granted. The Linux packages of this same server already run as a user of their own (hmailserver.service: User=hmailserver); this is the Windows half.

What this page covers: what the installer asks and does, what the account is granted and by what, why nothing the service can write decides that, what the service may no longer write - hMailServer.INI - and what became of each thing that wrote it, what the other users of the computer may read of its folders, and change of its programs (/ProtectFolders), the table of everything LocalSystem could do that the virtual account cannot, the live update's answer, where the Control Panel, the Control Deck and the checklist say which account the service runs as, and how to go back.

What it does not cover, because it stays declined: an installation under an ordinary account for shared hosting (discussion #267). The secret store is machine-scope DPAPI (DataProtector.cpp, CRYPTPROTECT_LOCAL_MACHINE), which anyone with administrator rights on the machine can read; the COM registration is machine-wide; and the message store is guarded by file permissions that an ordinary user cannot set against the other users of the same machine - who, on shared hosting, are the other tenants.

The rule the rest follows

A service that can make an elevated program act on something it chose has given back what the account took away. Three elevated programs act on this service's behalf - the installer, hMailServer.exe /GrantServiceAccess and hMailServer.exe /Register - so none of them takes a folder, a file or an account from anywhere the service can write:

  • hMailServer.INI is not the service's to write. It holds [Database], [Directories] and [Security] - how the server reaches its database, where its folders are, and who may administer it - and the elevated programs read it. The service runs on it read-only (below).
  • The grant takes its folders from trusted places alone: where the program is, and a list only administrators may write. A folder the configuration names is reported, never granted, until an administrator approves it after a question that names it.
  • /Register takes the account from its command line alone, never from ServiceAccountName.

What the installer does

The installer has a page for it, after the server profile, whenever the server component is installed. It says which case applies:

The machine The page offers The default
No hMailServer service yet - a new installation; or the LocalSystem service a new installation left when its database step did not complete (below) NT SERVICE\hMailServer, or LocalSystem as earlier versions ran it NT SERVICE\hMailServer
A service running as LocalSystem - an upgrade keep LocalSystem, or move it to NT SERVICE\hMailServer keep LocalSystem
A service running as any other account - an upgrade keep that account, or move it back to LocalSystem keep the account

An upgrade never changes the account unless the page is answered to. A silent installation takes the same choice as /serviceaccount=virtual, /serviceaccount=localsystem or /serviceaccount=keep; without it, a new installation gets the virtual account and an upgrade keeps what it has. When the database is SQL Server reached with Windows authentication, the move page says that the moved service needs a login there, which setup gives it first (step 4).

The order is the point:

  1. The service is registered - hMailServer.exe --service-account <word> /Register - as LocalSystem when it is to be moved (a new installation, or an upgrade of a LocalSystem service that chose the move), and as its chosen or kept account otherwise.
  2. A service that already runs as an account of its own is granted its folders again, for anything this version added.
  3. The database step runs - DBSetupQuick for the built-in database, DBSetup for an external one - and the chosen profile is applied. That step writes [Database] into hMailServer.INI through the service, over COM, and sets the service's dependency on a local database server; LocalSystem may do both, and the virtual account may do neither.
  4. For a move, when the database is SQL Server reached with Windows authentication, hMailServer.exe --grant-database-login gives NT SERVICE\hMailServer a login there, a user in the hMailServer database and db_owner (below) - as the administrator running setup, on a connection of its own, while the service is still LocalSystem's. The database step made or opened the database as LocalSystem; the moved service signs in as the virtual account, and without these it cannot open the database. When they cannot be given on a database this installation set up, the service is not moved: it stays on LocalSystem, working, the message gives the statements a SQL Server administrator runs, and setup ends with 10 (Upgrading.md, "If the upgrade fails"). Then the service is stopped and hMailServer.exe /GrantServiceAccess runs - with --confirm in an attended installation. By now the database names every certificate and key file, so the grant can say which are outside the folders it grants. Only when it ends in 0 is the service registered as NT SERVICE\hMailServer. Anything else - a folder nobody approved in a silent installation, a folder that can never be granted, No to the question, a grant that failed - leaves the service on LocalSystem, working, and says why; the installation still succeeds. A database step that failed leaves it on LocalSystem as well, and a new installation's leaves the mark that lets it be finished later (below).
  5. The service is started. A service this installation has just moved is then held to opening its database as NT SERVICE\hMailServer - the database's version is read through it, as the installer reads it for an upgrade - since a service reports itself running before it has opened anything. One that did not start, or did not open its database, is moved back to LocalSystem and started again, and setup says why and ends with 10 (11 when its database step changed the database): until 6.3.6 such a server, which listened on no port, ended setup with 0.

A grant that fails at step 2 fails the installation - that service may not start

  • and says so in its own words, not the database's.

A new installation whose database step did not complete - DBSetup refused a setting, the database server refused the sign-in, the wizard was closed - has its files, a service registered as LocalSystem for the database step, and no database; none of what follows that step has happened: the secure defaults (Secure Defaults), the profile, the stored-secrets key (Secrets at Rest) and the move. Setup ends with 10 and leaves a mark, NewInstallationPending (the account chosen, virtual or localsystem) and NewInstallationProfile (the profile, if one was) under HKEY_LOCAL_MACHINE\SOFTWARE\hMailServer in the 32-bit view, writable by administrators only. Either way the message offers then finishes it as a new installation:

  • Run setup again, with the setting corrected. It finds the mark - or the LocalSystem service beside a hMailServer.INI that names no database, which an earlier version's setup leaves - and sets it up as a new installation's: the pages offer, and a silent installation defaults to, the account and the profile the mark holds (NT SERVICE\hMailServer without one), /serviceaccount= and /profile= choosing otherwise, and the steps after the database run as for any new installation. A LocalSystem service whose hMailServer.INI names a database, or whose file is not found, is an upgrade's, and keeps LocalSystem unless the move is chosen. On the built-in database the same holds: setup run again finishes the installation, where DBSetupQuick.exe would set the database up and nothing after it, so the message after a new installation's failed step names setup.
  • Run DBSetup.exe as an administrator from the Bin folder - its wizard, or /unattended with the settings in its environment (WindowsOnArm.md). With the mark there, once the database is set up it runs the installer's steps itself, in the installer's order, with hMailServer.exe's verbs: the service stopped, --apply-new-install-defaults, the profile (--apply-profile NAME --once --if-new --confirm), --create-stored-secrets-key, and for the virtual account --grant-database-login, the grant and --service-account virtual /Register; then the service started and, when moved, held to opening its database, moved back to LocalSystem when it does not. Each step is said as it goes, and the mark is removed - kept only when the service would not stop, so that none of them ran. The wizard asks Windows for elevation itself when the mark is there; /unattended without elevation refuses, before it changes anything, with exit code 2.

A new installation whose administrator cannot open the database - SQL Server with Windows authentication, where the database step made the database as LocalSystem, a SQL Server administrator, and the person running Setup is none - has its database, and --apply-new-install-defaults (exit 2) and --create-stored-secrets-key (exit 1), run as that person, could not open it. Until 6.3.6 neither was ever applied: the recovery the message gives - the statements run by a SQL Server administrator, then Setup again - runs as an upgrade. Now Setup, and DBSetup finishing an installation, start the service with the argument that has it apply them itself: sc start hMailServer --apply-new-install-defaults. The service opens its database, applies a new installation's defaults with the verb's own rules - the database's NewInstallDefaultsPending mark required, the shape of a new database, the mark cleared once decided - and, for the new installation it found and applied them to, makes the stored-secrets key when it runs as LocalSystem, which may write hMailServer.INI (the virtual account may not, and says so, HM7498), before its listeners start. What it could not do is in the error log (HM7498, HM7510, HM7511), each step in the application log when that is on, and the Control Deck's setup checklist shows the outcome. An administrator can start it so at any time after; the mark is acted on whatever its age, since the argument is the request. The profile chosen is not applied this way: the Control Deck's checklist offers it.

Setup clears the mark before its own database step, so that the DBSetup it runs leaves those steps to it, and writes it again only if that step does not complete either; the uninstaller removes it. An update hMailServer.Updater runs - the live update, or Install update in the Control Panel - leaves the mark alone and upgrades the installation as it is: unattended, it keeps the account and changes nothing a new installation would, and its database step is held to the version before it, as any upgrade's is. Until 6.3.6 setup run again kept the service on LocalSystem, and DBSetup set the database up and nothing else - no self-signed certificate (the database kept its NewInstallDefaultsPending mark), no stored-secrets key, LocalSystem (the installer smoke VM, 2 October 2026).

Every registration of an account other than LocalSystem also sets the service's SID type to unrestricted, so that the service's token carries NT SERVICE\hMailServer whatever account it logs on as - which is what lets one set of grants serve the virtual account and a group managed service account alike. A registration that leaves the service on LocalSystem leaves the SID type alone.

What the account is granted: hMailServer.exe /GrantServiceAccess

Run elevated - by the installer, by the Control Panel's buttons, or by hand from an elevated Command Prompt in the Bin folder. It grants the service's SID, NT SERVICE\hMailServer (S-1-5-80-3504853920-2627644491-1723263464-2546768561-1700046809, derived from the name as Windows derives it), exactly this:

Where What Why
The program folder's own Data, and everything in it Modify The message store, and its ACME, Certificates, DKIM, Discovery, Files, Recoverable, Blobs, Lists, Updates and other folders
The program folder's own Logs, and everything in it Modify The logs, and the crash dumps and the crash record, which are written there
The program folder's own Temp, and everything in it Modify Messages being received and scanned, and the virus scanners' copies
The program folder's own Database, and everything in it Modify The built-in SQL Server Compact database
Each folder approved in HKEY_LOCAL_MACHINE\SOFTWARE\hMailServer\ServiceAccessPaths, and everything in it Modify, or Read A data, log or backup folder, an archive folder, a certificate folder elsewhere - what an administrator said the service may reach
Each other folder in the program folder - Bin, DBScripts, Events, Languages, WebAdmin, Addons and the rest - and everything in it Read The program, its scripts, its languages and the Deck. Not the program folder itself: every service holds bypass traverse checking, so the account reaches the folders in it without, and an entry on it would make Windows walk everything below it - the message store included, a tree the service writes in once it has moved
Each file with protected permissions in the DKIM key folder and the Updates folder, when those are in a folder granted Modify Modify, on the file The DKIM rotation's keys and the live update's token are written with permissions of their own (ProtectedFile) that inherit nothing, so a service moved off LocalSystem would otherwise lose the keys it signs with
A certificate or key file the configuration names inside a folder above, whose own permissions keep that folder's grant from it Read, on the file TLS and DKIM signing
Bin\hMailServerApiKeys.ini Modify, on the file The REST API's key store, which the service writes when a key is made or revoked. Made empty when it is not there yet - which the REST API reads as no keys - so that the service can write into it without being able to create files in Bin. It is the one file there the service may write, because nothing elevated takes a path, an account or a command from it: it holds key hashes the REST listener checks, and the service could mint a key through its own API anyway
hMailServer.INI Nothing Read-only to the service - see below

The program folder is where the program is: the folder above the Bin folder hMailServer.exe runs from, not [Directories] ProgramFolder. A binary that is not in a folder named Bin grants nothing (exit 2).

What the configuration names is only reported. The folders of [Directories] (DataFolder, LogFolder, TempFolder, DatabaseFolder for the built-in database, EventFolder), the backup folder, ArchiveDir and AcmeCertificateDirectory, and the certificate and key files - the SSL certificates, the domains' DKIM keys, the REST API's, the metrics listener's and the web services' - are read from the file and from the database (the settings from hm_inisettings itself, not the file's copy), and each is checked against the grants above. One the grants reach needs nothing. Otherwise:

  • a folder nobody has approved is listed NOT APPROVED;
  • a certificate or key file is listed OUTSIDE, with the way to fix it: move it into Data\Certificates, which the data folder's grant reaches, or approve its folder for Read. It is never given Read where it is: the configuration could name any file on the machine;
  • a folder that may never be granted is listed REFUSED, with why;
  • a backup folder on a network share is noted: the service reaches a share as the computer account (DOMAIN\COMPUTER$), and that is granted on the share.

Approving a folder. --approve-modify "<folder>" or --approve-read "<folder>" on the command line; or --confirm, with which the verb asks - in a box of its own, on the desktop of the elevated process, which nothing running at a lower integrity can answer - naming each folder not approved, each key file outside and each refusal, before it changes anything. Yes approves the folders, No changes nothing at all. An approval is recorded in HKEY_LOCAL_MACHINE\SOFTWARE\hMailServer\ServiceAccessPaths (64-bit view), one value per folder - its full path as the name, Modify or Read as the data - in a key the verb makes owned by Administrators and writable by Administrators and SYSTEM alone. If anybody else may change that key, or owns it, nothing in it is granted and the run fails with the reason. Approve only a folder you recognise: the service may then write in it.

Never granted, approved or not, a folder that:

  • is not a full path on a fixed local drive - a share, a device path (\\?\, \\.\), a relative path or a stream;
  • is a drive's root;
  • is, or holds, the program folder; or is in it but outside its own Data, Logs, Temp and Database - Bin above all;
  • is, holds or is in the Windows folder, a Program Files folder, the user profiles folder or ProgramData\Microsoft (every user's Start menu and startup folder);
  • is or holds ProgramData (a folder of its own in it may be approved);
  • is, or runs through, a junction, a symbolic link or a mount point that the administrators do not alone control - one the service's account, or anybody else who is not an administrator, made or could turn elsewhere. The refusal names who may change the link, or who owns it.

Every path is resolved first - .. taken out, short names made long - so none of these can be named another way.

A link the administrators alone control is gone through, by the rule the store commands follow ("What an administrator's command writes", below): keeping Data, Logs, a folder above them or a folder of the program such as Events on another drive behind a junction or a volume mount point an administrator made is an ordinary layout. Such a link - owned by BUILTIN\Administrators, SYSTEM or TrustedInstaller, which nobody else may change or remove from the folder it is in (OnlyAdministratorsControl) - is followed, and the path it holds is held from that volume's root in the same way - every link on it, the folder it ends at included, asked about in its turn, so a junction the service's account put where an administrator's link leads is refused - must be on a fixed local drive, and is held to the refusals above as though it had been named itself: a junction from Data to a drive's root, the Windows folder or Bin is refused there. A volume mounted at Data or Logs with no drive letter of its own is gone through, its root granted; one that also has a drive letter is that drive's root, and refused as one - take the letter away (mountvol <letter>: /D), or make the link a junction to a folder on that drive instead. The grant is written on the folder the link leads to, by its volume's name (\\?\Volume{...}\), so that what it inherits is that folder's own parent's, not the link's; the line says where the link leads. A link to a share is refused as not on a fixed local drive, the line naming the share it leads to. Until 6.3.6 every link on the way was refused, and the upgrade of an installation whose data folder an administrator had moved behind a junction failed its grant. The icacls that gives a link to the administrators alone is under "What an administrator's command writes".

How a grant is written. One allow entry for the service's SID is added and nothing is removed - no other entry is changed, an administrator's deny entry for the service included; an explicit deny outranks any allow, so the line says the grant FAILED and names it, and an inherited deny is named as the entry the new one now comes before. While a folder is granted, it and every folder above it are held open without sharing delete, so none of them can be renamed, removed or swapped for a link while the permission is written through the path - a link the administrators alone control included, and every folder from the root of the volume it leads to down. A file is granted through a handle opened without following links, and only when it has one name - a hard link elsewhere would carry the grant to wherever else it is. A second run finds the entries there and changes nothing: each line says granted, already there or FAILED with the reason, and the last line counts them. Windows hands a folder's entry down to everything below it as part of the grant, which on a large message store takes minutes the first time and nothing after that.

What it checks, and only says. When Everyone, Authenticated Users, Users, SERVICE or the service itself may write the program folder, Bin or hMailServer.INI - add, change or delete what is in it, or change who may - a CHECK line names the entry. The grant removes nobody's entry. An installation outside Program Files could inherit such an entry from a drive's root until 6.3.7; hMailServer.exe /ProtectFolders, which setup runs at every installation and upgrade before the grant, now takes away an entry any of the three inherits (below), so what is left for the line to name is an entry of its own, which an administrator gave, and the icacls ... /remove:g that takes it away.

--report-only says all of this - what would be approved and granted, and what is not reached - and changes nothing.

Exit codes: 0 every grant made and everything the configuration names reached, or confirmed; 1 a grant or an approval failed; 2 not in an installation's Bin folder; 3 not run as an administrator; 4 folders not approved or key files outside, not confirmed; 5 the configuration names a folder that is never granted; 6 the administrator answered No, and nothing was changed; 7 the grants were made, but this account could not read the database, so the certificate and key files and the backup and PST import folders it names are not known, and nobody confirmed. The installer and the Control Panel move the service only on 0.

A database it cannot read. On SQL Server with Windows authentication, where LocalSystem is a SQL Server administrator and the person running the grant is not, the grant cannot read what the database names. It says so on a NOT READ line and ends in 7 - until 6.3.6 it said so in a note and ended in 0, and the service was moved without anybody knowing whether a key file it reads lay outside its folders. With --confirm the question names the database it could not read, and Yes is a 0. A silent installation's move then waits, as for a folder nobody approved; an upgrade of a service already moved goes on, since nothing is moved. A database not set up yet names nothing, and is no 7.

What the other users of the computer may read: hMailServer.exe /ProtectFolders

Until 6.3.6 the program folder's own Data, Logs, Temp, Database and Events folders, and Bin\hMailServer.INI, took their permissions from the folder they are in. Under C:\Program Files that gives BUILTIN\Users - every account that can sign in to the computer: a Remote Desktop server's users, a shared machine's, a service running as an ordinary account - Read on all of them, and ALL APPLICATION PACKAGES the same. Any of them could read every message; the logs, with their addresses, subjects and whole SMTP, IMAP and POP3 conversations; the built-in database, with the accounts' password hashes and the stored secrets' sealed values, whenever the service that keeps its file open is stopped; a key file an administrator copied into Data\Certificates; the event scripts; and hMailServer.INI - the database's password, sealed with the machine's DPAPI key, which any account on the machine may use to unseal it (PasswordEncryption=6), the administrator password's hash and its second factor. The grant (above) adds the service's entries and removes nobody's. A data folder moved under a folder at a drive's root took that folder's permissions instead once anything rewrote its own - on Windows Server, Users' Read and the right to make files and folders in it; on a Windows client, Authenticated Users' Modify. On the installer smoke VM on 3 October 2026 a user in the Users group alone, signed in interactively, listed the five folders of a 6.3.5 installation and read two messages, a log naming seven addresses, the event script, an imported key file and hMailServer.INI, and unsealed the database's password from it.

Since 6.3.6 the installer runs hMailServer.exe /ProtectFolders, elevated, at every installation and upgrade of the server - whatever account the service runs as, before the database step - and an administrator may run it from an elevated prompt in the Bin folder at any time. Each of the six is made private:

What Who reaches it afterwards
Data, Logs, Temp, Database, and everything in them SYSTEM and the Administrators, full control; NT SERVICE\hMailServer Modify, the grant's, when the service runs as anything but LocalSystem
Events, and everything in it SYSTEM and the Administrators; the service Read, the grant's
Bin\hMailServer.INI SYSTEM and the Administrators; NT SERVICE\hMailServer Read, under every account - the service reads its configuration whatever it runs as, and writes it under none but LocalSystem

How. The permissions stop inheriting - a protected DACL - so that nothing the folder above hands down reaches it. Every deny entry is kept, an inherited one as the folder's own; every entry of the folder's own is kept, an administrator's and the grant's; SYSTEM's and the Administrators' inherited entries become full control, handed down; an inherited entry for the service's SID - the Read on hMailServer.INI that Bin's grant hands down - is kept as the file's own; and every other inherited allow entry is dropped and named on the line: BUILTIN\Users, ALL APPLICATION PACKAGES, ALL RESTRICTED APPLICATION PACKAGES, CREATOR OWNER, and on a moved data folder Authenticated Users. Windows hands the new permissions down to everything in the folder as part of the change, which on a large message store takes minutes the first time; a second run finds them already private and changes nothing. A file or folder below with permissions of its own - the DKIM keys and the update token (ProtectedFile), the self-signed certificate's key - keeps them.

Where it acts, and where it only says. The six are found from where the program is, as the grant's folders are, never from hMailServer.INI or the database; a junction or a mount point among them is gone through only when the administrators alone control it, and held from that volume's root while it is changed, as the grant goes through one - so a data folder an administrator moved to C:\MailStore\Data behind such a junction is made private there. Anything else is checked and said, never changed: a folder elsewhere may be shared on purpose, and an elevated program takes no path to change from the configuration.

  • A folder hMailServer.INI names outside the program folder ([Directories]), a folder approved for the service, the backup, archive and PST import folders, and the key and certificate files the database names: a CHECK line when Users, Authenticated Users, Everyone, INTERACTIVE or another group of ordinary accounts may read it, with the two icacls that close it.
  • A key or certificate file in Data\Certificates, Data\DKIM or Data\ACME with permissions of its own that such a group may read - a file moved there keeps the permissions it had where it was: a CHECK line, and icacls "<file>" /reset, which gives it the data folder's.
  • An entry of one of the six's own that lets such a group read it, which only an administrator can have given: kept, and a CHECK line with the icacls /remove:g that takes it away.

Bin\hMailServerApiKeys.ini is left as it is, readable as the rest of Bin is: it holds each REST API key's label and the SHA-256 of its 32 random bytes, which tells nobody the key, and the Control Panel lists the keys from it.

The program's own folders: what the other users may change. Until 6.3.7 the program folder itself, Bin and ControlPanel took their permissions from the folder they are in, as the six did. Under C:\Program Files that is right: SYSTEM, the Administrators and TrustedInstaller may change them and every other account may read and run them. In a folder at the root of a drive it is not. On a Windows client the root hands every folder made below it Authenticated Users Modify, so every account that can sign in to the computer could replace hMailServer.exe, or put a library in Bin that Windows loads into it before its own (a program's own folder is searched first), and the service - LocalSystem by default and after every upgrade that keeps it, or NT SERVICE\hMailServer - ran it at its next start, as did the next administrator to run DBSetup, the Control Panel's elevated steps or the uninstaller. On Windows Server the root lets Users make files and folders in it, which is enough for the library. Every release from 6.0.0 to 6.3.6 installed so at a drive's root (work item 78); the default folder, Program Files, was never affected.

Since 6.3.7 /ProtectFolders hardens the three, after the six:

What Who reaches it afterwards
The program folder itself - the uninstaller, and what the folders in it inherit (DBScripts, Languages, WebAdmin, Addons) SYSTEM and the Administrators, full control; BUILTIN\Users Read & execute where every user could read it before (Program Files, a drive's root), and no entry where no user could; the Administrators its owner
Bin, and everything in it The same; NT SERVICE\hMailServer Read & execute, the grant's, and Modify on Bin\hMailServerApiKeys.ini alone, the grant's, an entry of the file's own that stays
ControlPanel, and everything in it The same; NT SERVICE\hMailServer Read & execute, the grant's

How. Each stops inheriting, as the six do; every deny entry and every entry of its own is kept - the grant's Read for the service among them; SYSTEM and the Administrators get full control and BUILTIN\Users Read & execute, each handed down, the Users' a fresh entry whatever the folder above handed them, so that the Control Panel and DBSetup still run for an administrator whose programs run unelevated - but only where Everyone, Authenticated Users or Users could read the folder before, as under Program Files and at a drive's root: in a folder made for the purpose that only the administrators may open, the Users get no entry, so that what the program folder holds besides the six (a backup, a key file, a data folder of another name) does not become every user's to read; and every inherited allow entry is dropped, the line naming those that gave a right to change it: Authenticated Users from a client's drive root, Users and CREATOR OWNER from a Windows Server's, and under Program Files TrustedInstaller and CREATOR OWNER. A folder somebody other than SYSTEM, the Administrators or TrustedInstaller owns (one a user made at a drive's root before the installation, which any user can) is given to the Administrators, and the line names the owner it had: an owner may change the permissions whatever they say, and so could have written the folder's own entries too - those that let anybody but SYSTEM, the Administrators and TrustedInstaller change it go with that owner, and are named. A file in Bin or ControlPanel with permissions of its own keeps them. A second run finds the three already hardened and changes nothing; under Program Files the first run of 6.3.7 makes the three's entries their own, and no account can then do anything there it could not before. The program folder is not changed while one of the five folders in it could not be made private, since Windows hands its change down through that folder; Bin and ControlPanel are hardened all the same.

Then the rule of an administrator's install. After the three, the installation is held to the rule an administrator's install of an update holds it to before it runs anything from it ("The live update", below): the folders above the program folder, the program folder, Bin and ControlPanel with every folder, program and library in them, hMailServer.INI and AdminUpdate. What the hardening makes passes it, so a drive-root installation that the live update refused before 6.3.7 passes once /ProtectFolders has run. What it leaves - an entry of a file's or a folder's own that an administrator gave, an owner that is not SYSTEM, the Administrators or TrustedInstaller, or a folder above the program folder that others may delete or rename (an installation in C:\Apps\hMailServer, where C:\Apps inherits Authenticated Users Modify from the drive's root) - is a CHECK line with the icacls that removes it, the first the rule finds, and is never changed here: a folder above the program folder may hold other programs that rely on what it allows.

Setup. Before it copies anything, setup reads the permissions of the folder chosen on its folder page - or, when that is not there yet, of the nearest folder above it that is - and when Everyone, Authenticated Users, Users, INTERACTIVE or another group of every user may change what an installation there would hold, it says so, in a box and in the setup log (the log alone for a silent installation, and for an upgrade, whose folder page is not shown). It never refuses the folder: one made for the purpose off a drive's root is an administrator's choice, and the hardening is what closes it. A program folder setup makes is given SYSTEM's and the Administrators' full control and - where every user could read it as it was made - the Users' Read & execute, inheriting nothing, by icacls on the SIDs, before any other folder is made in it or any file copied - so the programs are never there, even for the seconds before /ProtectFolders, with a weaker list.

The Control Panel alone. An installation of the Control Panel without the server (the remote administration support with it or not) runs no /ProtectFolders: it has no server. Into a folder setup makes, that changes nothing - the folder is given its permissions before the files, as above. Into a folder that was there - one an administrator made at a drive's root, or an earlier installation's - setup hardens it in place before it copies anything: the program folder, then ControlPanel and Bin where they are there, each as /ProtectFolders hardens a server's - SYSTEM and the Administrators full control, the Users' Read & execute where every user could read it, nothing inherited, the entries of its own kept while SYSTEM, the Administrators or TrustedInstaller owns it, and otherwise the Administrators made its owner and no entry of its own left. It leaves alone, and says in the setup log, a drive's root; a folder that holds a server installation (Data or Bin\hMailServer.INI there), which that installation's own /ProtectFolders hardens once its data folders are private; and a folder that is a link - a junction, a symbolic link, a mount point - or is in one, whose permissions would be written wherever it leads. Every folder from below the drive's root down to the one it changes is held open, with nobody else allowed to delete or rename it, from before it is looked at until after it is changed, so that no user can put a link in its place in between.

Before upgrading an installation at a drive's root. From an elevated Command Prompt, with the installation's own folder in place of C:\hMailServer, first see who owns what is there. Of the two listings below, the first names the program folder's owner on its line ., and the owner of each file and folder directly in it - Bin, ControlPanel and the uninstaller among them - on theirs; the second names the owner of everything in the folders that hold the programs and the event script, hidden files included, and of nothing in the data folders:

dir /q /a "C:\hMailServer"
dir /q /s /a "C:\hMailServer\Bin" "C:\hMailServer\ControlPanel" "C:\hMailServer\Events" "C:\hMailServer\WebAdmin" "C:\hMailServer\DBScripts" "C:\hMailServer\Addons" "C:\hMailServer\Languages"

dir cuts an owner's name to 23 characters (NT SERVICE\TrustedInsta), shows ... for an owner it cannot read, and says File Not Found for a folder the installation does not have - the Control Panel alone has no Events, WebAdmin, DBScripts or Addons - and lists the others. Each icacls line below must say that it processed one file - Successfully processed 1 files on an English Windows: one that says 0 has changed nothing, and from /remove:g that is the only sign that it found no account for a SID or a name it was given. Then, as /ProtectFolders does, for the program folder, then Bin, then ControlPanel:

  1. Make the Administrators its owner - an owner may change the permissions whatever they say. The folder alone: /T would reach the message store too.

    icacls "C:\hMailServer" /setowner *S-1-5-32-544
    
  2. List its entries with icacls "C:\hMailServer"; one without (I) is the folder's own. Take away each entry of its own but SYSTEM's and the Administrators'. The Users' and the service's go by their SIDs, a line each, whatever they give - steps 3 and 4 give back what they need:

    icacls "C:\hMailServer" /remove:g *S-1-5-32-545
    icacls "C:\hMailServer" /remove:g *S-1-5-80-3504853920-2627644491-1723263464-2546768561-1700046809
    

    On an installation without the server - the Control Panel alone - leave out the second line, and step 4: there is no service, so its SID names no account, and icacls refuses a SID that names no account, and the rest of its line with it (exit 1332). Anybody else's goes by the name the list gives, in quotes - but the two application package groups by their SIDs, *S-1-15-2-1 and *S-1-15-2-2, since icacls does not take them by the names it gives them, APPLICATION PACKAGE AUTHORITY\ALL APPLICATION PACKAGES and APPLICATION PACKAGE AUTHORITY\ALL RESTRICTED APPLICATION PACKAGES. A (DENY) entry only takes away, and stays.

    icacls "C:\hMailServer" /remove:g "<name>"
    

    Where the list gives a SID and no name, Windows found no account for it - one deleted, one of a domain it cannot reach, the service's where there is no service - and icacls cannot take the entry away. PowerShell can, with the SID in place of <SID>; the line takes away that SID's allow entries of the folder's own, and nothing else:

    powershell -NoProfile -Command "$d = Get-Item -LiteralPath 'C:\hMailServer'; $a = $d.GetAccessControl('Access'); $a.RemoveAccessRuleAll((New-Object Security.AccessControl.FileSystemAccessRule ([Security.Principal.SecurityIdentifier]'<SID>'), 'FullControl', 'Allow')); $d.SetAccessControl($a)"
    
  3. Replace what it inherits, and give SYSTEM and the Administrators full control and the Users Read & execute:

    icacls "C:\hMailServer" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F *S-1-5-32-545:(OI)(CI)RX
    
  4. On Bin and ControlPanel, where the server is installed, give the service back its Read & execute, the grant's:

    icacls "C:\hMailServer\Bin" /grant *S-1-5-80-3504853920-2627644491-1723263464-2546768561-1700046809:(OI)(CI)RX
    

icacls on each of the three then lists SYSTEM's and the Administrators' (OI)(CI)(F), the Users' (OI)(CI)(RX), on Bin and ControlPanel the service's (OI)(CI)(RX) where the server is installed, and any deny entry, which stays - nothing marked (I) and nobody else's. The SIDs are SYSTEM's, the Administrators', the Users', the service's and the two application package groups', the same in every language of Windows; where the folder above lets no user read the installation, leave out the Users' grant at the end of step 3, as /ProtectFolders does. On the program folder each change of its entries reaches everything in it, the message store included where 6.3.5 or earlier left Data inheriting - minutes there, and Data then readable by Users until the upgrade's /ProtectFolders makes it private. The service's Modify on Bin\hMailServerApiKeys.ini is the file's own, and stays.

The steps keep anybody else from changing the three folders themselves from then on; they undo nothing changed before, and a file or folder in them keeps its owner and any entry of its own. Where accounts that are not administrators could sign in to the computer while the three took the root's permissions; where the listings named an owner other than BUILTIN\Administrators, NT AUTHORITY\SYSTEM or NT SERVICE\TrustedInstaller - ... among them - for the program folder, a file directly in it, or Bin, ControlPanel, Events, WebAdmin, DBScripts, Addons or Languages or anything in them; or where step 2 took away an entry that gave anybody more than (RX), the Users' and the service's included, or could not take one away: a program in them may have been changed already, and so may the event script in Events, which the service runs. Reinstall the programs from the release into a new folder that only the administrators may change - under Program Files, say - not over this installation, rather than rely on the steps: setup leaves in place every file it does not ship, a library put in Bin among them, and puts the event script and the scripts in Addons only where none is there.

Reinstalling into a new folder. Run nothing from the old folder, its uninstaller included, which runs the old Bin\hMailServer.exe as an administrator. Setup run over an installation shows no folder page and installs into the folder it finds, so it is given the new one with /DIR; and setup run into a folder without a settings file sets up a new server there - an empty one, without this installation's mail, accounts or settings - so the settings file and the data go into the new folder first. Bring nothing else from the old folder, and read what you bring, as you would read a program. (Where nobody but the administrators could have changed the old folder's programs, Relocating an Installation moves the whole installation instead, its programs copied with it: its step 6 runs /ProtectFolders on the copy, which a copy by hand does not get otherwise, and its step 11 runs setup at the new folder as step 3 below does.) From the elevated prompt, with the new folder in place of C:\Program Files\hMailServer (the Control Panel alone brings nothing: step 3 is all of it):

  1. Stop the service. Make the new folder's Bin, Data, Database and Events - the last three private to SYSTEM and the Administrators before anything is in them - and copy the mail, the built-in database's folder and the settings file, which is made private too. Copy, not move: a file moved within a drive keeps its owner and its permissions.

    net stop hMailServer
    mkdir "C:\Program Files\hMailServer\Bin" "C:\Program Files\hMailServer\Data" "C:\Program Files\hMailServer\Database" "C:\Program Files\hMailServer\Events"
    icacls "C:\Program Files\hMailServer\Data" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F
    icacls "C:\Program Files\hMailServer\Database" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F
    icacls "C:\Program Files\hMailServer\Events" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F
    robocopy "C:\hMailServer\Data" "C:\Program Files\hMailServer\Data" /E
    robocopy "C:\hMailServer\Database" "C:\Program Files\hMailServer\Database" /E
    copy "C:\hMailServer\Bin\hMailServer.INI" "C:\Program Files\hMailServer\Bin"
    icacls "C:\Program Files\hMailServer\Bin\hMailServer.INI" /inheritance:r /grant:r *S-1-5-18:F *S-1-5-32-544:F
    
  2. Read the copy of the settings file - in Notepad, say. It took Bin's permissions until 6.3.6, and it decides what the service runs and who may administer it:

    • [Directories] names the folders the service takes the database scripts and 7za.exe from (ProgramFolder) and the event scripts from (EventFolder), and those of its data, logs and temporary files. Delete each of its lines that names a folder in the old installation; setup writes the new folder's instead. Setup keeps every other line but ProgramFolder, which it always writes, so delete as well a line that names a folder outside both installations and that you did not set - an EventFolder of somebody's own, say, which anybody who could change Bin could have written.
    • AdministratorPassword, in [Security], is the administrator password's hash. Delete it, and setup asks for a new password; a silent setup takes it from HM_ADMIN_PASSWORD.
    • [Database] names the database the server opens, and AutoBanCommand, in [Settings] or [SettingsOverride], a program it runs.
    • Any other line with a path into the old installation still names it after setup, and the new service goes on reading that file from the old folder: PostgreSQLSslRootCert and TlsCaFile, in [Database], the CA files the database server's certificate is checked against; UpdateTrustRootsFile and UpdateLogPublicKeyFile, in [Settings], which are read from this file alone; and a path in [SettingsOverride]. Change each as step 4 of Relocating an Installation changes it, to a copy in the new folder of what it names - read before it is copied, as the event script is below, or taken again from where it came from. The two CA files and the two trust files go where nobody but the administrators may change them, the new Bin say, and not in Data, which the service may change: an administrator's install of an update refuses a trust file anybody else may change ("The live update", below). The two trust files are the live update's trust with UpdateSigningIdentity, UpdateSigningIssuer and UpdateSourceRepository beside them in [Settings], and decide whose signature an update is installed on: delete any of the five your site did not set, and an update is checked against the project's own signers (The Update Feed, "Where the trust is read from").
    • Delete as well any other line your site did not set in [SettingsOverride], each of which takes the place of the database's value of its [Settings] key at every start, any key but those five; in [LDAP], which the server reads from this file alone, and which decides where and how the password of a directory-linked account is checked; and in [SendingLimits] and [SendingLimitsOverrides], read from it alone too, which limit what an account may send.

    Read what is in the old Events - the event script - as well, and only then copy it:

    robocopy "C:\hMailServer\Events" "C:\Program Files\hMailServer\Events" /E
    
  3. Where the first line below finds an entry - a 32-bit installation's - setup installs into the folder that entry names, whatever /DIR says: take it away with the second. Then run the release's setup with the new folder. For the server, it finds the settings file there and goes on as an upgrade of this installation, the service's account kept, with the programs its own; sc qc hMailServer then names the new Bin.

    reg query "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\hMailServer_is1" /reg:32
    reg delete "HKLM\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\hMailServer_is1" /reg:32 /f
    hMailServer-<version>-x64.exe /DIR="C:\Program Files\hMailServer"
    
  4. The database comes across as it was - on the built-in database in a file that took the program folder's permissions until 6.3.6. In the new Control Panel, read the programs its settings name: ClamWin's and the custom scanner's under Anti-virus settings, and AutoBanCommand under Auto-ban. Make the REST API's keys again rather than copy Bin\hMailServerApiKeys.ini, to which anybody who could change Bin could have added one. A path the database holds into the old folder - a certificate's, a DKIM key's, the backup folder - still names it: step 9 of Relocating an Installation moves each. Until nothing names it, keep the old folder private to SYSTEM and the Administrators: where 6.3.5 or earlier left its Data, Logs, Database and Bin\hMailServer.INI taking the permissions of the folder they are in, every user may still read the mail, the logs, the built-in database and the settings file there, and nothing runs /ProtectFolders on a folder no installation is in. As soon as step 1 has copied what it copies - for a service running as NT SERVICE\hMailServer, once the paths named at the end of this step are moved too - make the Administrators the old folder's owner and replace what it inherits. The folder alone has its owner changed, and on a large message store the second line takes minutes, as above:

    icacls "C:\hMailServer" /setowner *S-1-5-32-544
    icacls "C:\hMailServer" /inheritance:r /grant:r *S-1-5-18:(OI)(CI)F *S-1-5-32-544:(OI)(CI)F
    

    Then take away each entry of its own but SYSTEM's and the Administrators', as step 2 of Before upgrading an installation at a drive's root does. Where those steps, or the /ProtectFolders of 6.3.7 or later, were run on the old folder, Bin and ControlPanel inherit nothing from it and keep any Read & execute the Users were given, and a settings file that 6.3.5 or earlier left taking Bin's permissions takes it from Bin: take that entry away from both as well, from ControlPanel where the installation has one:

    icacls "C:\hMailServer\Bin" /remove:g *S-1-5-32-545
    icacls "C:\hMailServer\ControlPanel" /remove:g *S-1-5-32-545
    

    Any other entry of its own that a folder in it has stays - the grant's for the service among them, on each folder that was in it when the old installation's grant last ran - so the service still reads what the database names in those, and writes what it names in Data, Logs, Temp and Database. A service running as NT SERVICE\hMailServer may have reached what the database names directly in the old folder, or in a folder made in it since then, and a folder it writes in anywhere else in it - a backup or archive folder, which the grant gives no more than Read there ("Never granted, approved or not", above) - only through what the Users and Authenticated Users were given there, which this step takes away: move each first, by step 9 of Relocating an Installation. Then, once nothing names it, delete the old folder - by hand, never with its uninstaller.

Exit codes: 0 each made private or hardened, or so already (CHECK lines do not change it); 1 one could not be, and its line says why - a link somebody else controls, permissions that could not be written; 2 not in an installation's Bin folder; 3 not run as an administrator. --report-only says what it would make private and who may read each now, and what it would harden and who may change each now, and changes nothing; it comes before the verb, as the mode of the other commands does: hMailServer.exe --report-only /ProtectFolders (after the verb it is not read, and nothing is changed or reported). A run that does not end in 0 does not fail the installation: setup says so, with the command to run again, and what could not be changed is as it was.

What an administrator notices. An account that is not an administrator - and an administrator's own programs not run as administrator - can no longer open these folders or hMailServer.INI:

  • Explorer asks You don't currently have permission to access this folder, and Continue adds an entry for the administrator's own account to the folder, which later runs keep. Open the logs with an editor run as administrator instead, to leave the folder as it is.
  • The Control Panel not run as administrator - on a Windows client, where an administrator's programs run unelevated - says Could not read the file on its live log page and its message-store check, where it would have said there was no log folder; what it does over COM is as before. On Windows Server the built-in Administrator account runs it elevated already.
  • A script, a backup program or a monitoring agent that read the logs or the message store as an ordinary account needs an entry of its own - icacls "<folder>" /grant "<account>:(OI)(CI)RX" - which the next run keeps.
  • An editor that saves hMailServer.INI by writing a new file and renaming it over the old one gives it Bin's permissions again: run hMailServer.exe /ProtectFolders after, or the next upgrade does.

Its login on SQL Server: hMailServer.exe --grant-database-login

On SQL Server reached with Windows authentication - [Database] Type=MSSQL and no Username - the server signs in as the account the service runs as. LocalSystem is NT AUTHORITY\SYSTEM to a SQL Server on the same machine; the virtual account is NT SERVICE\hMailServer, which has no login there until one is made, and no user in the hMailServer database, which can only be made once the database exists. The verb makes what is missing of the three, on a connection of its own, as the account that runs it:

IF NOT EXISTS (SELECT 1 FROM sys.server_principals WHERE name = N'NT SERVICE\hMailServer') CREATE LOGIN [NT SERVICE\hMailServer] FROM WINDOWS;
USE [hMailServer];
IF NOT EXISTS (SELECT 1 FROM sys.database_principals WHERE name = N'NT SERVICE\hMailServer') CREATE USER [NT SERVICE\hMailServer] FOR LOGIN [NT SERVICE\hMailServer];
ALTER ROLE db_owner ADD MEMBER [NT SERVICE\hMailServer];

(hMailServer being the database [Database] Database names.) It looks first and leaves what is there - a login, a user under another name for the same login, or the login as the database's owner - and takes nothing away. Each step is one line: made, or there already. It makes no user and changes no role in a database that has DDL triggers of its own (sys.triggers, enabled, on the database): such a trigger runs with the rights on SQL Server of whoever's statement fires it, so anybody who was ever db_owner there - an earlier server's login, another program's - could have left one to run as the administrator, server-wide. The verb then says so, and a SQL Server administrator reads the triggers before running the statements. Run it from an elevated prompt in the Bin folder as an account that may make logins on that SQL Server (securityadmin or sysadmin) and that can open the database; an administrator who is a SQL Server administrator through the Administrators group is one only when elevated. The installer runs it as the administrator running setup, before it moves the service (above), and so does DBSetup when it finishes a new installation. When it cannot do a step, it says which and why, and prints the statements above for a SQL Server administrator to run, with the sqlcmd line that runs them. It says everything on its own output and writes nothing to the error log or the Application log: a connection the account running it cannot make would otherwise be reported there as the server's own failure to reach its database (HM5028), and the log folder is the service's.

A SQL Server on another computer is left alone: there the virtual account signs in as this computer's own account, DOMAIN\COMPUTER$, as LocalSystem does, so the login that worked keeps working, and a login named NT SERVICE\hMailServer could not even be made there. The verb compares SQL Server's MachineName and ComputerNamePhysicalNetBIOS with this computer's name, and says which it found.

Exit codes: 0 the service's account can open the database - its login, user and db_owner made now or there already - or needs nothing here, which the lines say (not SQL Server with Windows authentication, or SQL Server on another computer); 1 one of them could not be made; 2 hMailServer.INI names no database, or the database could not be opened as the account that ran it. Run before --service-account virtual /Register with &&, it moves only a service that will open its database.

hMailServer.INI is read-only to the service

The file keeps four kinds of thing, and under NT SERVICE\hMailServer the service writes none of them. What each writer does instead:

What wrote the file Under the virtual account How
The [Settings] copy of the settings store, at every start, reinitialise and save (IniSettingStore) Not written. The database is the settings store and its values are in use, as ever Said in the application log, not as an error, with the settings whose line there is older than the stored value counted and named, at a reinitialise where application logging is on. At the service's start the line is dropped: the settings store is reconciled before the server has read its logging settings (Application::InitInstance runs IniSettingStore::Synchronize before Configuration::Load). A setting removed while the file still has its line is kept stored as its default - which the server treats the same - because a dropped row would be put back from the line at the next start; a key this build does not read is refused with a sentence. Each row records what the file really holds, so an old copy is never taken for an edit, and an edit is reported once, as HM5804. A backup restored stores its settings in the database
PasswordPepper, kept in the file only Refused with a sentence An administrator sets it in the file with an elevated editor and restarts the service
The live update's trust - UpdateSigningIdentity, UpdateSigningIssuer, UpdateSourceRepository, UpdateTrustRootsFile and UpdateLogPublicKeyFile - read from the file alone from 6.3.6 Refused with a sentence, under every account: the process an update is checked for may not choose what it is checked against (The Update Feed, "Where the trust is read from") An administrator sets them in the file with an elevated editor and restarts the service
[Directories] - COM Directories.* Refused with a sentence An administrator changes the file and approves a new folder for the service
[Database] - COM Database.CreateInternalDatabase, CreateExternalDatabase, SetDefaultDatabase, which DBSetup and DBSetupQuick call Refused before anything is made The installer does its database step under LocalSystem (above). To change the database later, move the service to LocalSystem for the change and back, or edit [Database] and restart
[Security] AdministratorPassword - COM Settings.SetAdministratorPassword, the Control Panel's Administrator password card Refused with a sentence hMailServer.exe /SetAdministratorPassword, from an elevated prompt, reads the password from standard input and writes the hash the card writes; restart the service. From PowerShell: $p = Read-Host -AsSecureString 'New administrator password'; [Net.NetworkCredential]::new('', $p).Password | & '.\hMailServer.exe' /SetAdministratorPassword
[Security] AdministratorTotpSecret - COM Settings.EnrolAdministratorTOTP and DisableAdministratorTOTP, POST and DELETE /api/v1/administrator/second-factor Refused with a sentence (409 over REST) To turn it off, delete the line with an elevated editor and restart. Enrolling needs an account that may write the file: the server makes the secret and hands it out once, so move the service to LocalSystem for the enrolment and back

Bin\hMailServerApiKeys.ini, a file of its own, is still written (above).

The server decides by asking the file - opened for writing and closed at once - not by the account's name, so a service that may write it (LocalSystem, an account an administrator granted) behaves as before. The Linux packages already install /etc/hmailserver/hMailServer.ini as 0640 root:hmailserver; the same handling applies there, and a setting saved is no longer an error at every start. The [Settings] copy is stale for anything that reads the file itself rather than the database: the server when its table cannot be read, the installer (for ShutdownDrainSeconds), the Control Panel when it has no COM connection, and a person opening the file.

/Register takes the account from its command line

hMailServer.exe --service-account virtual|localsystem|keep|DOMAIN\name$ /Register. Without --service-account it keeps the account a service has, and makes a new one LocalSystem. DOMAIN\name$ is a group managed service account, which has no password. Any other account has one, and is refused: it is set in services.msc (the service's Log on tab), and every upgrade keeps it.

ServiceAccountName and ServiceAccountPassword are no longer used: the server still loads them with its other settings (IniFileSettings.cpp), and nothing reads them. They were what /Register used, and they are written by others: they are rows of the settings store, which hMailServer administrators - who need not be Windows administrators - set over REST and COM, and which the service writes; and the file holding their copy is written by whichever account last could. A registration that read them would let any of those make the next elevated /Register give the service LocalSystem back. Asking whether the file is writable by administrators alone would answer for one of the two places the value comes from, so the value is not read at all. The Control Panel no longer edits them; its Setup checklist page asks for a stored password to be deleted.

What an administrator's command writes

hMailServer.exe is the service, and also the commands an administrator runs: Setup, DBSetup and the Control Panel run /Register, /RegisterTypeLib, /Unregister, /GrantServiceAccess, /ProtectFolders, --grant-database-login, --apply-new-install-defaults, --apply-profile, --create-stored-secrets-key, --set-admin-password and the live update's two elevated, and an administrator runs the rest - --upgrade-database, the stored-secrets commands, the store and object-store commands, --apply-environment, --check-database - from an elevated prompt. Each reads [Database] or [Security] from hMailServer.INI, or changes the service, so each runs with the administrator's rights; and the folders the service is granted Modify on - Logs, Data, Temp, Database - are folders a program running as NT SERVICE\hMailServer can put a junction or a mount point in. A file an administrator's process wrote there by its path could be written wherever that leads.

So, since 6.3.6:

  • No log, no event, no crash record. Every command line but RunAsService, /Debug and /Test forbids the logger's writes as the first thing it does (ForbidServiceFolderWrites in hMailServer.cpp, Logger:: ForbidWritesForThisProcess): no line reaches the error log or any other log, the Application log, the OnError event script or the SQL log device; and it arms no crash oracle, whose record and dump go to the log folder. What each command did, and why it failed, is on its own standard output and standard error, where it already said it; Setup, DBSetup and the Control Panel keep that in their logs. /Register says there why a registration failed. Until 6.3.6 only the live update's verbs, the grant and --grant-database-login did this; the others wrote their errors to the error log, and every one armed the crash oracle.
  • No junction followed where Windows can refuse it. On Windows 11 22H2 and Windows Server 2025 and later the same commands follow no junction that a user who is not an administrator made (ProcessRedirectionTrustPolicy); an older Windows refuses the request, and the next two rules hold there alone.
  • The new installation's certificate is made through held folders. --apply-new-install-defaults writes the self-signed certificate and its private key into the data folder's Certificates folder. It holds every folder of that path open from the drive's root down - none renamed or removed while held, and none a link but one the administrators alone control (below) - makes Certificates and each file relative to the folder above, never through a link of its name, and checks each file it made to be in that folder before writing a byte to it. The key's permissions - SYSTEM, the Administrators, the account running it and Read for the service's account - are given when it is made, not set by its path later.
  • A store command finds no link in the data folder that somebody else controls. --compress-store (and --decompress), --encrypt-store, --decrypt-store, --recount-store, --measure-store, --copy-store-from-object-storage and --copy-store-to-object-storage reach each message by its path. Before anything is read or changed the data folder is held from its drive's root down and every folder and file in it listed once; a junction, a mount point or a symbolic link on the way to it or in it that the administrators do not alone control stops the command, exit 1, naming it and why. A file that is only tiered or deduplicated is not a link. A copy to the object store that removes nothing is held to it too: it reads with an administrator's rights and hands what it read to a store the service's account reads. So is --measure-store since the re-check of 6.3.6, although it changes nothing and says what it read to the administrator alone: it reads every file with an administrator's rights, and one rule for every store command leaves no exception to reason about.
  • A link the administrators alone control is gone through. Keeping the data on another drive through a junction or a volume mount point an administrator made - Data itself, a folder above it, or a domain's folder in it - is an ordinary layout. Such a link is gone through when it is owned by BUILTIN\Administrators, SYSTEM or TrustedInstaller - an account without an administrator's rights cannot make one so owned - nobody else may change it (write its data or attributes, which is how a link is pointed elsewhere, add to it, delete it, or change its permissions or owner), and no entry in the permissions of the folder it is in lets anybody else remove it from there or change that folder's permissions or owner (OnlyAdministratorsControl, ProtectedFile.h). The path it holds - where it says it leads, read from the link - is then held in the same way from that volume's own root and checked to be the folder the link opened, so a link somebody else made further along is refused there, the folder the link leads to itself included: until 6.3.6's re-check the folder an open through the link ended at was walked instead, and that open follows a second link unasked. A link to a file is never gone through. What a refusal says names who may change the link, or who owns it. A link made in the data folder inherits the write rights setup grants the service's account there, and one made in a folder at a drive's root inherits them for Users or Authenticated Users, the service's account among them: either way the service could turn it elsewhere, and it is refused until it is given permissions only the administrators may change. An elevated prompt on Windows Server makes a link owned by BUILTIN\Administrators already; where the owner is the administrator's own account instead (a Windows client without UAC's split token, for one), give it to the Administrators too. For the link (and, for a mount point, the folder it is mounted on) - /L acts on a symbolic link itself rather than where it leads: icacls <link> /setowner *S-1-5-32-544 /L and icacls <link> /inheritance:r /grant:r *S-1-5-32-544:(F) *S-1-5-18:(F) *S-1-5-32-545:(RX) /L
    • or make it below a folder only they may write, such as the program folder. The service still reaches the mail through it: what is below the link is reached with the permissions of the folder it leads to; only the link itself, which the service then may not remove or change, is held to the link's. A folder no administrator may list - System Volume Information, when the data folder is a drive's root - is left unlisted when the administrators alone control it in the same way: nobody else can have put anything in it. Until the re-check of 6.3.6 every link and every such folder stopped the command.
  • A store command reaches no file outside the data folder. The names it rewrites, removes and copies over are the database's - each message's file name, each deleted item's, each held part's - and, on an object store, its keys: both are the service's to write. A row whose file is outside the data folder - a full path elsewhere, or a part that is .. - stops every store command, naming the message, before it reads or changes a file; a key whose name would lead out of it is not listed; and a held part's name is its SHA-256 in hex or names no file. A full path below the folder a junction or mount point of the data folder leads to
    • written when the data was there by that name - is in it. A full path elsewhere is usually one a server before 5.4 wrote: put DataFolder back to the folder it names, run the Data Directory Synchronizer there (Addons\DataDirectorySynchronizer; the diagnostic Test message file locations says whether any are left), which turns each into a name below the data folder, and move the data as Relocating an Installation says; or delete the message.

What is left: a program that still runs as the service's account after the service has stopped could make a link while a long store command runs, after the folder was listed - or, owning a folder in the data folder, as it owns those it made, give itself the right to remove an administrator's link from it and put one of its own there; on Windows 11 22H2, Windows Server 2025 and later the command does not follow it, and the next command's listing refuses it everywhere. An account holding the right to restore files (SeRestorePrivilege, Backup Operators) can give a file any owner; the service's account holds none, and one configured to would be an administrator in all but name. The SQL Server Compact database file in Database is opened by its path by the commands that open the database; that engine opens only a file that is a database, and creates none.

build/check-elevated-verbs.py holds the commands to these rules and lists them: it refuses a command line added to the three that may log, a dispatch before the decision, the crash oracle armed for any other, the certificate written by its path, and a store command - --measure-store included - or a copy dispatched without the check or behind a condition. --self-test breaks each rule in a copy and proves the check reports it.

On Linux, a command run as root

The package runs the service as hmailserver (the unit's User=), which owns the data directory /var/lib/hmailserver and the log directory /var/log/hmailserver and so can put a symbolic link anywhere in them. Since the re-check of 6.3.6:

  • The commands that work in those folders run as the service's account. The store commands (--measure-store, --compress-store and --decompress, --recount-store, --encrypt-store, --decrypt-store), the copies to and from an object store, --sample-domain, --apply-profile, --revert-profile and --apply-environment are run as that account, as their help says. Run as root (sudo hmailserver --compress-store), each now becomes it - the data directory's owner, with that account's groups - before it opens anything, says so on standard error, and so reaches only what the service reaches, through any link, and leaves no file behind that the service cannot open. A data directory that cannot be examined refuses, exit 2. So does one whose way is not root's and its owner's alone - a folder above it, or above where a symbolic link on the way points, that belongs to another account or that another account or group may write - since an account able to put a link there could choose which folder, and so which owner, the path leads to; the refusal names the folder. The package's /var/lib/hmailserver is below folders only root writes. Until then they ran as root, held only to name no file outside the data directory, and followed a symbolic link the service's account put in it.
  • Over a data directory that belongs to root, they run as root where nothing but root could have put a link. A service run as root - a source build or a container without User=, or the Linux regression suite - has a data directory of root's, and a command run as root there crosses no boundary, provided no other account could have put a symbolic link where it goes. So it runs on as root, and says so on standard error ("Run as root: ... runs as root, the data directory ... and everything under it being root's alone ..."), when every one of these is root's alone: the data directory, every folder under it, and every folder a symbolic link under it leads to; the log, temporary, event and database folders ([Directories] LogFolder, TempFolder, EventFolder, DatabaseFolder, each that is set - every command opens the database through the server's start, which can write into any of them, and the store commands take their lock in the log directory), and everything under each; the way to each, as above with root for the owner; and the configuration that names them all - --config's file, or the one found - and the way to it, since the command reads it again as it starts and writes its settings back to it, and an account that could change it could point the folders at a tree of root's of its choosing. Root's alone means each folder (and the configuration) belongs to root and no other account may write it: no write for others, and for its group only when that is root's group and no access control list stands behind the group's bits; no folder carries a default access control list that gives another account - or, on a set-group-ID folder, a group other than root's - a write to what is made in it; and each symbolic link belongs to root and leads where the way is root's alone. The folders are walked with lstat, following only links that pass; a file is not judged, since in such a folder no other account can put one there or replace one with a link. A folder that is not there yet is judged by the folder that would hold it. What the command then makes is root's alone too: it runs with at least every other account's write taken away by its umask (022), whatever umask it was started under. Anything else - a folder of another account's in the store, one a group other than root's or every account may write, a link another account owns, a log directory of the service's account's beside a data directory of root's, a configuration another account owns or may write, a log directory below Ubuntu's /var/log, which the syslog group may write - refuses, exit 2, naming the first folder, file or link that fails and what to do: give it to root and take the other accounts' write from it (chown root:root, chmod go-w, setfacl -bk), name folders that are root's alone in the configuration, or give the data directory to the service's account, as the package does, so that the command runs as that account. Until the re-check of 6.3.6 a data directory of root's refused every one of these commands, which stopped administrators of such a service and kept nobody out.
  • Every other command line run as root writes nothing in the log directory. --set-setting, the stored-secrets commands, --create-database, --upgrade-database, --set-admin-password, --check-database, --verify-update and the rest need root for the configuration or say only what they read; each says what it did, and why it failed, on its own output. What would have gone to the error log is not written, but for the stored-secrets code's HM7496 and HM7497, which go to standard error (Secrets at Rest). The server itself, and a command run as the service's account, log as before.
  • --create-database makes .new-installation new. Anything at that name in the data directory is removed first - a symbolic link itself, not what it names - and the file is then created exclusively, never through a link (WriteNewFileNoFollow); the stored-secrets key file was made so already (DataProtector::CreateKeyFileFor).

What LocalSystem could do and the virtual account cannot

Every right the service used that only LocalSystem - or an administrator - has, and what changed:

What Under LocalSystem Under NT SERVICE\hMailServer How
Its own folders Anything on the machine The grants above /GrantServiceAccess
hMailServer.INI Written Read only The section above
A file you name that is outside them - a certificate, a key, a DKIM key file, a message to import, a backup to restore Read wherever it is Read only where the account may The grant lists the configured ones as OUTSIDE; move them into Data\Certificates, or approve their folder for Read
The Windows event log's source (HKLM\SYSTEM\...\EventLog\Application\hMailServer) Registered at the first error forwarded Cannot write HKLM /Register, which runs elevated, registers it; the running service only reads it (WindowsEventLog::RegisterSource). Writing to the Application log itself is allowed to every service
The COM AppID (HKCR\AppID) Re-written at every start That write fails, silently and harmlessly /Register writes it, as it always has
The DCOM access and launch permissions Unchanged Unchanged They are about the callers - the Control Panel, scripts - who stay the administrators
Changing its own service entry (COM Utilities.MakeDependent, which DBSetup uses to depend on a local database service) Worked Refused The installer sets the database up while the service is LocalSystem; DBSetup run later falls back to sc.exe config hMailServer depend= RPCSS/<service> itself, as an administrator. The right is deliberately not granted: a service that may rewrite its own entry may make itself LocalSystem again
The live update The helper runs the installer The service cannot run it The update is still checked, downloaded and verified; an administrator installs it with Install update in the Control Panel on the server, after Windows' prompt (below)
The auto-ban's Windows Defender Firewall rules (AutoBanFirewall) Written Refused Writing a firewall rule takes an administrator and there is no narrower right to grant. Said once in the error log (HM7460) and in the checklist; banned addresses are still refused by the server. AutoBanCommand still runs - as the service account, so a command that asks a router or a cloud firewall works and one that runs netsh locally does not. Rules a LocalSystem run left behind cannot be removed by the new account, and are named once (HM7461) with the PowerShell that removes them
A port Any Any Windows lets any account bind any port, and the firewall's rule is per program: nothing to grant
The secret store Machine-scope DPAPI The same Any account on the machine can decrypt it - which is why shared hosting stays declined
SQL Server with integrated authentication, on the same machine The login NT AUTHORITY\SYSTEM The login NT SERVICE\hMailServer hMailServer.exe --grant-database-login (above) makes it, a user for it in the hMailServer database and db_owner - which the installer runs before the move, as the administrator running setup; where that account may not, the service stays on LocalSystem and setup gives the statements for a SQL Server administrator
SQL Server with integrated authentication, on another machine The computer account, DOMAIN\COMPUTER$ The computer account, as before A virtual account is the computer account on the network, so the login that worked keeps working. For an identity of the server's own rather than the whole computer's, run the service as a group managed service account (--service-account DOMAIN\hmail$ /Register) and give that a login
MySQL, MariaDB, PostgreSQL A user and password The same Nothing changes
A backup written to a network share The computer account The computer account Grant DOMAIN\COMPUTER$ on the share and its folder, as for LocalSystem
A backup or archive folder on this machine, set after the move Written Written once approved Grant access lists it as not approved and asks
A custom virus scanner, ClamWin Ran as LocalSystem Runs as the service account It reads the message in the temp folder, which is granted, and its own program and signatures, which the Users group can normally read. A scanner that needs more has to be granted it
ClamAV's clamd, SpamAssassin's spamd Over TCP Over TCP Nothing changes: the message is streamed, not named by path
Event scripts Ran with LocalSystem's rights Run with the service account's A script that writes files, starts programs or reaches a database with integrated authentication can do only what the account can
Active Directory sign-in (LogonUser, LDAP bind) Worked The same No privilege is needed for a network logon. Kerberos acceptance with no explicit account (GssapiAcceptor) uses the computer account's keys under both; this is expected and not yet exercised on a domain-joined bench
Crash dumps Written to the log folder The same hMailServer.Minidump.exe runs as the service account and writes to the granted log folder
"Log on as a service" Implicit Held through NT SERVICE\ALL SERVICES A domain policy that replaces the right has to keep that group, or the service reports error 1069 and does not start

The live update

The helper that applies an update (hMailServer.Updater.exe) is started by the service and runs as the service's account, and the installer it runs needs an administrator. On LocalSystem - an upgraded installation that kept it - nothing about the live update changes: the service hands the installer to the helper, in the update window or when Install update is clicked, as The Update Feed describes.

Under an account that is not an administrator the service still checks for the update, downloads it and verifies it, and from 6.3.6 an administrator installs it with one click: Install update on the Updates page of the Control Panel on the server (on Server status in 6.3.6). Windows asks for permission once; everything after that happens by itself, and the Control Panel comes back signed in, on the new version, when the service is running. The service never runs anything as an administrator, and nothing it can write decides what the administrator's side runs.

What happens after the click

  1. Windows' prompt. The Control Panel starts Bin\hMailServer.exe /InstallUpdate of its own installation elevated (runas), for the release the Status page shows. Only a Control Panel on the server does this: one connected to another computer says to open the Control Panel there, and so do the Control Deck and POST /api/v1/update/install (409, code update_install_by_administrator). A Control Panel that is not the installation's own - a copy run from elsewhere - says to open the installation's. If the prompt is declined, nothing has changed anywhere.
  2. The elevated stage checks the installation. It finds the installation from the service's registration and hMailServer.INI, which only administrators can write, never from what the service says, and checks that nobody but SYSTEM, the Administrators and TrustedInstaller can change what an administrator's process will run or read there: the folders above the program folder and the program folder itself (deleting in them, deleting them, or changing their permissions or owner), and Bin and ControlPanel with every folder, program and library in them, hMailServer.INI, AdminUpdate and the trust files hMailServer.INI names (any write). Something others can change stops the install before anything has changed, and the Control Panel names the folder, the account, the right and the icacls command that removes it: a program run from there as an administrator could be one that account put there. hMailServer.exe /ProtectFolders gives the program folder, Bin and ControlPanel the permissions this passes, and says the rest of what it finds ("The program's own folders", above). The same rule is checked by the Control Panel before the prompt, from a table of cases both sides are tested against (build/testdata/install-permissions-cases.json).
  3. It copies the installer out of the service's reach. AdminUpdate\<id> in the program folder - owned by the Administrators, SYSTEM and Administrators alone may change it, the administrator who clicked may read it - is made new for each install, and the installer and its Sigstore bundle are copied into it from Data\Updates, which the service writes. AdminUpdate itself lets anyone read its permissions, and nothing else of it, so that the Control Panel of every later install can hold it to the rule before the prompt. A name that leads through a link or a junction, a folder where a file should be, or a copy bigger than 1 GB is refused. The copies are held open, so that nothing can change them between the check and the install, and everything below is checked on the held bytes.
  4. It checks the installer itself, trusting nothing the service said about it:
    • the Authenticode signature, the primary one, with Windows' own verification, and the publisher pinned: the certificate names exactly CN=Progressive Robot Ltd, O=Progressive Robot Ltd, L=Chester, S=Cheshire West and Chester, C=GB, its chain ends, by SHA-256, in Microsoft ID Verified Code Signing PCA 2021 and Microsoft Identity Verification Root Certificate Authority 2020, it is issued for code signing, and the signature carries a timestamp. A file that Windows trusts but another publisher signed is refused. Where the installed hMailServer.exe is signed by somebody else - a site that builds and signs its own - the installer is held to that signer instead, the same name through the same root, and where the installed one is not signed at all there is nothing to hold it to, and the one-click install is refused. A name is compared whole: one with a NUL in it is refused, never read up to the NUL;
    • revocation, online, of the whole chain, within 30 seconds: a certificate that is revoked is refused, and so is one whose revocation could not be checked - said as the revocation server not being reachable, never as revoked. Windows' own answer is not enough for that: its Authenticode policy takes a revocation server that does not answer as good, so the stage reads the revocation status of each certificate in the chain Windows built, the root aside, and refuses where one is unknown or was checked offline (on the installer smoke VM before 6.3.6 was released, a revocation server held unanswered let an installer through as "revocation checked online"; with its outbound HTTP blocked, Windows answered 0 for the real 6.3.5 while each certificate of its chain below the root said its revocation was unknown and checked offline, and with HTTP open and nothing cached the same check fetched Microsoft's lists and passed). The chain read is the signer's; the timestamp's own chain is left to Windows' answer. So whoever can keep this computer from the revocation server - www.microsoft.com, where Microsoft publishes the lists for the project's certificates - can stop the one-click install: a firewall that lets out only the download, a proxy the check cannot pass, or somebody on the network between. That is a denial, refused before anything is changed, never an install of anything else; the installer can still be run by hand, where Windows' own check, which takes an unreachable revocation server as good, is what applies;
    • the version in its version resource is the release offered, the architecture in its description (hMailServer Setup (x64) or (ARM64)) is this installation's, and the release is newer than the running server, whose version is compiled into the stage;
    • the Sigstore bundle - the pipeline for that release's tag, and the transparency log - checked by a second copy of hMailServer.exe that the stage starts with no privilege but change-notify, the Administrators group and every other group UAC takes from an administrator (Backup Operators, Domain Admins and the rest) made deny-only, at low integrity, in a job that holds it to one process and 512 MB, and that is given the bundle and the installer's SHA-256 through a pipe and nothing else. The bundle is parsed there, not in the elevated process, and a child that a crafted bundle took over could write nothing of the administrator's: at medium integrity, the administrator's own unelevated level, it could still have written their profile and registry, which their programs and Windows' auto-elevating ones read. Its answer is not what the install rests on - the Authenticode pin is.
  5. The service prepares, on the administrator's request (COM Status.PrepareUpdateForAdministrator, asked from this computer only): it checks its own download again, takes the backup (UpdateBackupBeforeApply, to the destination Backup, restore & migration sets) and fetches the running version's installer as the rollback image. The stage copies the rollback image out and checks it as it checked the installer, held to the running version. A backup that fails asks the administrator whether to install without one; with no destination set, the window says the rollback puts the program back and not the database. Nothing elevated takes anything from the service's answer: the service's part is the backup, the drain and the token.
  6. The install begins (Status.BeginUpdateForAdministrator): the service issues the database upgrade's single-use token, closes its listeners and lets the mail in progress finish (the drain), and the stage hands over to a copy of hMailServer.Updater.exe in the work folder, checked by its SHA-256. The Control Panel closes, so that the installer can replace it.
  7. The Updater installs as the administrator: it ends every program still running from the installation, runs the held installer silently - the token in its environment, not on its command line - with its temporary files in the work folder, waits for the service to come back, runs the held rollback image if it does not, or if the installer ends with 10, but never once the installer's database step has changed the database (exit code 11, upgraded-incomplete; Upgrading Guide, "If the upgrade fails"), and asks Windows to hold off a sign-out or a shutdown while it works (the reason shows on the sign-out screen). It writes the outcome into the work folder, which only administrators can write, puts an entry in the Application log (HM7466 when it starts, HM7467 with the outcome), and opens the Control Panel again as the administrator who clicked, not elevated: as a child of that user's desktop shell, explorer.exe in the same session, with the shell's own token.
  8. The Control Panel comes back signed in, from a handoff it wrote before it closed - DPAPI for the Windows user who clicked, bound to a key only that user and the Administrators can read in the work folder, good for 30 minutes and deleted when it is read - and shows the outcome. Where the administrator password has a second factor it asks for the sign-in instead. The service reports the outcome at its start, in the application log and the audit trail (object type update, interface Windows, the Windows account that approved it; AlertsAndAuditTrail.md).

The whole of the elevated side writes only into the work folder, never into Data, Logs or anything else the service can change, and hMailServer.exe loads the libraries it links from Bin and System32 only (/DEPENDENTLOADFLAG). A stage that finds another administrator's install under way on the server says so and does nothing. {app}\AdminUpdate is removed by the uninstaller.

The token

The token the installer's database upgrade presents is in the Updates folder, in a file the service can write (ProtectedFile: SYSTEM, the Administrators and the service), and a token lets its holder sign in over COM as the server's administrator. That is not a way for the service to reach an administrator: a service that could forge it holds everything the server's administrator can do already, and redeeming it runs nothing elevated. In an administrator's install a service that rewrote it could at worst make the genuine installer's database upgrade fail, and the Updater put the previous version back - a denial, not a compromise. The Updater reads the file only if it is no link, sits where it is expected and holds exactly 64 lowercase hexadecimal characters, and gives it to a 6.3.6 or later installer in its environment (HM_UPGRADE_TOKEN), so that it never appears on a command line - nor in the installer's log, which Inno Setup begins with the command line it was started with; the installer reads it and removes it from its own environment at once. The live update of a service running as LocalSystem gives it the same way from 6.3.6; up to 6.3.5 it was on the installer's command line, and so in Updates\install-<version>.log - a single-use token, spent once the installer's database step had run, in a folder only SYSTEM, the Administrators and the service can read.

Why this and not a scheduled task

A scheduled task running as SYSTEM, which the service could trigger, was considered and not built. The only file it could run is one in a folder the service writes, so to be safe it would have to copy the installer somewhere the service cannot reach, verify it there itself - not trusting the service's word, which is the whole point - pin the publisher, refuse an older version, and write its log and outcome nowhere the service could turn into a link to a system file. That is a second updater running as SYSTEM, and a service that can make SYSTEM run a file has given back what the account took away.

The administrator's install does every one of those checks, and differs from the task in what starts it: an administrator's click at the server, and Windows' prompt, which the service can neither cause, show nor answer. No elevated program waits for the service or listens to it; the elevated stage starts from the click or not at all, and what it runs is what it checked itself.

From 6.3.4 and 6.3.5

A server on 6.3.4 or 6.3.5 under this account cannot install 6.3.6 or a later release this way: the refusal is compiled into those versions, and their Install update asks the service alone. It is installed by hand once - download the installer from the release page, check that Get-AuthenticodeSignature reports it Valid and signed by CN=Progressive Robot Ltd, and run it as an administrator, keeping the service account on the installer's page. Windows SmartScreen may warn about a file downloaded with a browser that it has not yet seen often; the signature is what to check. From 6.3.6 on, each update is the one click above, and never meets SmartScreen: the installer is not downloaded by a browser, and the Updater starts it directly.

The unattended update window does not install under this account: when the window is open and an installer is verified, the error log says once per process that an administrator installs it with Install update in the Control Panel on this server (HM7463), and no backup is taken for an install that cannot happen.

Where you see it

  • The checklist (GET /api/v1/setup/checklist, SetupChecklist.md): the tenth item, service_account. service_account_local_system (on Linux service_account_root) is action needed while the server runs as the most privileged account - a recommendation, as the missing profile is, never an error; service_account_own names any other account and is done. Under an account that is not an administrator, autoban_firewall_not_permitted is added while AutoBanFirewall is on, and update_apply_by_administrator while the update check is. The server judges from its own token, not from what the service manager was asked for.
  • The Control Deck's dashboard shows the same finding, as information, while the server runs as LocalSystem or root, with a way to the checklist.
  • The Control Panel, Server limits & expert settings, card Windows service account: what the service runs as, and three buttons, each run after the elevation prompt with what it said shown afterwards - Grant access (--confirm /GrantServiceAccess), Move to NT SERVICE\hMailServer (the same grant, then --service-account virtual /Register only if it ended in 0) and Move to LocalSystem (--service-account localsystem /Register). External setup carries the same item, and asks for a stored ServiceAccountPassword to be deleted.

Moving it, and going back

To move a service that runs as LocalSystem to the virtual account: run the installer again and choose it, use the Control Panel's button, or from an elevated Command Prompt in the Bin folder

hMailServer.exe --grant-database-login
hMailServer.exe --confirm /GrantServiceAccess
hMailServer.exe --service-account virtual /Register
net stop hMailServer && net start hMailServer

running each line only when the one before it ended in 0. The first matters only for SQL Server with Windows authentication, and says "Nothing to do" otherwise; the Control Panel's button does not run it, so on such a database run it, or the statements it gives, before the button. To go back to LocalSystem: run the installer again and choose Move it back to LocalSystem (/serviceaccount=localsystem silently), use the Control Panel's button, or

hMailServer.exe --service-account localsystem /Register
net stop hMailServer && net start hMailServer

The grants and approvals stay behind and do nothing under LocalSystem; icacls <folder> /remove "NT SERVICE\hMailServer" /T takes the grants away, and deleting the ServiceAccessPaths key the approvals, if you want them gone.

Error codes

Code Severity Said when
HM7460 Medium, once per process AutoBanFirewall is on and the account cannot write firewall rules
HM7461 High, once per process Auto-ban rules a LocalSystem run left in the firewall cannot be removed by this account
HM7462 Medium /Register could not set the service's SID type to unrestricted
HM7463 Medium, once per process The update window is open and the update cannot be applied unattended by this account: an administrator installs it with Install update in the Control Panel on this server
HM7464 Medium /Register was given an account it does not set - one with a password - and registered nothing
HM7465 Medium An administrator's install did not stop the service within 10 minutes of its start: the drain is left, the token revoked, and the server accepts mail again. The Updater's log in AdminUpdate says how far it got
HM7466 Information, in the Windows Application log An administrator's install has started: who, which version over which, and the work folder. Written by hMailServer.Updater.exe, with the administrator's account as the entry's user
HM7467 Information, a warning or an error, in the Windows Application log An administrator's install ended: installed, cancelled or refused (a warning), or not installed, rolled back, with its rollback failed, or installed with its database upgraded and a step not complete - upgraded-incomplete, not rolled back - (an error), with the reason
HM7468 Medium The service's preparation of an administrator's install failed: the step (the check of its download, the backup, the rollback image) and the reason
HM7469 Medium, once per process The outcome of the last administrator's install could not be read, or an install that began more than an hour ago wrote none - the Windows session it ran in may have been signed out

None of HM7460 to HM7464 fires on a server as it is shipped: the firewall half of the auto-ban and the unattended update window are both off until they are switched on. HM7465 to HM7469 come only from an administrator's install. An administrator's install that did not succeed is also reported as HM5480, as the unattended apply's is - and one whose database step changed the database before a step did not complete, which nothing puts back, as HM5481 (Upgrading Guide, "If the upgrade fails").

Tests

  • ServiceAccountTester (Common/Util/ServiceAccount.cpp), in the internal suite Utilities.RunTestSuite runs: the service SID against the one Windows publishes for TrustedInstaller; which folders a grant may reach, on a machine made up for the purpose - [Directories] DataFolder pointed at Bin, at the program folder and at Data\..\Bin, the Windows folder, Program Files, a profile, a share, a device path, a stream, a drive's root, all refused; the program's own four and a folder of its own on another drive, not - and the plan made from them: a malicious data folder refused and not granted, an unapproved backup folder listed, an approved one granted, an approval of the Windows folder refused, a key file outside listed and one inside read on the file alone; the logon /Register passes for each --service-account, an account with a password and LocalSystem under another name refused; a grant on a directory of its own under the temp folder, made, made again with nothing changed, the one explicit entry it leaves, handed down to a file that was there before and one made after, an administrator's own entry kept; a file with protected permissions granted without losing its protection or its other entries; a hard-linked file refused; a path through a symbolic link refused where one can be made; a path through a junction only the administrators control held to where it leads, the junction and that folder not removable while held and the grant's path the volume's name of it, one the service's account may change refused naming that account, also where the administrators' junction leads to it, one leading to a folder no grant may reach refused, a folder missing beyond one found missing, and a grant through one made on the folder it leads to, handed down, that folder keeping what it inherits from its own folder and the junction still the administrators' alone (skipped only where the tester is no administrator); a held folder that cannot be removed; and /ProtectFolders (TestProtect_) on folders of its own that hand down what Program Files hands down - BUILTIN\Users' Read - beside a deny entry and Read for the service's SID: Users no longer reach the folder, a file already in it or one made after; the deny kept as the folder's own and first; an administrator's entry and the service's grant kept; SYSTEM and the Administrators given full control handed down; a file keeping the service's Read as its own, or given it where it had none; a second run changing nothing; a file with two names refused; and an entry of the folder's own for Users, given after, kept and still seen. And the program's own folders it hardens: in memory (TestHardenedShape_, which needs no administrator), the permissions built from what a Windows client's drive root, a Windows Server's, Program Files, an inherited Modify or Read for the service, a deny entry beside an administrator's entry, no list at all and a folder only the administrators may open hand a folder down, each held to the rule of an administrator's install of an update and passing it, nobody but SYSTEM, the Administrators and TrustedInstaller left to write, the Users' Read & execute given where every user could read before and no entry for them where none could, the dropped entries that gave a right to write named and the Users' Read not, the grant's Read, the deny (first) and the administrator's entry kept, and taken for hardened only when protected and the Administrators' to own; a folder a user made with entries of its own for itself and Everyone keeping them while the Administrators own it and losing them, named, when its owner is not trusted; and on the disk (TestHarden_), an installation of the tester's own under a folder that hands down what a client's drive root does - the program folder with the uninstaller, Bin with the server, a library, hMailServer.INI private and the key store, ControlPanel - which Authenticated Users may change and the rule refuses before, hardened: nobody but SYSTEM and the Administrators may change a folder, a file in it or one made after, Users still read and run them, the grant's entries and the key store's Modify kept, the rule passing every folder, program and library, a second run changing nothing, an entry an administrator gives Users afterwards kept and named by the rule, and a Bin a user owns given to the Administrators, the Modify that user gave itself taken away (where the tester runs as an administrator's own account).
  • Installation/InstallerPackaging.cs: the page's three forms and its defaults, /serviceaccount=, the word /Register is given and that hMailServer.exe knows it, that nothing reads ServiceAccountName any more, the registration before the database step and the move after it, the move only on a grant that ended in 0, --confirm in an attended installation, and a failed grant's own message; and /ProtectFolders run at every installation and upgrade, under no condition, after the registration and before the grant and the database step, its output in the setup log and its message suppressible, the verb taking the program folder's own six from where it is, holding each from the drive's root, and giving the service Read on hMailServer.INI; and the verb hardening the program folder, Bin and ControlPanel after the six, through the pin, the program folder not while one of the six failed, with a fresh Read & execute for Users where every user could read before, nothing inherited and the Administrators as owner, then holding the installation to the rule of an administrator's install; setup's message naming them, the program folder the first [Dirs] makes and given its permissions by SID before anything else, and the folder page saying, in a suppressible box and the log, and never refusing, when the chosen folder lets every user change what is in it.
  • API/RestApiSetupChecklist.cs: the tenth item, judged from the server's own token and, beside a running service, checked against the account sc.exe qc reports.
  • The administrator's install: AdminUpdateTester (Common/Util/AdminUpdateTester.cpp) holds every check of the elevated stage to its negative - the permission rule against the table of cases the Control Panel's InstallationPermissionsTests reads too, Bin at a client's and at a Windows Server's drive root refused and Bin and a program in it as /ProtectFolders hardens them passing among them; the work folder made new and an old one, one with others' rights and a junction refused; the copy through a junction, a link and a stream refused; the architecture marker; the pin alone, a subject with an attribute more or less, another chain, no code-signing use; and the real signed installers of 6.3.1 and 6.3.3 to 6.3.5 (put on the bench by build/fetch-installer-fixtures.ps1, checked by their SHA-256) passing, with 6.3.0 (unsigned), a byte changed, a file cut short or grown, the .NET runtime's installer (another publisher, trusted by Windows) and a catalogue-signed system file refused, the site-signer path held to the installed signer's name and root (the same name through another root refused), a name with a NUL in it refused, a revocation check with no time to run refused as not reachable, and a chain whose revocation status is unknown or offline below its root refused while one with only its root unchecked passes; and the lowered child's token read back - the Administrators and every group UAC filters deny-only, no privilege but change-notify, low integrity - its job's limit, its timeout and its output cap. AdminUpdateStageTester holds the stage's request to its child (a round trip, and every malformed frame refused), the child's answers, the Updater's outcome file, and the real 6.3.5 bundle verified through the child as the stage runs it. The Control Panel's AdministratorUpdateTests, AfterUpdateHandoffTests (a handoff without its key, another user's, an old one and a second read refused) and UpdateStatusTextTests; API/UpdateApply.cs, which on the bench's LocalSystem service holds the administrator's path shut, and where the service is another account holds the 409 and the state; and build/check-elevated-verbs.py, which fails when a verb that runs elevated reaches the log or the crash handler. The whole install, with Windows' prompt, is run by hand on the installer smoke VM.
  • The live registration - a new installation under the virtual account, an upgrade that keeps LocalSystem, a move each way, the question the grant asks - is exercised by hand on a throwaway machine; nothing here registers a service on the bench.

Home — full index

Start here

1. Install and run

2. Secure it

3. Operate it

4. Extend it

5. Contribute to it

Look it up — from any journey


About this wiki

Clone this wiki locally