Exchange Unattended Install Script [Update]

Back in 2013, I published an unattended installation script for Exchange Server 2013, together with a walkthrough on this blog. At the time, the goal was simple: make Exchange deployments more predictable, repeatable, and less error‑prone by removing as much manual interaction as possible.

Exchange has evolved, deployment practices have matured, and automation expectations are much higher. Over the past years, I have continued to maintain and refine the script to keep pace with those changes. Recently, I completed a major cleanup and refresh. Those watching my GitHub have likely seen the incremental changes.

This post serves as a refresher and high‑level overview on the current state.

Goal

This script automates the unattended installation of Microsoft Exchange Server 2016, 2019, and Exchange Server SE on Windows Server 2016 through 2025. For this, it follows a state machine process. This is necessary because some steps require a reboot before continuing.

The script handles the full installation lifecycle: Windows features, prerequisites (.NET, VC++ runtimes, IIS components), Active Directory preparation, Exchange setup, and post-configuration and hardening. With the -AutoPilot switch, the script manages automatic reboots and logon cycles, tracking progress in a JSON state file to track where it is in the process.

Supported Builds and Operating Systems

Exchange VersionMinimum OSMaximum OS
Exchange 2016 CU23Windows Server 2016Windows Server 2019
Exchange 2019
CU10–CU14
Windows Server 2019Windows Server 2022
Exchange 2019
CU15
Windows Server 2019Windows Server 2025
Exchange Server SE RTMWindows Server 2019 Windows Server 2025

For Exchange 2019 and up, deployment on Desktop or Core is supported. Support for Exchange 2013, older CUs for Exchange 2016 and Exchange 2019, and older operating systems (WS2008, WS2008 R2, WS2012, or WS2012 R2) has been removed since version 4.0 of the script.

Requirements

  • PowerShell 5.1 or later
  • Domain-joined system (Edge Server role is the exception)
  • An account with local administrator rights
  • When using -AutoPilot: the account must be able to configure and perform auto-logon
  • When creating a new Exchange organization (-Organization) or need to upgrade schema or domain configuration: Schema Admin and Enterprise Admin rights
  • Static IP address (running as an Azure VM is the exception)

Usage

The syntax for calling the script depends on which of the common scenarios you want to deploy. By default, it is to perform an installation:

Install-Exchange15.ps1 [-Organization] [-MDBName ] [-MDBDBPath ] [-MDBLogPath ] [-InstallPath ] [-SourcePath ] [-TargetPath ] [-AutoPilot] [-Credentials ] [-IncludeFixes] [-NoNet481] [-DoNotEnableEP] [-DoNotEnableEP_FEEWS] [-DisableSSL3] [-DisableRC4] [-EnableECC] [-NoCBC] [-EnableAMSI] [-DisableTLS10] [-DisableTLS11] [-DisableInsecureRenegotiation] [-DisableWeakCiphers] [-DisableWeakHashAlgorithms] [-DisableNonForwardSecretKeyExchange] [-DisableCredentialGuard] [-EnableTLS12] [-EnableTLS13] [-SCP ] [-DiagnosticData] [-Lock] [-SkipRolesCheck]

You can use it to install an Edge Transport server:

Install-Exchange15.ps1 -InstallEdge -EdgeDNSSuffix [-InstallPath ] [-SourcePath ] [-AutoPilot] [-Credentials ] [-IncludeFixes] [-NoNet481] [-DoNotEnableEP] [-DoNotEnableEP_FEEWS] [-DisableSSL3] [-DisableRC4] [-EnableECC] [-NoCBC] [-EnableAMSI] [-DisableTLS10] [-DisableTLS11] [-DisableInsecureRenegotiation] [-DisableWeakCiphers] [-DisableWeakHashAlgorithms] [-DisableNonForwardSecretKeyExchange] [-DisableCredentialGuard] [-EnableTLS12] [-EnableTLS13] [-DiagnosticData] [-Lock] [-SkipRolesCheck]

