Exchange Server

Exchange Server 2019 Setup

Full Exchange Server 2019 deployment on a Vultr VPS joined to Active Directory. Covers prerequisites, installation errors (Defender freeze fix, console coordinates), Let's Encrypt SSL via Win-ACME, and Office 365 Hybrid Configuration Wizard troubleshooting.

Exchange Server 2019 setup in hybrid AD lab
Setting up enterprise mail flow with Exchange Server 2019
Windows Server 2022
Exchange Server 2019
UCMA 4.0
Win-ACME SSL
IIS 10.0
Microsoft Defender
.NET Framework 4.8
Cloudflare DNS

VPS Specifications

Exchange 2019 is heavy. It needs at minimum 16 GB RAM and 4 cores to run without grinding to a halt. Deployed a separate Vultr instance for it:

SettingValue
PlanCloud Compute — High Performance (VX1-M-4C-32G)
vCPUs4
RAM32 GB
Storage240 GB SSD
OSWindows Server 2022 Standard
LabelMAILSERVER (EXCH01)
Vultr deploy summary showing London location, vx1-m-4c-32g-240s CPU plan with 4 vCPUs and 32 GB RAM
Selecting location, 4 vCPUs, and 32 GB RAM on Vultr
Vultr Server Type selection window with Windows Standard 2022 x64 selected
Selecting Windows Server 2022 Standard as the OS type

Step 1 — Server Preparation

Booted the server. The Vultr vendor script executed in the background to set initial parameters, activate Windows, configure pagefile sizes, and clean up visual settings.

Vultr dashboard showing noVNC console with Windows Server 2022 boot script completing background configurations
First boot — vendor script configuring host parameters

First step was setting up the private network interface (10.8.96.5) and pointing the DNS to DC01 (10.10.0.1) so it could resolve the AD root domain.

IPv4 Properties window showing static IP set to 10.8.96.5 and DNS pointed to 10.10.0.1
Configuring static private IP and DNS resolver
ipconfig command output showing Ethernet adapter instance with autoconfigured IP and private IP
ipconfig output confirming interface state
IPv4 Properties window updating DNS settings to point to the Domain Controller IP 10.8.96.3
Updating DNS to match the Domain Controller private IP

Tested connection to the DC using nslookup to verify domain name resolution:

PowerShell — DNS Test
PS> nslookup hq.apex-corp.xyz
Server: dc01.hq.apex-corp.xyz Address: 10.8.96.3 Name: hq.apex-corp.xyz Addresses: 10.8.96.3
PowerShell — Ping Test
PS> ping dc01.hq.apex-corp.xyz -n 4
Pinging dc01.hq.apex-corp.xyz [10.8.96.3] with 32 bytes of data: Reply from 10.8.96.3: bytes=32 time=2ms TTL=128 Reply from 10.8.96.3: bytes=32 time=2ms TTL=128 Reply from 10.8.96.3: bytes=32 time=2ms TTL=128 Reply from 10.8.96.3: bytes=32 time=2ms TTL=128
nslookup command resolving hq.apex-corp.xyz to the DC IP 10.8.96.3
nslookup confirms the DC01 DNS is responding
PowerShell ping from the Domain Controller to the Mail Server showing successful 2ms latency replies
Pinging from DC to Mail Server to confirm local routing works

Step 2 — Install Windows Server Features

Exchange requires a specific set of IIS web roles and management modules. The naming conventions on Windows Server 2022 require using legacy NET-Framework-45 names in the PowerShell command, although it installs 4.8 under the hood.

PowerShell
Install-WindowsFeature NET-Framework-45-Features, RPC-over-HTTP-proxy,
  RSAT-Clustering, RSAT-Clustering-CmdInterface, RSAT-Clustering-Mgmt,
  RSAT-Clustering-PowerShell, Web-Mgmt-Console, WAS-Process-Model,
  Web-Asp-Net45, Web-Basic-Auth, Web-Client-Auth, Web-Digest-Auth,
  Web-Dir-Browsing, Web-Dyn-Compression, Web-Http-Errors, Web-Http-Logging,
  Web-Http-Redirect, Web-Http-Tracing, Web-ISAPI-Ext, Web-ISAPI-Filter,
  Web-Lgcy-Mgmt-Console, Web-Metabase, Web-Mgmt-Console, Web-Mgmt-Service,
  Web-Net-Ext45, Web-Request-Monitor, Web-Server, Web-Stat-Compression,
  Web-Static-Content, Web-Windows-Auth, Web-WMI, Windows-Identity-Foundation,
  RSAT-ADDS -Restart
PowerShell Install-WindowsFeature output showing successful role deployment
Installing mandatory IIS and RSAT roles via PowerShell

Step 3 — Join to the Domain

Joined the new Mail Server to the domain using a domain admin account:

PowerShell
PS> Add-Computer -DomainName "hq.apex-corp.xyz" -Credential (Get-Credential) -Restart
Windows credential prompt dialog for domain join with user credentials entered
Entering Domain Admin credentials to join the forest

