Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Updating a package with a Windows Service resets service's account and password

Tags:

wix

I'm working on an MSI installer with WiX. I'm trying to keep this as simple to develop as possible: this is an internal product, and my users are our IT personnel.

The product includes a Windows Service that must be configured to run under a different account for each machine.

The workflow I was planning for my users (for first-time install) is as follows:

  1. Run the installer (The installer sets up the service under a default account)
  2. Stop the service via sc or Local Services applet
  3. Update the service properties to run under the correct machine-specific account. (The account is different for each machine, and only the IT personnel has access to the passwords.)
  4. Restart the service

Subsequent updates would consist of installing from updated MSI files.

Testing a "small" update, I was surprised to find that the installer reset the service back to running under the default account. This is a major problem for me because it makes it really hard for my users to update their servers. They would have to re-enter the account information on each machine every time there is an update. I expected that would happen with a "major" update, but not on a "small" one.

  1. Is there a way to configure the installer so that it does not change the existing account/password configuration for a service during a "small" or a "minor" update?

  2. Will this happen during a "repair" as well (I haven't tried that)?

Here's what my component looks like in the .wxs file:

<Component Id="cmpService" Guid="{MYGUIDHERE}">
  <File Id="filService" KeyPath="yes" Name="ServiceApp.exe" />
  <ServiceInstall Id="ServiceInstall" Name="ServiceApp" DisplayName="My Service"
                  Type="ownProcess" Start="auto" ErrorControl="normal"
                  Account="LocalSystem">
    <util:PermissionEx ... attributes here... />
  </ServiceInstall>
  <ServiceControl Id="StartService" Start="install" Stop="both" Remove="uninstall"
                  Name="ServiceApp" Wait="yes" />
</Component>

I had expected that Remove="uninstall" would preserve the service in place if there were no changes to it. Apparently not. (I'm not too worried if this happens on "major" updates).

I also noticed that the ServiceConfig element has attributes (OnReinstall) that seem to fit the bill, but based on candle error messages, it's pretty clear that OnReinstall is intended to affect only the configuration members of the element (PreShutdownDelay, etc.) rather than the service installation as a whole.

I've looked into these:

  • Let the user specify in which account a service runs
  • WiX MajorUpgrade of Windows Service, preserving .config, and avoiding a reboot
  • How to only stop and not uninstall windows services when major upgrade in wix?

Curiously, this answer suggests that this is an issue only for "major" upgrades. That wasn't my experience. Was my experience a fluke?

  • How do I create a custom dialog in WiX for user input?

It would have been OK prompting for an account and password during installation, but storing the password in the registry or elsewhere is a not really an option in this case, and having to reenter the credentials on every update is just as disruptive as having to reconfigure the service by hand.

like image 406
Euro Micelli Avatar asked Nov 18 '14 02:11

Euro Micelli


2 Answers

I had a consultation phone-call with FireGiant today about this exact issue and we came to a solution.

Backstory:

  • Our application install MSI installs a Windows Service using LocalService initially, however our actual desktop software changes this to NetworkService or even a custom user account as may be necessary in certain network environments.
  • Our <Component> <ServiceInstall> element had Account="NT AUTHORITY\LocalService" and looked like this:

    <Component Id="Comp_File_OurServiceExe" Guid="*">
    
        <File Source="$(var.TargetDir)OurService.exe" id="File_OurServiceExe" KeyPath="yes" />
    
        <ServiceInstall
            Id           = "ServiceInstall_OurServiceExe"
            Vital        = "yes"
    
            Name         = "RussianSpyingService"
            DisplayName  = "Russian Spying Service"
            Description  = "Crawls your network for incriminating files to send to the FSB"
            Account      = "NT AUTHORITY\LocalService"
            Type         = "ownProcess"
            Arguments    = "-mode service"
            Interactive  = "no"
            Start        = "auto"
            ErrorControl = "normal"
        >
    
            <ServiceConfig DelayedAutoStart="yes" OnInstall="yes" OnUninstall="no"  OnReinstall="yes" />
            <util:ServiceConfig FirstFailureActionType="restart" SecondFailureActionType="restart" ThirdFailureActionType="none" ResetPeriodInDays="1" />
        </ServiceInstall>
    </Component>
    
  • When these repro-steps are followed the service registration/configuration would be unintentionally reset:

    1. Complete an install using the MSI version 1.0.0
    2. Open Services.msc and change the RussianSpyingService to use NT AUTHORITY\NetworkService (instead of NT AUTHORITY\LocalService)
    3. Create a new MSI using the same *.wxs files, but higher file versions and give it a higher version, e.g. 1.0.1 (don't forget MSI only uses the first 3 components of a version number and ignores the 4th version)
    4. After that install has finished, observe the the RussianSpyingService has been reset to use NT AUTHORITY\LocalService.

As an aside, I asked FireGiant (their consultants previously worked at Microsoft and helped other teams at the company use MSI) who other software, like SQL Server are able to use MSI to install Windows Services that work fine despite configuration changes between upgrade-installs. They told me that products like SQL Server often use Custom Actions for windows service configuration and despite the general advice to avoid Custom Actions it's acceptable because the SQL Server team at Microsoft is big enough to devote engineering and test resources to ensure they work.

Solution

  • In short: "Use MSI properties!"
  • Specifically, define an MSI property that represents the Account attribute value and load that value from the registry during MSI startup and if the value is not present, use a default value of NT AUTHORITY\LocalService.
  • Ideally the property value would be stored in the application's own registry key and it is the application's responsibility to ensure that value matches the current service configuration.
    • This can be done by creating a new registry key in HKLM that lets LocalService or NetworkService (or whatever the service account is) write to it, so when the service starts-up it records its user-account's name there - but this is complex.
    • Do not use HKCU to store the value because that won't work: HKCU resolves to completely different registry hives (that might not even be loaded or accessible) for different users.
  • The other option is technically not supported by Microsoft because it uses the Windows registry's own services registration key raw ObjectName (Account name) value - which so-happens to be in the same format used by the AccountName="" attribute. It's also the most pragmatic and it's what is described below:

Here's what worked for us:

  1. Within your <Wix> ... <Product>... element, add this <Property> declaration and <RegistrySearch /> element:

    <?xml version="1.0" encoding="UTF-8"?>
    <Wix
        xmlns       = "http://schemas.microsoft.com/wix/2006/wi"
        xmlns:netfx = "http://schemas.microsoft.com/wix/NetFxExtension"
        xmlns:util  = "http://schemas.microsoft.com/wix/UtilExtension"
    >
    
        <Product
            Id="*"
            UpgradeCode="{your_const_GUID}"
            otherAttributes="goHere"
        >
    
            <!-- [...] -->
    
            <Property Id="SERVICE_ACCOUNT_NAME" Value="NT AUTHORITY\LocalService">
                <!-- Properties used in <RegistrySearch /> must be public (ALL_UPPERCASE), not private (AT_LEAST_1_lowercase_CHARACTER) -->
                <RegistrySearch Id="DetermineExistingServiceAccountName" Type="raw" Root="HKLM" Key="SYSTEM\CurrentControlSet\Services\RussianSpyingService" Name="ObjectName" />
            </Property>
    
            <!-- [...] -->
    
        </Product>
    </Wix>
    
  2. Update your <ServiceInstall element to use the new SERVICE_ACCOUNT_NAME MSI property for Account="" instead of the previous hardcoded NT AUTHORITY\LocalService:

    <ServiceInstall
        Id           = "ServiceInstall_OurServiceExe"
        Vital        = "yes"
    
        Name         = "RussianSpyingService"
        DisplayName  = "Russian Spying Service"
        Description  = "Crawls your network for incriminating files to send to the FSB"
        Account      = "[SERVICE_ACCOUNT_NAME]"
        Type         = "ownProcess"
        Arguments    = "-mode service"
        Interactive  = "no"
        Start        = "auto"
        ErrorControl = "normal"
    >
    
        <ServiceConfig DelayedAutoStart="yes" OnInstall="yes" OnUninstall="no"  OnReinstall="yes" />
        <util:ServiceConfig FirstFailureActionType="restart" SecondFailureActionType="restart" ThirdFailureActionType="none" ResetPeriodInDays="1" />
    
    </ServiceInstall>
    
  3. Build and run your installer and perform the upgrade scenario and you'll see any customized service Account User Name will be preserved between upgrade installs.

You can generalize this approach for other properties too.

Disclaimer:

  • Microsoft does not officially endorse user-land programs directly fiddling with the HKLM\SYSTEM\CurrentControlSet\Services\ registry key. All operations on Windows Services are meant to go through the documented and supported Win32 Service Control Manager API: https://docs.microsoft.com/en-us/windows/desktop/services/service-control-manager
    • This means that Microsoft could at their discretion, change Windows Service configuration so it no-longer uses the HKLM\SYSTEM\CurrentControlSet\Services\ key.
    • (This would presumably break lots of third-party software, if Microsoft were to do this they would probably add some kind of virtualization or re-mapping system to it like they do with SysWow6432Node).
  • I only tested it with LocalService and NetworkService. I didn't see what happens if you modify the service configuration to use a custom user account post-install before running an upgrade. I do expect that it will also preserve the configuration in that case as it would be performing a string-comparison on the ObjectName value in SCM and it has no access to passwords.
like image 118
Dai Avatar answered Oct 19 '22 10:10

Dai


What finally ended up working for me was

  <DeleteServices><![CDATA[REMOVE ~= "ALL" AND (NOT UPGRADINGPRODUCTCODE)]]> </DeleteServices>
  <InstallServices><![CDATA[NOT Installed]]> </InstallServices>

I arrived at this answer through a series of trial and error attempts and a combination of a few other threads with similar answers.

One of the possible reasons why only the doesn't work is because WIX also removes the service upon re-install.. we only want to install the service once, during the initial install. We also want to make sure that the service is removed upon uninstall. This is the only combination of conditions which worked for me, allowing the service to keep its settings and user account.

like image 45
Vikram S. Avatar answered Oct 19 '22 10:10

Vikram S.