The Smart Way to Update Zabbix Agents: PowerShell Automation for Businesses

As a Managed Service Provider (MSP) in Boca Raton, FL, automation is at the heart of delivering efficient, reliable IT services to our clients across South Florida. One common challenge we face is keeping monitoring Zabbix Agents up to date. While Zabbix is a powerful open-source monitoring solution, it lacks an evergreen download link for its Windows agent. This means administrators often need to manually check for the latest version, update scripts, and redeploy installers—a time-consuming and error-prone process.

To solve this, we developed a PowerShell function that dynamically retrieves the latest Zabbix Windows Agent 2 MSI from the official CDN and optionally downloads it. This eliminates the need for hardcoding URLs or manually updating scripts whenever a new version is released.

Why This Matters

  • Time Savings: No more manual checks or script edits for every update.
  • Consistency: Always deploy the latest stable version across your environment.
  • Automation-Friendly: Integrates seamlessly into deployment pipelines, RMM tools, or scheduled tasks.
  • Error Reduction: Avoid broken links or outdated agents that compromise monitoring accuracy.

For MSPs and IT teams managing multiple endpoints, this approach ensures proactive monitoring without unnecessary overhead.

What is Zabbix and How Do We Use It to Help Businesses?

Zabbix is a powerful, open-source monitoring platform that tracks the health and performance of servers, networks, applications, and cloud services in real time. For businesses, this means early detection of issues before they impact operations, improved uptime, and better resource planning. At our MSP in Boca Raton, we leverage Zabbix as one of our core tools to proactively identify and resolve problems before they disrupt your business. This proactive approach keeps your systems running efficiently, minimizes downtime, and ensures your team stays productive. By combining Zabbix with our automation strategies, we help clients maintain a stable, secure, and high-performing IT environment.

How It Works

The function scans the Zabbix CDN, detects the newest version (or a specific train like 7.4), and returns a fully qualified URL. If you specify a download path, it retrieves the MSI and computes its SHA256 hash for integrity verification.

Key Parameters

  • Architecture: amd64 (default) or i386
  • Train: Optional (e.g., 7.4, 7.2). Auto-detects latest if omitted.
  • Channel: Defaults to stable
  • DownloadTo: Path to save the MSI (optional)
  • ListAllVersions: Returns all patch versions for a train
  • TimeoutSec, MaxRetries, RetryDelaySeconds: Network resilience options

Example Usage

PowerShell
# A) Get the newest overall Agent 2 (detect newest train automatically, amd64)
$r = Get-ZabbixAgent2LatestCdn -Architecture amd64 -Verbose
$r | Format-List

# B) Pin to a train (e.g., 7.2 LTS)
$rLts = Get-ZabbixAgent2LatestCdn -Train '7.2' -Architecture amd64
$rLts.Url

# C) Download and compute SHA256 with retry logic
$rDl = Get-ZabbixAgent2LatestCdn -Architecture amd64 -DownloadTo $env:TEMP -MaxRetries 5 -Verbose
$rDl.FilePath
$rDl.Sha256

# D) Download with explicit error handling
try {
    $result = Get-ZabbixAgent2LatestCdn -Train '7.4' -DownloadTo 'C:\Temp\Zabbix' -Verbose
    Write-Host "Successfully downloaded to: $($result.FilePath)" -ForegroundColor Green
    Write-Host "SHA256: $($result.Sha256)" -ForegroundColor Green
}
catch {
    Write-Error "Failed to download Zabbix Agent 2: $_"
}

The output includes:

  • Full version (e.g., 7.4.1)
  • Architecture
  • CDN URL
  • SHA256 hash
  • File size and path (if downloaded)

Why We Built This

As an MSP serving businesses in Boca Raton and throughout South Florida, we manage diverse IT environments where uptime and monitoring accuracy are critical. Automating repetitive tasks like agent updates allows us to focus on strategic IT initiatives for our clients—improving security, scalability, and performance.


Want to Simplify IT Management?

If your business struggles with manual updates, monitoring gaps, or IT inefficiencies, our team can help. We specialize in managed IT services, automation, and proactive monitoring solutions tailored for businesses in Boca Raton and the South Florida region.

👉 Contact us today to learn how we can streamline your IT operations.

The Code