Or you can use it for recovery:

Install-Exchange15.ps1 -Recover [-InstallPath ] [-SourcePath ] [-AutoPilot] [-Credentials ] [-IncludeFixes] [-NoNet481] [-DoNotEnableEP] [-DoNotEnableEP_FEEWS] [-DisableSSL3] [-DisableRC4] [-EnableECC] [-NoCBC] [-EnableAMSI] [-DisableTLS10] [-DisableTLS11] [-DisableInsecureRenegotiation] [-DisableWeakCiphers] [-DisableWeakHashAlgorithms] [-DisableNonForwardSecretKeyExchange] [-DisableCredentialGuard] [-EnableTLS12] [-EnableTLS13] [-DiagnosticData] [-Lock] [-SkipRolesCheck]

The script Install-Exchange15.ps1 has a ton of options. An explanation of these is given in the table below. Depending on the operating mode (regular setup, preparation only, Edge Transport installation, or recovery), parameters may or may not become available.

ParameterDescription
-SourcePathPath to Exchange setup EXE folder or ISO file
-OrganizationExchange organization name to create. Omit to skip AD preparation.
-InstallEdgeInstall the Edge Transport server role instead of Mailbox
-AutoPilotFully automated mode — handles reboots and resumes automatically
-CredentialsCredentials AutoPilot uses for automatic logon after each reboot
-InstallPathWorking folder for state file, logs, and downloaded prerequisites (default: C:\Install)
-MDBNameName of the initial mailbox database
-MDBDBPathPath for the mailbox database file
-MDBLogPathPath for the mailbox database transaction logs
-TargetPathExchange binaries installation path (default: C:\Program Files\Microsoft\Exchange Server\V15)
-SCPAutodiscover Service Connection Point URL to set after installation. Use - to clear.
-IncludeFixesInstall additional recommended hotfixes and security updates
-DisableSSL3Disable SSL 3.0
-DisableRC4Disable the RC4 cipher suite
-EnableECCConfigure Elliptic Curve Cryptography
-EnableTLS12Configure TLS 1.2
-EnableTLS13Configure TLS 1.3 (WS2022/WS2025 with Exchange 2019 CU15+)
-EnableAMSIEnable AMSI body scanning for ECP, EWS, OWA, and PowerShell virtual directories
-DisableTLS10Disable TLS 1.0
-DisableTLS11Disable TLS 1.1
-DisableInsecureRenegotiationDisallow insecure TLS renegotiation (AllowInsecureRenegoClients and AllowInsecureRenegoServers set to 0)
-DisableWeakCiphersDisable weak SCHANNEL ciphers: NULL, DES 56/56, RC4 40/128, RC4 56/128, RC4 64/128, RC4 128/128, Triple DES 168
-DisableWeakHashAlgorithmsDisable weak SCHANNEL hash algorithms: MD5 and SHA-1
-DisableNonForwardSecretKeyExchangeDisable non-forward-secret key exchange (PKCS/static RSA)
-DisableCredentialGuardDisable Credential Guard (LsaCfgFlags and EnableVirtualizationBasedSecurity set to 0)
-NoSetupInstall prerequisites only; skip Exchange setup
-RecoverRun in RecoverServer mode
-NoNet481Use .NET 4.8 instead of 4.8.1
-DoNotEnableEPSkip enabling Extended Protection (Exchange 2019 CU14+)
-LockLock the workstation screen during installation
-DiagnosticDataSet the initial diagnostic data collection mode

Because of the number of parameters, you might want to use splatting when calling the script, for example:

