In this article, I will set up WSUS with automated patch approvals based on groups.

The patches will be assigned to different groups to adhere to technical drivers, functional drivers, and generalization:

  • Technical Driver => Ensure that the patches do not « break » the computers
  • Functional Driver => Ensure that the applications continue to function
  • Generalization => Deployment across the entire fleet.

WSUS Installation

Role Installation

  • Add the Windows Server Update Services Role.
  • Leave the default IIS installation choices.
  • We will use the database integrated into Windows Server; leave the default options.
  • Regarding the storage location:
    • If you just want to validate the patches to install, don’t check anything.
    • If you want to validate and host the patches to save bandwidth, specify a path.

In this tutorial I will choose not to store the patches on my WSUS server. In this case, WSUS will be used to validate the patches on my devices, but the clients will download the patches directly from the Microsoft servers.

  • Click on RUN to initialize WSUS

Initialization

  • Synchronize WSUS with the Internet.
  • The first connection will be necessary to get the updated list of products.
  • You can then choose the products (Here Windows 10 and Windows 11).
  • Also choose the criticality of the patches to install.
  • Specify the time for automatic synchronization.
  • Do not initiate the first synchronization (we need to create other rules first).

Additional Components

Also install to display reports in the WSUS console: 

Change to https (8531)

The use of HTTPS on port 8531 has become mandatory for Windows 10 and Windows 11.

Creation of the certificate template (on the PKI).

  • Your PKI should allow your server to request a certificate.
  • You can duplicate the « Web Server » template on your root certificate server.
  • Give it a name.
  • Grant Enroll permissions for your WSUS server.

Provide enrollment permissions to your WSUS server.

  • Open the IIS console.
  • Click on your server and select « Server Certificates ».
  • Click on « Create Domain Certificate… ».
  • Enter the information.
  • Choose the certification authority (CA) of your domain and the friendly name.

Association du certificat

  • In the IIS console, click on « WSUS Administration » and then click on « Edit Bindings ».
  • Edit port 8531 and select the certificate.
  • Edit port 8531 and select the certificate.
  • Force SSL encryption for virtual applications :
    • ApiRemoting30
    • ClientWebService
    • DSSAuthWebService
    • ServerSyncWebService
    • SimpleAuthWebService

On WSUS

  • Launch a Command Prompt (as an administrator).
  • Enter the command:
cd "c:\Program Files\Update Services\Tools"
WsusUtil.exe configuressl wsus.leblogosd.lan
  • Restart the server

Automatic Approval Configuration

Automatic Patch Approval can be set up for computer groups. The assignment to the first group will be done through WSUS, while for the assignment to other groups, a PowerShell script will be used.

Assignment for the first group using WSUS

  • We will create groups for the computers. These groups will later allow us to approve different patches for these groups.

Manually create 2 or 3 groups. The computers will be assigned to these groups via GPO (or via registry key for computers in a workgroup).

  • In the options, we will create an automatic approval rule.
  • All patches will be automatically approved for a group (Here, L1Patch).

All patches will be installed as soon as they are available on the computers assigned to this group.

  • You can then initiate the synchronization with our approval rule.

Assignment to other groups through PowerShell

Script to be placed in a scheduled task for automatic patch approval

$logFolder = "c:\exploit\logs"
$maxLogs = 60

$SyncApprovals = @(

    @{"Source" = "TESTERS"  ; "Target" = "PILOT"  ; "MinDays" = 5},
    @{"Source" = "PILOT"  ; "Target" = "PROD"    ; "MinDays" = 5}
)

# ----------------- Log Files ----------------------
If (!(Test-Path $logFolder)) { New-Item -Path "$logFolder" -ItemType Directory }
$logFile = $logFolder + "\WSUS-ManageApprovals" + (Get-Date -format "yyyyMMdd-HHmmss").ToString() + ".log"
Start-Transcript -Path $logFile -Force | Out-Null

# ----------------- Connect WSUS server ------------
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
$wsusServer       = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
$subscription     = $wsusServer.GetSubscription()
$wsusServerConfig = $wsusServer.GetConfiguration()
$targetGroups     = $wsusServer.GetComputerTargetGroups()
$updates          = $wsusServer.GetUpdates()

# ----------------- Synchronize --------------------
$subscription.StartSynchronization()