PowerShell
function Get-ZabbixAgent2LatestCdn {
    <#
    .SYNOPSIS
        Returns the fully versioned CDN URL for the latest Windows Zabbix Agent 2 MSI.

    .PARAMETER Architecture
        'amd64' (default) or 'i386'.

    .PARAMETER Train
        Optional (e.g., '7.4', '7.2', '7.0'). If omitted, the newest train is auto-detected from the CDN.

    .PARAMETER Channel
        CDN channel. Default: 'stable'.

    .PARAMETER DownloadTo
        Optional path to save the MSI. If provided, the file is downloaded and SHA256 is computed.

    .PARAMETER MaxRetries
        Maximum number of retry attempts for network operations. Default: 3.

    .PARAMETER RetryDelaySeconds
        Delay in seconds between retry attempts. Default: 2.

    .PARAMETER TimeoutSec
        Timeout in seconds for web requests. Default: 30.

    .PARAMETER ListAllVersions
        If specified, returns all available patch versions (X.Y.Z) for the selected train instead of the latest.

    .PARAMETER BaseUrl
        Base URL for Zabbix binaries CDN. Default: "https://cdn.zabbix.com/zabbix/binaries/".

    .OUTPUTS
        PSCustomObject with properties:
            - Train:        The major/minor train (e.g., '7.4').
            - Version:      The full version (e.g., '7.4.1').
            - Architecture: The architecture of the Zabbix Agent 2 ('amd64' or 'i386').
            - Url:          The fully qualified URL to the MSI file.
            - Sha256:       The SHA256 hash of the downloaded MSI (if downloaded).
            - FilePath:     The file path provided or resolved for the MSI (may be relative or as given).
            - FullFilePath: The absolute/resolved full file path to the MSI (if downloaded or exists).
            - FileSize:     The size of the MSI file in bytes (if available).
            - Channel:      The CDN channel used (e.g., 'stable').
            - Source:       The source domain (e.g., 'cdn.zabbix.com').
            - FileName:     The MSI file name.

    .EXAMPLE
        $r = Get-ZabbixAgent2LatestCdn -Architecture amd64
        $r.Url

    .EXAMPLE
        $r = Get-ZabbixAgent2LatestCdn -Train '7.2' -DownloadTo 'C:\Temp' -Verbose
    #>
    [CmdletBinding()]
    [OutputType([PSCustomObject], [String[]])] # Clarify output types
    param(
        [ValidateSet('amd64','i386')]
        [string]$Architecture = 'amd64',

        [ValidatePattern('^\d+\.\d+$')]
        [string]$Train,

        [ValidateSet('stable','lts','beta','alpha')]
        [string]$Channel = 'stable',

        [string]$DownloadTo,

        [ValidateRange(1, 10)]
        [int]$MaxRetries = 3,

        [ValidateRange(1, 60)]
        [int]$RetryDelaySeconds = 2,

        [ValidateRange(10, 300)]
        [int]$TimeoutSec = 30,

        [switch]$ListAllVersions,

        [string]$BaseUrl = "https://cdn.zabbix.com/zabbix/binaries/"
    )

    begin {
        # Ensure TLS 1.2+ is enabled
        try {
            $currentProtocol = [Net.ServicePointManager]::SecurityProtocol
            if ($currentProtocol -notmatch 'Tls12|Tls13') {
                [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13
                $PSCmdlet.WriteVerbose("Enabled TLS 1.2/1.3 for secure connections")
            }
        }
        catch {
            $PSCmdlet.WriteWarning("Failed to configure TLS protocol: $_")
        }

        # Join base URL and channel for web URLs
        if ($BaseUrl.EndsWith('/')) {
            $base = "$BaseUrl$Channel/"
        } else {
            $base = "$BaseUrl/$Channel/"
        }

        # Helper function for retrying web requests
        function Invoke-WebRequestWithRetry {
            param(
                [string]$Uri,
                [string]$Method = 'GET',
                [string]$OutFile,
                [int]$Retries = $MaxRetries,
                [int]$Delay = $RetryDelaySeconds,
                [int]$TimeoutSec = $TimeoutSec
            )

            $attempt = 0
            $lastError = $null

            while ($attempt -lt $Retries) {
                $attempt++
                try {
                    Write-Verbose "Attempt $attempt of $Retries for $Uri"
                    
                    $params = @{
                        Uri              = $Uri
                        UseBasicParsing  = $true
                        ErrorAction      = 'Stop'
                        TimeoutSec       = $TimeoutSec
                        Headers          = @{ 'User-Agent' = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) PowerShell Script' }
                    }

                    if ($Method) { $params['Method'] = $Method }
                    if ($OutFile) {
                        $params['OutFile'] = $OutFile
                        # Show progress for downloads
                        Write-Progress -Activity "Downloading Zabbix Agent 2" -Status "Downloading from CDN..." -PercentComplete 0
                    }

                    $response = Invoke-WebRequest @params
                    
                    if ($OutFile) {
                        Write-Progress -Activity "Downloading Zabbix Agent 2" -Completed
                    }
                    
                    return $response
                }
                catch {
                    $lastError = $_
                    Write-Verbose "Attempt $attempt failed: $($_.Exception.Message)"
                    
                    if ($attempt -lt $Retries) {
                        Write-Verbose "Retrying in $Delay seconds..."
                        Start-Sleep -Seconds $Delay
                    }
                }
            }

            # All retries exhausted
            throw "Failed after $Retries attempts for $Uri. Last error: $($lastError.Exception.Message)"
        }
    }

    process {
        try {
            # Auto-detect newest train if not provided
            if (-not $Train) {
                $PSCmdlet.WriteVerbose("Auto-detecting latest train from $base")
                
                $root = Invoke-WebRequestWithRetry -Uri $base
                
                if ([string]::IsNullOrWhiteSpace($root.Content)) {
                    throw "Empty response received from CDN root: $base"
                }

                $trains = [regex]::Matches($root.Content, 'href="(?<t>\d+\.\d+)/"') |
                    ForEach-Object { $_.Groups['t'].Value } |
                    Where-Object { $_ -match '^\d+\.\d+$' } |
                    Sort-Object { [version]$_ } -Descending

                if (-not $trains -or $trains.Count -eq 0) {
                    throw "Could not enumerate trains at $base. Please specify -Train parameter explicitly."
                }
                
                $Train = $trains[0]
                $PSCmdlet.WriteVerbose("Auto-detected train: $Train")
            }
            else {
                $PSCmdlet.WriteVerbose("Using specified train: $Train")
            }

            # Enumerate patch versions under the chosen train
            $trainUri = "$base$Train/"
            $PSCmdlet.WriteVerbose("Fetching patch versions from $trainUri")
            
            $trainPage = Invoke-WebRequestWithRetry -Uri $trainUri

            if ([string]::IsNullOrWhiteSpace($trainPage.Content)) {
                throw "Empty response received from train URI: $trainUri"
            }

            $versions = [regex]::Matches($trainPage.Content, 'href="(?<v>\d+\.\d+\.\d+)/"') |
                ForEach-Object { $_.Groups['v'].Value } |
                Where-Object { $_ -match '^\d+\.\d+\.\d+$' } |
                Sort-Object { [version]$_ } -Descending

            if (-not $versions -or $versions.Count -eq 0) {
                throw "No patch directories (X.Y.Z) found under $trainUri. The train may not exist or may not have releases yet."
            }

            if ($ListAllVersions) {
                $PSCmdlet.WriteVerbose("Returning all available patch versions for train $Train")
                return $versions
            }

            $latestVersion = $versions[0]
            $PSCmdlet.WriteVerbose("Latest version in train $Train is $latestVersion")

            # Build the MSI URL
            $file = "zabbix_agent2-$latestVersion-windows-$Architecture-openssl.msi"
            $url = "$trainUri$latestVersion/$file"
            $PSCmdlet.WriteVerbose("Constructed MSI URL: $url")

            # HEAD request to verify MSI exists
            $PSCmdlet.WriteVerbose("Verifying MSI availability...")
            try {
                $head = Invoke-WebRequestWithRetry -Uri $url -Method Head

                if ($head.StatusCode -ne 200) {
                    throw "MSI returned unexpected status code: $($head.StatusCode)"
                }

                # Get file size if available
                $fileSizeRaw = $head.Headers['Content-Length']
                $fileSize = $null
                if ($fileSizeRaw) {
                    # Ensure we get a single value and parse as long
                    $fileSizeStr = ($fileSizeRaw | Select-Object -First 1)
                    if ($fileSizeStr -match '^\d+$') {
                        $fileSize = [long]$fileSizeStr
                        $PSCmdlet.WriteVerbose("MSI file size: {0} MB" -f ([math]::Round($fileSize / 1MB, 2)))
                    } else {
                        $PSCmdlet.WriteVerbose("Content-Length header is not a valid number: $fileSizeStr")
                    }
                }
            }
            catch {
                throw "Expected MSI not found at $url. Error: $($_.Exception.Message)"
            }

            # Prepare result object
            $result = [pscustomobject]@{
                Train        = $Train
                Version      = $latestVersion
                Architecture = $Architecture
                Url          = $url
                Sha256       = $null
                FilePath     = $null
                FullFilePath = $null
                FileSize     = $fileSize
                Channel      = $Channel
                Source       = 'cdn.zabbix.com'
                FileName     = $file
            }

            # Optional download and hash computation
            if ($DownloadTo) {
                # Validate DownloadTo path
                if ([string]::IsNullOrWhiteSpace($DownloadTo)) {
                    throw "DownloadTo path cannot be empty."
                }

                $PSCmdlet.WriteVerbose("Download requested to: $DownloadTo")
                
                # Determine full destination path
                $dest = $DownloadTo
                if (Test-Path -LiteralPath $dest -PathType Container) {
                    $dest = Join-Path $dest $file
                }
                else {
                    $parent = Split-Path -Parent $dest
                    if ($parent -and -not (Test-Path $parent)) {
                        $PSCmdlet.WriteVerbose("Creating directory: $parent")
                        New-Item -ItemType Directory -Path $parent -Force -ErrorAction Stop | Out-Null
                    }
                }

                # Check if file already exists
                if (Test-Path -LiteralPath $dest) {
                    $PSCmdlet.WriteVerbose("File already exists at destination: $dest")
                    $PSCmdlet.WriteVerbose("Computing hash of existing file...")
                    try {
                        $existingHash = (Get-FileHash -Path $dest -Algorithm SHA256 -ErrorAction Stop).Hash
                        # Verify existing file size matches expected
                        $existingSize = (Get-Item -LiteralPath $dest).Length
                        if ($fileSize -and $existingSize -ne [long]$fileSize) {
                            $PSCmdlet.WriteWarning("Existing file size ($existingSize bytes) differs from CDN ($fileSize bytes). Re-downloading...")
                            Remove-Item -LiteralPath $dest -Force -ErrorAction Stop
                        }
                        else {
                            $PSCmdlet.WriteVerbose("Using existing file with SHA256: $existingHash")
                            $result.FilePath = $dest
                            $result.FullFilePath = (Resolve-Path -LiteralPath $dest).Path
                            $result.Sha256 = $existingHash
                            return $result
                        }
                    }
                    catch {
                        $PSCmdlet.WriteWarning("Could not verify existing file: $_. Re-downloading...")
                        Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue
                    }
                }

                # Download the file
                $PSCmdlet.WriteVerbose("Downloading MSI to: $dest")
                try {
                    Invoke-WebRequestWithRetry -Uri $url -OutFile $dest
                    # Verify download succeeded and file exists
                    if (-not (Test-Path -LiteralPath $dest)) {
                        throw "Download completed but file not found at destination: $dest"
                    }
                    $downloadedSize = (Get-Item -LiteralPath $dest).Length
                    if ($downloadedSize -eq 0) {
                        throw "Downloaded file is empty (0 bytes)"
                    }
                    $PSCmdlet.WriteVerbose("Download complete. File size: $([math]::Round($downloadedSize / 1MB, 2)) MB")
                    # Compute hash
                    $PSCmdlet.WriteVerbose("Computing SHA256 hash...")
                    $hash = Get-FileHash -Path $dest -Algorithm SHA256 -ErrorAction Stop
                    $result.FilePath = $dest
                    $result.FullFilePath = (Resolve-Path -LiteralPath $dest).Path
                    $result.Sha256 = $hash.Hash
                    $PSCmdlet.WriteVerbose("SHA256: $($hash.Hash)")
                }
                catch {
                    # Clean up partial download
                    if (Test-Path -LiteralPath $dest) {
                        $PSCmdlet.WriteWarning("Cleaning up partial download...")
                        Remove-Item -LiteralPath $dest -Force -ErrorAction SilentlyContinue
                    }
                    throw "Download failed: $($_.Exception.Message)"
                }
            }

            return $result
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    }
}

Let’s talk.

Book a free consultation with us today and discover how the right IT partner can transform your operations and future-proof your business.

Reach out to us today to learn how we can help optimize your IT infrastructure and ensure your business runs smoothly. 561-556-2000

Are you interested in more articles? Check out Syncro PowerShell to set Windows Power Settings

Require assistance?

Support from our knowledgeable help desk staff ensures your team stays productive by swiftly and accurately resolving issues.