Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions doc/100-General/10-Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Released closed milestones can be found on [GitHub](https://github.com/Icinga/ic

[Issues and PRs](https://github.com/Icinga/icinga-powershell-framework/milestone/46)

### Enhancements

* [#884](https://github.com/Icinga/icinga-powershell-framework/pull/884) Adds new feature, allowing to offload Windows Updates to a background task running as SYSTEM, ensuring Icinga for Windows itself can only run with minimal privileges, while Windows updates can still be fetched by using `Invoke-IcingaCheckUpdates`

## 1.15.0 (2026-06-30)

[Issues and PRs](https://github.com/Icinga/icinga-powershell-framework/milestone/45)
Expand Down
179 changes: 179 additions & 0 deletions doc/160-Features/10-Windows-Update-Offload.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
# Windows Update Offload

The Windows Update Offload feature allows Icinga for Windows to retrieve pending Windows updates using a dedicated background scheduled task running under the `NT AUTHORITY\SYSTEM` account, securely caching the results for the monitoring check plugin.

**Note:** Before using any of the commands below, you must initialize the Icinga PowerShell Framework inside an administrative PowerShell instance with `icinga -Shell`.

---

## Overview and Motivation

By default, security best practices dictate that the Icinga Agent should be run with the least necessary privileges—such as `NT AUTHORITY\NetworkService` or a dedicated service account—rather than `NT AUTHORITY\SYSTEM`.

However, the Windows Update API (`Microsoft.Update.Session` COM object) cannot be queried over remote connections or by unprivileged service accounts without administrative or SYSTEM permissions. Calling update checks in such environments leads to permission errors.

### Related Knowledge Base Articles

* **[IWKB000006](../knowledgebase/IWKB000006.md):** The user you are running this command as does not have permission to access the Windows Update ComObject "Microsoft.Update.Session".

### When to Use Windows Update Offload

* **Preferred Alternative to Running Everything as SYSTEM:** Rather than elevating the entire Icinga Agent service to `SYSTEM` (which introduces broader security risks), only the update-fetching routine is offloaded to a background task running as `SYSTEM`.
* **Alternative when JEA is not possible:** While [Just Enough Administration (JEA)](../130-JEA/01-Introduction.md) is supported by Icinga for Windows to grant elevated privileges, JEA cannot be implemented or deployed in every environment. If JEA is not viable and you want to monitor Windows Updates without running the Icinga Agent as `SYSTEM`, **Windows Update Offload is the preferred and recommended solution**.

---

## How It Works

1. **Background Scheduled Task:**
When enabled, a Windows Scheduled Task named `Fetch Windows Updates` is created under `\Icinga\Icinga for Windows\`. This task is configured to start automatically at system startup and runs as `NT AUTHORITY\SYSTEM` (`S-1-5-18`).

2. **Periodic Update Fetching:**
The task runs `jobs\FetchWindowsUpdates.ps1` in an endless loop. Every **10 minutes (600 seconds)**, it queries the `Microsoft.Update.Session` COM object for pending updates that are not yet installed (`IsInstalled=0`).

3. **Atomic and Secure Cache Storage:**
The serialized update objects are first written to a temporary file (`pending.xml.tmp`) and then atomically moved to `pending.xml` inside the cache directory (`cache\provider\windows_updates\pending.xml`). Directory and file permissions are strictly secured with `Set-IcingaUserPermissions` so that only `SYSTEM` and the Icinga for Windows service account have access.

4. **Transparent Check Integration:**
When `Invoke-IcingaCheckUpdates` is executed:
* **Offload Enabled:** The plugin reads the update list directly from the cached XML file, eliminating the need for elevated permissions at check execution time.
* **Offload Disabled:** The plugin queries the Windows Update COM object directly and live.

---

## Cache Age Monitoring & Thresholds

To ensure you are never monitoring stale or outdated information if the background task fails or hangs, the plugin checks the last write timestamp of the cache file:

| Cache Age | Check State | Meaning |
| --- | --- | --- |
| `< 20 minutes` | **OK** | Background task is updating the cache normally. |
| `> 20 minutes (1200s)` | **WARNING** | Data is stale. The background task may have been delayed or failed its last run. |
| `> 30 minutes (1800s)` | **CRITICAL** | Data is severely outdated. The background task is likely hung, terminated, disabled, or encountering persistent errors. |

The check output displays:
* **`Last Update Check`:** Time offset in seconds since the cache file was last written.
* **`Last Fetch Timestamp`:** The exact UTC timestamp when the cache was written (e.g. `2026-09-21 11:30:00 UTC`).

---

## Enabling Windows Update Offload

To enable the feature, open an administrative PowerShell prompt and run:

```powershell
Enable-IcingaWindowsUpdateOffload;
```

```text
[Notice]: The task "Fetch Windows Updates" has been successfully registered at location "\Icinga\Icinga for Windows\".
```

This will:
* Set `Framework.WindowsUpdateOffload` to `$TRUE` in your framework configuration.
* Register the scheduled task `Fetch Windows Updates` under `\Icinga\Icinga for Windows\`.
* Automatically trigger the initial run of the task so that the cache file is created immediately.

### Silent Mode

If you are automating the setup, you can suppress console output by passing the `-Silent` switch:

```powershell
Enable-IcingaWindowsUpdateOffload -Silent;
```

---

## Checking Offload Status

You can verify whether the feature is currently active with `Get-IcingaWindowsUpdateOffload`:

```powershell
Get-IcingaWindowsUpdateOffload;
```

```text
True
```

You can also check the state of the scheduled task using the standard PowerShell cmdlet:

```powershell
Get-ScheduledTask -TaskName 'Fetch Windows Updates' -TaskPath '\Icinga\Icinga for Windows\';
```

```text
TaskPath TaskName State
-------- -------- -----
\Icinga\Icinga for Windows\ Fetch Windows Updates Running
```

---

## Disabling Windows Update Offload

To disable the offload feature, open an administrative PowerShell prompt and run:

```powershell
Disable-IcingaWindowsUpdateOffload;
```

```text
[Notice]: The "Fetch Windows Updates" task was removed from the system.
```

This will:
* Set `Framework.WindowsUpdateOffload` to `$FALSE`.
* Stop and unregister the `Fetch Windows Updates` scheduled task.
* Remove the `pending.xml` cache file to prevent stale data from being kept on disk.
* Revert `Invoke-IcingaCheckUpdates` to querying updates directly.

---

## Check Plugin Example Output

When `Invoke-IcingaCheckUpdates` runs with Windows Update Offload enabled:

```powershell
Invoke-IcingaCheckUpdates;
```

```text
[OK] Windows Updates: 8 Ok (All must be [OK])
\_ [INFO] Last Fetch Timestamp: 2026-09-21 11:54:18 UTC
\_ [OK] Last Update Check: 1m
\_ [INFO] Microsoft Defender (All must be [OK])
\_ [INFO] Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.459.318.0) - Current Channel (Broad) [9/21/2026 12:00:00 AM]: Nothing
\_ [INFO] Update Count: 1c
\_ [INFO] Other (All must be [OK])
\_ [INFO] Update Count: 0c
\_ [INFO] Reboot Pending: No
\_ [INFO] Security Updates (All must be [OK])
\_ [INFO] 2026-09 Security Update (KB5129195) (26200.9457) [9/14/2026 12:00:00 AM]: Nothing
\_ [INFO] Update Count: 1c
\_ [INFO] Total Pending Updates: 2c
\_ [INFO] Update Rollups (All must be [OK])
\_ [INFO] Update Count: 0c
```

If the background task stops running and the cache exceeds the threshold:

```text
[WARNING] Windows Updates: 1 Warning 7 Ok [WARNING] Last Update Check (All must be [OK])
\_ [INFO] Last Fetch Timestamp: 2026-09-21 11:59:18 UTC
\_ [WARNING] Last Update Check: Value 26.37m is greater than threshold 20m
\_ [INFO] Microsoft Defender (All must be [OK])
\_ [INFO] Security Intelligence Update for Microsoft Defender Antivirus - KB2267602 (Version 1.459.318.0) - Current Channel (Broad) [9/21/2026 12:00:00 AM]: Nothing
\_ [INFO] Update Count: 1c
\_ [INFO] Other (All must be [OK])
\_ [INFO] Update Count: 0c
\_ [INFO] Reboot Pending: Yes
\_ [INFO] Security Updates (All must be [OK])
\_ [INFO] 2026-09 Security Update (KB5129195) (26200.9457) [9/14/2026 12:00:00 AM]: Nothing
\_ [INFO] Update Count: 1c
\_ [INFO] Total Pending Updates: 2c
\_ [INFO] Update Rollups (All must be [OK])
\_ [INFO] Update Count: 0c
```

This immediately signals that the background process requires attention.
10 changes: 8 additions & 2 deletions doc/knowledgebase/IWKB000006.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ The Windows COM Object is rejecting every access to these information over remot

## Solution

Right now there is no solution available for this problem. Microsoft is not allowing to grant permission to these objects over remote connections, which makes it impossible to use them. In addition there is no proper alternative for fetching pending Windows Updates and Hotfixes.
To resolve this issue without running the entire Icinga Agent service as `NT AUTHORITY\SYSTEM`, you can use the [Windows Update Offload](https://icinga.com/docs/icinga-for-windows/latest/doc/160-Features/10-Windows-Update-Offload) feature.

A possible fix which is suggested online is to add a scheduled task, running the command after being triggered by our execution and afterwards fetching the result from the task. This solution requires more research, testing and development.
By enabling Windows Update Offload, a dedicated background scheduled task running as `SYSTEM` fetches pending Windows updates periodically and caches the results securely. The check plugin then reads from this cache without requiring elevated permissions or direct COM object access during check execution:

```powershell
Enable-IcingaWindowsUpdateOffload;
```

An alternative solution is to use [JEA](https://icinga.com/docs/icinga-for-windows/latest/doc/130-JEA/01-JEA-Profiles/)
5 changes: 5 additions & 0 deletions icinga-powershell-framework.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,11 @@ function Use-Icinga()
Enable-IcingaFrameworkDebugMode;
}

# Enable Windows Update Offload in case it is enabled in our config
if (Get-IcingaWindowsUpdateOffload) {
Enable-IcingaWindowsUpdateOffload -Silent;
}

$EventLogMessages = Invoke-IcingaNamespaceCmdlets -Command 'Register-IcingaEventLogMessages*';
foreach ($entry in $EventLogMessages.Values) {
foreach ($event in $entry.Keys) {
Expand Down
38 changes: 38 additions & 0 deletions jobs/FetchWindowsUpdates.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
Use-Icinga;

$UpdateDir = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates';
$UpdateFile = Join-Path -Path $UpdateDir -ChildPath 'pending.xml';
$UpdateTmpFile = Join-Path -Path $UpdateDir -ChildPath 'pending.xml.tmp';

# In case the file does not yet exist, create it once and ensure we update the permissions that
# noone besides the SYSTEM and Icinga for Windows user can access them
if (-not (Test-Path -Path $UpdateDir)) {
New-Item -Path $UpdateDir -ItemType Directory | Out-Null;
Set-IcingaUserPermissions;
}

while ($TRUE) {
try {
#$WindowsUpdates = Get-IcingaWindowsUpdatePendingList -AsTask;
# Fetch all informations about installed updates and add them
$Updates = Get-IcingaWindowsUpdateRaw;
$XMLObj = [System.Management.Automation.PSSerializer]::Serialize($Updates, 3);

# First write the new update data to a tmp file to avoid race conditions
Write-IcingaFileSecure -File $UpdateTmpFile -Value $XMLObj;

# Now simply move the new tmp file to the target file - keep doing this until the file does not exist anymore
# This atomic operation ensures that we do not have a corrupt file on disk
while ((Test-Path -Path $UpdateTmpFile)) {
Move-Item -Path $UpdateTmpFile -Destination $UpdateFile -Force -ErrorAction SilentlyContinue;
Start-Sleep -Seconds 1;
}
} catch {
Write-IcingaEventMessage -EventId 1200 -Namespace 'Framework' -Objects $UpdateFile, $XMLObj, $_.Exception.Message;
} finally {
$Updates = $null;
$XMLObj = $null;
# Fetch Windows Updates every 10 minutes (600 seconds)
Start-Sleep -Seconds 600;
}
}
1 change: 1 addition & 0 deletions lib/core/framework/New-IcingaEnvironmentVariable.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ function New-IcingaEnvironmentVariable()

$Global:Icinga.Protected.Add('DeveloperMode', $FALSE);
$Global:Icinga.Protected.Add('DebugMode', $FALSE);
$Global:Icinga.Protected.Add('WindowsUpdateOffload', $FALSE);
$Global:Icinga.Protected.Add('JEAContext', $FALSE);
$Global:Icinga.Protected.Add('RunAsDaemon', $FALSE);
$Global:Icinga.Protected.Add('Minimal', $FALSE);
Expand Down
6 changes: 6 additions & 0 deletions lib/core/logging/Icinga_EventLog_Enums.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ if ($null -eq $IcingaEventLogEnums -Or $IcingaEventLogEnums.ContainsKey('Framewo
'Details' = 'Icinga for Windows could not read the specified cache file, as the content seems to be corrupt. This happens mostly in case of unexpected shutdowns or terminations during the write process.';
'EventId' = 1104;
};
1200 = @{
'EntryType' = 'Error';
'Message' = 'Unable to fetch Windows Updates from background task';
'Details' = 'Icinga for Windows failed to fetch the pending Windows updates by using the scheduled background task due to an error.';
'EventId' = 1200;
};
1400 = @{
'EntryType' = 'Error';
'Message' = 'Icinga for Windows background daemon not found';
Expand Down
39 changes: 39 additions & 0 deletions lib/provider/updates/Disable-IcingaWindowsUpdateOffload.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<#
.SYNOPSIS
Disables the Windows Update Offload feature.
.DESCRIPTION
Disables the Windows Update Offload feature. The background scheduled task
('Fetch Windows Updates') is stopped and unregistered, the internal configuration
is set to FALSE, and the cached XML file is safely removed.

Subsequent check executions will query the Windows Update COM object directly,
which requires appropriate permissions.

This command requires administrative privileges.
.FUNCTIONALITY
Disables Windows Update Offload and cleans up tasks and cache files.
.EXAMPLE
PS>Disable-IcingaWindowsUpdateOffload;
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>

function Disable-IcingaWindowsUpdateOffload()
{
# Disable scheduled tasks and clear internal config values
$Global:Icinga.Protected.WindowsUpdateOffload = $FALSE;

# Only run this if we use an administrative shell
if (-not (Test-AdministrativeShell)) {
Write-IcingaConsoleError 'You require administrative privileges to run this command';
return;
}

Set-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload' -Value $FALSE;

Unregister-IcingaWindowsScheduledTaskWindowsUpdates;

# Remove the XML file containing the update information to not store old data
$UpdateFile = Join-Path -Path (Get-IcingaCacheDir) -ChildPath 'provider\windows_updates\pending.xml';
Remove-ItemSecure -Path $UpdateFile -Retries 5 -Force | Out-Null;
}
47 changes: 47 additions & 0 deletions lib/provider/updates/Enable-IcingaWindowsUpdateOffload.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<#
.SYNOPSIS
Enables the Windows Update Offload feature.
.DESCRIPTION
Enables the Windows Update Offload feature. When enabled, a Windows Scheduled Task
('Fetch Windows Updates') running as SYSTEM will fetch pending Windows updates in
the background every 10 minutes and save them securely to the cache directory.

Check plugins will read the update state from the cache file instead of querying
the Windows Update COM-Object live. This allows running the Icinga Agent service
as a non-SYSTEM user without requiring JEA.

This command requires administrative privileges.
.FUNCTIONALITY
Enables Windows Update Offload and registers the background task.
.PARAMETER Silent
Suppresses console error and notice messages.
.EXAMPLE
PS>Enable-IcingaWindowsUpdateOffload;
.EXAMPLE
PS>Enable-IcingaWindowsUpdateOffload -Silent;
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>

function Enable-IcingaWindowsUpdateOffload()
{
param (
[switch]$Silent = $false
);

# Register the scheduled task and set internal config values
$Global:Icinga.Protected.WindowsUpdateOffload = $TRUE;

# Only run this if we use an administrative shell
if (-not (Test-AdministrativeShell)) {
if (-not $Silent) {
Write-IcingaConsoleError 'You require administrative privileges to run this command';
}

return;
}

Set-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload' -Value $TRUE;

Register-IcingaWindowsScheduledTaskWindowsUpdates -Silent:$Silent;
}
26 changes: 26 additions & 0 deletions lib/provider/updates/Get-IcingaWindowsUpdateOffload.psm1
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<#
.SYNOPSIS
Returns the current configuration status of the Windows Update Offload feature.
.DESCRIPTION
Checks if the Windows Update Offload feature is currently enabled in the
Icinga PowerShell configuration under 'Framework.WindowsUpdateOffload'.
.FUNCTIONALITY
Retrieves the Windows Update Offload feature state.
.OUTPUTS
System.Boolean
.EXAMPLE
PS>Get-IcingaWindowsUpdateOffload;
.LINK
https://github.com/Icinga/icinga-powershell-framework
#>

function Get-IcingaWindowsUpdateOffload()
{
$UpdateOffload = Get-IcingaPowerShellConfig -Path 'Framework.WindowsUpdateOffload';

if ($null -eq $UpdateOffload) {
return $FALSE;
}

return $UpdateOffload;
}
Loading