# ----------------- Manage KB ----------------------
$workingUpdates = $updates | Where { -Not $_.IsDeclined}
Foreach ($update in $workingUpdates){
  If ($update.Title -Match "Windows Malicious Software Removal Tool") { $update.Decline()                } # Decline "Windows Malicious Software Removal Tool" Update automatically
  If ($update.RequiresLicenseAgreementAcceptance)                     { $update.AcceptLicenseAgreement() } # Accept license agreement if required
}

# ----------------- Approve KB ------------------
write-host "********** Approve KB **********"
$workingUpdates = $updates | Where { -Not $_.IsDeclined}
Foreach ($update in $workingUpdates){
  $approvals = $update.GetUpdateApprovals()
  Foreach ($syncApproval in $SyncApprovals){
    $sourceGroup    = $targetGroups | Where { $_.Name                  -eq $syncApproval.Source }
    $sourceApproval = $approvals    | Where { $_.ComputerTargetGroupId -eq $sourceGroup.ID      }

        If ($($sourceApproval.Action) -eq "Install"){
      $LastChangeKB = (New-TimeSpan -start $sourceApproval.GoLiveTime -End (Get-Date)).Days
        If($LastChangeKB -ge $syncApproval.MinDays) {

            $targetGroup    = $targetGroups | Where { $_.Name                  -eq $syncApproval.Target }
            $targetApproval = $approvals    | Where { $_.ComputerTargetGroupId -eq $targetGroup.ID }
          If ($($targetApproval.Action) -ne "Install") {
                    Write-host "Enable : $($syncApproval.Target) => $($update.Title) - $LastChangeKB days"
            $update.Approve("Install", $targetGroup )  | Out-Null
          }
        }
        }
  }
}
write-host ""


# ------------ Disabled KB
write-host "********** Disable Superseded **********"
$workingUpdates = $updates | Where { -Not $_.IsDeclined} | Where { $_.IsSuperseded}
Foreach ($update in $workingUpdates){
  write-host "$($update.Title)"
  Foreach ($Supersede in $update.GetRelatedUpdates([Microsoft.UpdateServices.Administration.UpdateRelationship]::UpdatesThatSupersedeThisUpdate)){
    write-host "----- Replace By : $($Supersede.Title)"
    $approvals = $Supersede.GetUpdateApprovals()

    Foreach ($approval in $approvals) {
      Foreach ($targetGroup in $targetGroups){
        If (( $targetGroup.Id -eq $approval.ComputerTargetGroupId ) -and ($($approval.Action) -eq "Install")) {
          write-host "---------- $($targetGroup.Name)"
          if ($($targetGroup.Name) -like "*GLOBAL*ODD*") {
            Write-host "---------- Decline : $($update.Title)"
            $update.Decline()

          }
        }
      }
    }
  }

}
write-host ""


# ----------------- Cleanup Server ----------------
write-host "********** Cleanup WSUS **********"
$cleanupInterface = $wsusServer.GetCleanupManager();
$cleanupScope = new-object 'Microsoft.UpdateServices.Administration.CleanupScope';
$cleanupScope.DeclineSupersededUpdates = $True;
$cleanupScope.DeclineExpiredUpdates = $True;
$cleanupScope.CleanupObsoleteComputers = $True;
$cleanupScope.CleanupObsoleteUpdates = $True;
$cleanupScope.CompressUpdates = $True;
$cleanupScope.CleanupUnneededContentFiles = $True;
$cleanupInterface.PerformCleanup($cleanupScope)
write-host ""

Memory configuration

I have experienced WSUS crashes related to memory configuration. Here’s an example of a configuration to prevent service crashes.

  • In the IIS settings, under the Advanced options of the WsuPool.
  • Configure the memory => 0 for unlimited.

Computer Configuration

On WSUS

  • Configure automatic assignment of computers to groups

Without this option, computers will not be assigned to our groups despite assignment by registry key or GPO.

By GPO

The first thing to do is to update the ADMX on your Active Directory to have the new GPOs for WSUS.

The Legacy Policies folder should no longer be used as it represents the old generation of GPOs.

  • Automatic Installation

This simply involves setting the installation time. Option 4 is the most important in this configuration to force the installation.

  • Automatic reboot

This GPO is more suitable for « Industrial » Computers with patch installation scheduled at night.

  • WSUS Servers

The WSUS server name will be specified.

  • WSUS Targeting

This option is very important as it will determine whether the computer is a pilot machine (installing patches before everyone else) or a production machine.

A manual action or script is often put in place to move a machine from the TEST group to the PRODUCTION group after xxx days.

Via Registry Key

A tool is available to configure the WSUS client registry keys in graphical mode. It can be found at: https://github.com/blndev/wsusworkgroup