$Cred = Get-Credential
$Params = @{
    Organization                   = 'Fabrikam'
    SourcePath                     = '\\server\iso\ExchangeServer2019-x64-CU15.iso'
    InstallPath                    = 'C:\Install'
    Credentials                    = $Cred
    MDBName                        = 'MDB1'
    MDBDBPath                      = 'C:\MailboxData\MDB1\DB'
    MDBLogPath                     = 'C:\MailboxData\MDB1\Log'
    SCP                            = 'https://autodiscover.fabrikam.com/autodiscover/autodiscover.xml'
    AutoPilot                      = $true
    DisableSSL3                    = $true
    DisableRC4                     = $true
    DisableTLS10                   = $true
    DisableTLS11                   = $true
    DisableInsecureRenegotiation   = $true
    DisableWeakCiphers             = $true
    DisableWeakHashAlgorithms      = $true
    DisableNonForwardSecretKeyExchange = $true
    EnableTLS12                    = $true
    EnableECC                      = $true
    EnableAMSI                     = $true
    Verbose                        = $true
}
.\Install-Exchange15.ps1 @Params
Capture2

More information

More information and recent documentation updates will be published on GitHub, including instructions in the README and changes in CHANGELOG.MD.

Download

The script is available from GitHub.

References

This post replaces the previous articles on the installation script, which are still there for historical purposes (and to show what has been updated or replaced over time):

Results Install-Exchange15 survey

stats chartA short blog on a small survey I’ve been running for some time now on the usage of Install-Exchange15, the PowerShell script for fully automated deployment of Exchange 2013 or Exchange 2016.

I started the survey because I was curious on a few things:

  • How the script is used; do folks use it for deploying in lab environments, or also actual production environments.
  • What Exchange versions are deployed; only current ones (n-2 at most, i.e. lagging 2 Cumulative Update generations at most), or also older versions.
  • What operating systems are used to deploy Exchange using this script.

The second and last items are of most interest, as keeping backward compatibility in the script, for example like deploying Exchange Server 2013 SP1 on Windows Servers 2008, requires keeping a lot of ‘legacy code’ in there.

Fortunately, the survey shows many of you use the script to deploy recent Exchange builds on current operating systems. So, in time, you will see support for older builds and operating systems being removed, making the script more lean and mean as well.

Now, on to the results:

In what environments do you use the script to deploy Exchange?

Lab

Production

Yes

86%

72%

No

14%

28%

Do you use Install-Exchange15.ps1 for previous (N-2 or older) Exchange 2013/2016 builds?

Yes 28%
No 72%

On which Operating Systems do you deploy Exchange 2013/2016? (multiple options possible)

Windows Server 2008 0%
Windows Server 2008 R2 18%
Windows Server 2012 18%
Windows Server 2012 R2 100%
Windows Server 2016 8%

Finally, a summary of the feedback and requests send in by respondents through the open comments section:

  • Installation on Windows Server 2016. The survey was created before Windows Server 2016 was supported, so we used the feedback given on people deploying on WS2016 in the above results.
  • In general, positive feedback on having this script for automated deployment, as well as the SCP feature.
  • Request for having a GUI to create the answer file.
  • Request to having the option to configure the virtual directories after installation. However, the script allows for inserting custom (Exchange) cmdlets in its post-configure phase.
  • Request to output cause of failed Exchange setup to the screen. That however, is something I wouldn’t recommend; the Exchange setup log files contain the details.
  • Request to have some sort of visible clue if the installation was successful or not.

Public Folder Hierarchy and Client Access

Ex2013 LogoWhen investigating performance issues of a multi-node, multi-role Exchange 2013 server deployment, I found the CPU utilization of a single Exchange 2013 server constantly above the load of the rest.

When checking the Processor Utilization % for all Exchange servers using Performance Monitor, the daily trend image looked like this:

clip_image002

As you can clearly see, one single server is constantly experiencing more load than the other servers. It is also above the 80% mark, causing all sorts of potential side-effects if Managed Availability would kick in.

When checking the processes on that server, the major CPU load was generated by the Microsoft.Exchange.RPCClientAccess.service as well as the related w3svc# process. The load balancer performed a near even distribution of client connections over these servers. You can use the Exchange Performance Health Checker script with the LoadBalancingReport switch to verify this.