Verified domain join by checking access to active network shares on the Domain Controller (STARK-TOWER):

Windows File Explorer showing active network shares CertEnroll, SYSVOL, HR-Records on STARK-TOWER
Verifying domain join by browsing STARK-TOWER shares

Step 4 — Install Exchange Prerequisites

Before installing Exchange, download the Exchange 2019 ISO along with three crucial dependencies: Visual C++ 2013, Visual C++ 2015-2022, Unified Communications Managed API (UCMA) 4.0, and the IIS URL Rewrite module.

Web browser showing download progress of ExchangeServer2019-x64-CU14.ISO
Downloading the Exchange 2019 Cumulative Update 14 ISO
PowerShell — Prerequisite Script
New-Item -Path "C:\ExchangePrep" -ItemType Directory -Force
cd "C:\ExchangePrep"

# 1. Visual C++ 2013
Invoke-WebRequest -Uri "https://download.microsoft.com/download/2/E/6/2E61CFA4-993B-4DD4-91DA-3737CD5CD6E3/vcredist_x64.exe" -OutFile "vcredist_x64.exe"
Start-Process .\vcredist_x64.exe -ArgumentList '/quiet /norestart' -Wait

# 2. Visual C++ 2015-2022
Invoke-WebRequest -Uri "https://aka.ms/vs/17/release/vc_redist.x64.exe" -OutFile "vc_redist_x64.exe"
Start-Process .\vc_redist_x64.exe -ArgumentList '/quiet /norestart' -Wait

# 3. UCMA 4.0
Invoke-WebRequest -Uri "https://download.microsoft.com/download/2/C/4/2C47A12D-DCA3-4C11-AC1F-95B72FA1D5C2/UcmaRuntimeSetup.msi" -OutFile "UcmaRuntimeSetup.msi"
Start-Process msiexec.exe -ArgumentList '/i UcmaRuntimeSetup.msi /quiet /norestart' -Wait

# 4. IIS URL Rewrite Module 2.1
Invoke-WebRequest -Uri "https://download.microsoft.com/download/1/2/8/128E2E22-C1B9-44A4-BE2A-5859ED1D4592/rewrite_amd64_en-US.msi" -OutFile "rewrite_amd64.msi"
Start-Process msiexec.exe -ArgumentList '/i rewrite_amd64.msi /quiet /norestart' -Wait

cd C:\
Remove-Item -Path "C:\ExchangePrep" -Recurse -Force

Step 5 — Prepare the AD Forest for Exchange

Mount the downloaded ISO and run the preparatory commands to extend the Active Directory schema. Run this from an elevated session as Schema Admin / Domain Admin.

PowerShell running Mount-DiskImage for the Exchange ISO
Mounting the ISO to read setup files

Prepare the schema first:

PowerShell
PS> F:\Setup.exe /PrepareSchema /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF
PowerShell showing schema extension and organization prep completing successfully
Preparing Active Directory schema organization
Error: /PrepareAllDomains fails

Running the standard /PrepareAllDomains switch returned a DomainNotReachableException because the child domain vought.hq.apex-corp.xyz was temporarily unreachable. The setup script crashed trying to contact it.

Fix: Run a targeted prep only for the active domain:

D:\Setup.exe /PrepareDomain:hq.apex-corp.xyz /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF

Step 6 — Install Exchange Server

Run the unattended installer. Crucial Warning: Windows Defender real-time scanning will intercept every single file copy operation, pinning the CPU to 100% and causing the installer to freeze indefinitely at **16%**.

PowerShell
PS> Set-MpPreference -DisableRealtimeMonitoring $true PS> D:\Setup.exe /Mode:Install /Role:Mailbox /IAcceptExchangeServerLicenseTerms_DiagnosticDataOFF
Exchange unattended setup running Mailbox role installation showing progress during file copy
Unattended setup running — installing the Mailbox role
Exchange Server installation progress showing file copy progress at 34 percent
Disabling Defender real-time protection speeds up this process from hours to 20 minutes
Error: Setup Console Coordinate Crash

If you click inside or resize the PowerShell window during setup, it will crash with a coordinate mismatch exception. Relaunch it and don't touch the window. Open a separate console to monitor the logs: Get-Content C:\ExchangeSetupLogs\ExchangeSetup.log -Tail 10 -Wait

Once installation finishes, reboot and verify that all key Microsoft Exchange services are active and running:

PowerShell showing running statuses of Microsoft Exchange transport, topology, and mailbox services
Verifying Microsoft Exchange services are active after reboot

Step 7 — Post-Install Configuration

Tested access to Outlook Web App (OWA) locally at https://localhost/owa:

Outlook Web App login interface page prompting for domain username and password
Outlook Web App login screen

Opened the Exchange Admin Center (EAC) to verify recipients. Registered the Tony Stark account and created mailboxes for synced users.

Exchange Admin Center showing user mailbox creation menu with Tony Stark listed
EAC recipients dashboard — adding mailboxes

Opened the Exchange Management Shell (EMS) to script mailbox creation for all domain users:

Windows Start Menu showing search result for Exchange Management Shell
Opening the Exchange Management Shell
PowerShell — EMS
Get-ADUser -Filter * -SearchBase "DC=hq,DC=apex-corp,DC=xyz" | ForEach-Object {
    Write-Host "Creating mailbox for: $($_.SamAccountName)..." -ForegroundColor Cyan
    Enable-Mailbox -Identity $_.SamAccountName -ErrorAction SilentlyContinue
}
PowerShell script output enabling mailboxes for users Rogers, Romanoff, Banner, Fury, Stark
EMS script successfully provisioning mailboxes for all synced AD users

Step 8 — Enable Inbound Public Email

By default, Exchange Frontend Receive connectors block external anonymous SMTP handshakes to prevent acting as a spam relay. To allow public incoming mails from external addresses (like Gmail), configure the frontend connectors.

PowerShell — EMS
# Register the public domain
New-AcceptedDomain -Name "apex-corp.xyz" -DomainName "apex-corp.xyz" -DomainType Authoritative

# Update the default address policies to apply the public domain suffix
Set-EmailAddressPolicy -Identity "Default Policy" -EnabledEmailAddressTemplates "SMTP:%[email protected]"
Update-EmailAddressPolicy -Identity "Default Policy"

# Authorize anonymous SMTP handshakes on port 25
$serverName = $env:COMPUTERNAME
Set-ReceiveConnector -Identity "$serverName\Default Frontend $serverName" -PermissionGroups AnonymousUsers

Verified OWA login from external endpoints. Logged in as Bruce Wayne (HQ\b.wayne) on a mobile device and verified local mail delivery:

Mobile browser displaying OWA login page on server IP 45.63.99.87 with user credentials entered
Logging into OWA from a mobile browser
Mobile OWA inbox for Bruce Wayne showing test emails received from Tony Stark
Mobile inbox confirming internal mail delivery works

Step 9 — Let's Encrypt SSL Certificate

Configured Cloudflare DNS A records pointing the public mail hosts to the Exchange public IP, along with MX and SPF records.

Cloudflare DNS dashboard showing A records for mail, CNAME for autodiscover, and MX and SPF TXT records for apex-corp.xyz
Configuring DNS records in Cloudflare — proxy must be OFF (grey cloud)
EMS terminal showing New-AcceptedDomain output confirming apex-corp.xyz is registered
Confirming public accepted domain registry in Exchange

Used Win-ACME (wacs.exe) to request and bind Let's Encrypt certificates for mail.apex-corp.xyz and autodiscover.apex-corp.xyz:

Win-ACME command line utility output showing SSL certificate creation and IIS binding setup
Win-ACME automated certificate enrollment and IIS integration
PowerShell displaying certificates list with Let's Encrypt issuer and IIS and SMTP services assigned
SSL Certificate registered and assigned to services
Error: Empty Certificate Thumbprint

When selecting the certificate by Subject in PowerShell, the query can return empty if SAN names differ. Use Issuer query instead:

$cert = Get-ExchangeCertificate | Where-Object {$_.Issuer -like "*Let's Encrypt*"} | Select-Object -First 1

PowerShell retrieving the cert and enabling it for IIS and SMTP services
Enabling the certificate via PowerShell

Step 10 — Outbound Egress & Verification

ServiceStatus
SMTP Inbound (Port 25)Verified
SMTP OutboundVerified
OWA / ECP HTTPSVerified
ActiveSync (Mobile)Working
AutodiscoverConfigured
Let's Encrypt SSLBound
Outbound Send ConnectorActive

Configured an outbound send connector to allow sending mails to external domains via MX records:

EMS terminal executing New-SendConnector for OutboundInternet with AddressSpaces set to wildcard
Creating the outbound send connector in Exchange

Received mail from outside (Gmail) to verify inbound delivery:

Exchange SMTP tracking logs showing RECEIVE and SEND logs for email from admin@mrakashkumar.in
SMTP transport logs confirming external mail inbound handshakes
Tony Stark inbox in OWA showing email received from admin@mrakashkumar.in confirming external mail flows
OWA inbox confirming mail delivery from external address

Tested sending mail outbound from OWA to an external account. Outbound port 25 is blocked by default by most VPS providers. After opening a support ticket, Vultr unblocked port 25 egress, allowing mail to route out cleanly:

Mobile mail client inbox showing mail from Tony Stark at apex-corp.xyz successfully received
Verification — external email successfully received from the lab domain

Security Implications

This Exchange deployment introduces key enterprise attack surfaces:

Exchange WriteDACL

The Exchange Windows Permissions group possesses WriteDACL permissions on the domain object by default. Compromise of the Exchange server (MAILSERVER$) allows privilege escalation directly to Domain Admin.

OWA Password Spraying

Since OWA is publicly exposed on port 443, it is vulnerable to low-and-slow password spraying attacks against the domain users list.

GAL Exfiltration

Compromising a single low-privilege domain mailbox allows exfiltration of the entire Global Address List (GAL) through EWS or OWA API endpoints, facilitating highly targeted phishing.