This tool is capable of displaying the configuration coming from a GPO.

WSUS Server Migration

Here is a script for exporting the « Computer Group » patch assignments. This script can be used to migrate from an old WSUS server to a new WSUS server.

Exploitation

SQL Maintenance

On the Microsoft website, you can find a SQL maintenance script for the WSUS database: : Reindex the Windows Server Update Services (WSUS) database

USE SUSDB;
GO
SET NOCOUNT ON;

-- Rebuild or reorganize indexes based on their fragmentation levels
DECLARE @work_to_do TABLE (
    objectid int
    , indexid int
    , pagedensity float
    , fragmentation float
    , numrows int
)

DECLARE @objectid int;
DECLARE @indexid int;
DECLARE @schemaname nvarchar(130);
DECLARE @objectname nvarchar(130);
DECLARE @indexname nvarchar(130);
DECLARE @numrows int
DECLARE @density float;
DECLARE @fragmentation float;
DECLARE @command nvarchar(4000);
DECLARE @fillfactorset bit
DECLARE @numpages int

-- Select indexes that need to be defragmented based on the following
-- * Page density is low
-- * External fragmentation is high in relation to index size
PRINT 'Estimating fragmentation: Begin. ' + convert(nvarchar, getdate(), 121)
INSERT @work_to_do
SELECT
    f.object_id
    , index_id
    , avg_page_space_used_in_percent
    , avg_fragmentation_in_percent
    , record_count
FROM
    sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'SAMPLED') AS f
WHERE
    (f.avg_page_space_used_in_percent < 85.0 and f.avg_page_space_used_in_percent/100.0 * page_count < page_count - 1)
    or (f.page_count > 50 and f.avg_fragmentation_in_percent > 15.0)
    or (f.page_count > 10 and f.avg_fragmentation_in_percent > 80.0)

PRINT 'Number of indexes to rebuild: ' + cast(@@ROWCOUNT as nvarchar(20))

PRINT 'Estimating fragmentation: End. ' + convert(nvarchar, getdate(), 121)

SELECT @numpages = sum(ps.used_page_count)
FROM
    @work_to_do AS fi
    INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id
    INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id

-- Declare the cursor for the list of indexes to be processed.
DECLARE curIndexes CURSOR FOR SELECT * FROM @work_to_do

-- Open the cursor.
OPEN curIndexes

-- Loop through the indexes
WHILE (1=1)
BEGIN
    FETCH NEXT FROM curIndexes
    INTO @objectid, @indexid, @density, @fragmentation, @numrows;
    IF @@FETCH_STATUS < 0 BREAK;

    SELECT
        @objectname = QUOTENAME(o.name)
        , @schemaname = QUOTENAME(s.name)
    FROM
        sys.objects AS o
        INNER JOIN sys.schemas as s ON s.schema_id = o.schema_id
    WHERE
        o.object_id = @objectid;

    SELECT
        @indexname = QUOTENAME(name)
        , @fillfactorset = CASE fill_factor WHEN 0 THEN 0 ELSE 1 END
    FROM
        sys.indexes
    WHERE
        object_id = @objectid AND index_id = @indexid;

    IF ((@density BETWEEN 75.0 AND 85.0) AND @fillfactorset = 1) OR (@fragmentation < 30.0)
        SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
    ELSE IF @numrows >= 5000 AND @fillfactorset = 0
        SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90)';
    ELSE
        SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD';
    PRINT convert(nvarchar, getdate(), 121) + N' Executing: ' + @command;
    EXEC (@command);
    PRINT convert(nvarchar, getdate(), 121) + N' Done.';
END

-- Close and deallocate the cursor.
CLOSE curIndexes;
DEALLOCATE curIndexes;


IF EXISTS (SELECT * FROM @work_to_do)
BEGIN
    PRINT 'Estimated number of pages in fragmented indexes: ' + cast(@numpages as nvarchar(20))
    SELECT @numpages = @numpages - sum(ps.used_page_count)
    FROM
        @work_to_do AS fi
        INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id
        INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id

    PRINT 'Estimated number of pages freed: ' + cast(@numpages as nvarchar(20))
END
GO


--Update all statistics
PRINT 'Updating all statistics.' + convert(nvarchar, getdate(), 121)
EXEC sp_updatestats
PRINT 'Done updating statistics.' + convert(nvarchar, getdate(), 121)
GO


0 commentaire

Laisser un commentaire

Emplacement de l’avatar

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.