Next, we checked if there was an overactive mailbox on that particular server. For that purpose, we ran the following cmdlet in the Exchange Management Shell, which showed us the Public Folder mailbox was very active:

Get-StoreUsageStatistics –Server <ExchangeServer> | ? {$_.DigestCategory –eq ‘timeInServer’} | Sort TimeOnServer –Descending

image

Note: More on tracking overactive mailboxes using Get-StoreUsageStatistics in this excellent write-up by Andrew HigginBotham.

Another clue was provided through the PublicFolders Healthset, which was picked up by System Center Operations Manager as well:

The PublicFolders Health Set has detected a problem with PublicFolderMailbox.ConnectionCount at 10-7-2016 06:12:22. 0 failures were found. The Health Manager is reporting that The total number of hierarchy connections for public folder mailbox PFMailbox1 has reached 2001. Consider creating a new public folder mailbox for load balancing hierarchy accesses.

Apparently, there were more than 2,000 connections being made to the PFMailbox1 Public Folder mailbox. This was odd, as there were multiple Public Folder mailboxes created with hierarchy. Users are expected to be automatically distributed over these mailboxes, falling within the 2,000 concurrent logons limit as mentioned here. Note that this limit applies to public folder mailboxes serving hierarchy as well; even if clients don’t access Public Folders, they still will connect to these Public Folder mailboxes in order to obtain hierarchy information.

Next thing we checked was to which default Public Folder mailbox mailboxes were configured to connect. To accomplish this we can inspect the mailbox property DefaultPublicFolderMailbox:

Get-Mailbox –ResultSize Unlimited | Group-Object DefaultPublicFolderMailbox –NoElement

Count Name
----- ----
10139 contoso.com/Accounts/Users/PFMailbox1

Apparently all mailboxes were automatically set to connect to a single Public Folder mailbox. Then maybe something was preventing the other Public Folders from serving hierarchy:

Get-Mailbox –PublicFolder | Select Name,*Hierarchy*

Name       IsExcludedFromServingHierarchy IsHierarchyReady
----       ------------------------------ ----------------
PFMailbox1 False                          True
PFMailbox2 False                          False
PFMailbox3 False                          False
PFMailbox4 False                          False

IsExcludedFromServingHierarchy was False for all 4 servers, which indicates they are not blocked from serving hierarchy. However, the hierarchy was not ‘ready’ for 3 of them. This could be due to the hierarchy being out of date or not being created at all.

The output of (Get-PublicFolderMailboxDiagnostics PFMailbox2 -IncludeHierarchyInfo).SyncInfo indeed indicated there were problems synchronizing contents from the PFMailbox1 mailbox. We then ran the following cmdlet to trigger updating synchronizing the hierarchy again:

Update-PublicFolderMailbox –InvokeSynchronizer –Identity PFMailbox2

image

The Get-Mailbox –Identity PFMailbox2 –PublicFolder | Select Name,*Hierarchy* now showed IsHierarchyReady was True. We ran the same cmdlet for the other two Public Folder mailboxes as well.

After a while, we verified the effect on the assignment of DefaultPublicFolderMailbox on the mailboxes:

Get-Mailbox –ResultSize Unlimited | Group DefaultPublicFolderMailbox –NoElement

Count Name
----- ----
2601  contoso.com/Accounts/Users/PFMBPFMailbox2
2309  contoso.com/Accounts/Users/PFMBPFMailbox4
2632  contoso.com/Accounts/Users/PFMBPFMailbox1
2597  contoso.com/Accounts/Users/PFMBPFMailbox3

Public folder assignments were now (more or less) equally distributed over the 4 Public Folder mailboxes, and life was good.

We also verified Public Folder access distribution by querying the Exchange RpcClientAccess log files. An excellent tool to aid in this task is LogParser with LogParser Studio. We configured LogParser Studio to query log files at ‘<Installation folder>\Logging\RPC Client Access’ on the Exchange servers. The query used, grouped all entries per date, operation (in this case we are only interested in PublicLogon), and part of the field ‘operation-specific’; more exactly, the legacyDN part which tells which (Public Folder) mailbox was accessed:

SELECT EXTRACT_PREFIX([#Fields: date-time], 0, ‘T’) As Date, Count (*) as Total, [Operation],
EXTRACT_PREFIX(EXTRACT_SUFFIX([operation-specific], 0, ‘cn=’), 0, ‘ in database ‘) as PFMailbox
FROM ‘[LOGFILEPATH]’
WHERE [operation]=’PublicLogon’
AND [failures] IS NULL
GROUP BY Date, [Operation], PFMailbox
ORDER BY Date ASC

The output showed all Public Folder mailboxes were now accessed by clients, and logons to the Public Folder mailboxes were now (more or less) equally distributed:

image

Blocking Mixed Exchange 2013/2016 DAG

Ex2013 LogoIn the RTM version of Exchange 2016, there’s an issue in that it is allows you to add Exchange 2016 Mailbox servers to Exchange 2013 Database Availability Groups, and vice-versa. As stated in the Release Notes (you do read those?), creating such a mixed version DAG is not supported. In theory, you could even jeopardize your Exchange data, as database structures from both versions are different. This action is also not prevented from the Exchange Admin Center, requiring organizations to have very strict procedures and knowledgeable Exchange administrators.

If you are worried about this situation and you want to prevent accidently adding Mailbox servers to an existing DAG consisting of members of a different Exchange version, there is a way (until this is blocked by the product itself, of course). Cmdlet Extension Agents to the rescue!

The Scripting Agent not only allows you to add additional instructions to existing Exchange cmdlets, but also to provide additional validation before cmdlets are executed. I did two short articles on Cmdlet Extension Agents’ Scripting Agent here and here, so I will skip introductions.

First you need to download a file named ScriptingAgentConfig.xml from the location below. If you already have Scripting Agents, you need to integrate the code in your existing ScriptingAgentConfig.xml files. The code checks if the server you want to add using the Add-DatabaseAvailabilityGroup cmdlet is of a different major version than one of the current DAG members.

Next, you need to copy this ScriptingAgentConfig.xml file to $ENV:ExInstallPath on every Exchange 2013 and Exchange 2016 server in your organization, e.g. C:\Program Files\Microsoft\Exchange Server\V15\Bin\CmdletExtensionAgents\ScriptingAgentConfig.xml.  To help your with this process, Exchange fellow Paul Cunningham made a small script to push this XML from the current folder to every Exchange server in your organization, PushScriptingAgentConfig.ps1.

Last step is to enable the Scripting Agent using:

Enable-CmdletExtensionAgent ‘Scripting Agent’

After distributing the scripting agent file and enabling the scripting agent, when you try to add an Exchange 2016 (version 15.1) server to an Database Availability Group consisting of Exchange 2013 Mailbox servers, using Add-DatabaseAvailabilityGroupServer, you will receive an error message:

DAGCheck

This also works vice-versa, thus when you inadvertently try to add Exchange 2013 servers to an Exchange 2016 Database Availability Group, provided you distributed the XML on the Exchange 2013 servers as well. The error is also thrown when you try to perform this action using the Exchange Admin Console.

You can download the ScriptingAgentConfig.XML for blocking Mixed Exchange 2013/2016 DAGs from the TechNet here.

Clearing AutoComplete and other Recipient Caches

Exchange 2010 Logo

Last version: 1.21, April 28th, 2021: Updated formatting and link to GitHub

Anyone who has participated in migrations or transitions to Exchange has most likely encountered or has had to work around potential issues caused by the nickname cache. A “cache,” also known by its file extension, NK2 in older Outlook clients, is a convenience feature in Outlook and Outlook WebApp (OWA) which lets users pick recipients from a list of frequently-used recipients. This list is displayed when the end user types in the first few letters.

The potential issue revolves around end users using those lists to send messages, as the list contains cached recipient information. Because this information is static, it may become invalid at some point. Thus, when users pick recipients when sending messages, they may be sending messages to non-existent recipients or invalid e-mail addresses, which create issues like non-delivery of e-mail.

Read the full article over on ENow Solutions Engine blog.

Clean-AutoComplete

Using the script mentioned in the article, which can be used to clear cached recipient information, is straightforward. It requires Exchange 2010 or later and Exchange Web Services Managed API 1.2 (or later) which you can download here. Alternatively, you can copy the Microsoft.Exchange.WebServices.DLL with the script as it will also look for it in the current folder.

The script Clean-AutoComplete.ps1 has the following syntax:

Clear-AutoComplete.ps1 [-Mailbox] <String> [-Server <String>] [-Impersonation] [-Credentials <PSCredential>] [-Type <Array>] [-Pattern <String[]>]

Where:

  • Mailbox is the name or e-mail address of the mailbox.
  • Server is the name of the Client Access Server to access for Exchange Web Services. When omitted, the script will use AutoDiscover.
  • Switch Impersonation specifies if impersonation will be used for mailbox access, otherwise the current user context will be used.
  • Credentials specifies the user credentials to use.
  • Type specifies what cached recipient information to clear. Options are Outlook  (Outlook AutoComplete stream), OWA (OWA Autocomplete stream), SuggestedContacts, RecipientCache or All. Default is Outlook,OWA.
  • Pattern is the pattern of e-mail entries to remove from cache. Only works with OWA, SuggestedContacts and RecipientCache type clearances.

So for example, suppose you want to clear the Autocomplete stream used by Outlook on a mailbox, you can use:

Clear-AutoComplete.ps1 -Identity Olrik -Type Outlook -Verbose
ScreenCap

To remove the Autocomplete stream used by OWA on your Office 365 account, you can use:

Clear-AutoComplete.ps1 -Identity olrik@office365tenant.com –Credentials (Get-Credential) –Type OWA

Be advised that clearing the Outlook AutoComplete stream will only have effect for Outlook running in Online mode. Outlook caches this information as well in the OST file, leaving the options of running Outlook with the /CleanAutocompleteCache switch, or remove and let Outlook recreate the OST file. The temporary Stream_AutoComplete *.dat files created under %USERPROFILE%\AppData\Local\Microsoft\Outlook\RoamCache are used by Outlook to speed things up.

Disabling Auto-Complete and Suggested Contacts
Alternatively, you can disable Auto-Complete, the equivalent of unchecking the Outlook option ‘Use Auto-Complete List to suggest names when typing in the To, Cc and Bcc line‘, by setting the following registry key:

Note: In the examples below, you need to modify the version number in the examples corresponding to the Outlook version you wish to apply these settings against. Use 16.0 as indicated for Outlook 2016, but change it to 15.0 for Outlook 2013, or 14.0 for Outlook 2010.

HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Outlook\Preferences\
ShowAutoSug=0 (REG_DWORD)

To configure this setting using a Group Policy, use the following registry setting:

HKEY_CURRENT_USER\Software\Policies\Microsoft\office\16.0\Outlook\Preferences\ShowAutoSug=0 (REG_DWORD)

You can also disable Suggested Contacts folder, the equivalent of unchecking the Outlook option ‘Automatically create Outlook contacts for recipients that do not belong to an Outlook Address Book’, with the following registry key:

HKEY_CURRENT_USER\Software\Microsoft\office\16.0\Outlook\Contact\CreateContactsForOneOffs= 0 (REG_DWORD)

The related Group Policy setting is:

HKEY_CURRENT_USER\Software\Policies\Microsoft\office\16.0\Outlook\Contact\CreateContactsForOneOffs= 0 (REG_DWORD)

Feedback
Feedback is welcomed through the comments. If you got scripting suggestions or questions, do not hesitate using the contact form.

Download
You can download the script from GitHub here.