Tuesday, 7 November 2023

PrintNighmare-safe Printer Management

Hello Folks,

Hopefully this post will help many out there who may struggle to emulate a Point-and-Print solution but without compromising the security of the corporate network.

For those new to the topic, Microsoft tightened the security of the printing subsystem as a result of a number of critical vulnerabilities, commonly referred to as PrintNighmare - details here. The fix consists in limiting driver installation to administrators only. To clarify, before the PrintNightmare security measures, end users were allowed to install printer drivers.

Given that lots of administrators have implemented Point-and-Print, this security measure broke printing for lots of organizations. Microsoft provided a workaround, which effectively cancels the security measures, by means of a Registry setting. It is well documented in various places such as this. Basically by implementing the Registry hack, you'd revert back your printing subsystem to its pre-patch, vulnerable state  by allowing end users to install printer drivers.

The core of the problem is to get the drivers onto the client computers without user interaction.

This post is meant to address the following issues:

  1. Install printer drivers on users' computers.
  2. Eliminate security prompts and user interaction.
  3. Automate the process and centralize administration.
  4. Maintain security (no need to undo Microsoft's security measures via the Registry hack).
  5. Use only built-in tools pre-canned with the operating system.

CAVEAT: I do not claim by any means that it is a perfect solutions. There is plenty of room for improvement. Likely you'll have to adapt it to your needs in case you run a large, multi-department and/or geographically distributed environment. Make it suit your needs.

In a nutshell the process is as follows:

  1. Set up the print server: install drivers, create ports, install, configure and share printers (ensure they are published in Active Directory).
  2. Create a new GPO or nominate an existing one to distribute printers to users. Create shared printer definitions in User Configuration / Preferences / Control Panel Settings / Printers. Link it to the OU holding your users.
  3. Use item-level targeting to define who gets what printer. Also make sure that the process runs in the logged-on user's security context.
  4. On the print server, export device drivers to a shared folder that client computers can access.
  5. Create a new GPO or nominate an existing one to distribute a computer startup script. Link it to the OU holding your client computer objects.
  6. Reboot the client computers to make them run the computer startup script. The script imports the drivers that have been exported at point 4 above. It runs in the System security context, administrator by definition, therefore no security prompt is displayed.
  7. Users will be connected to their printers as defined in the GPO at point 2 above.
At points 2 and 5, depending on your environment, you may use a single, dedicated GPO linked to both user and computer OUs (or to the domain root to that matter if such is your environment - I recommend against it though).

At the core of the solution there are two PowerShell scripts:
  1. A server-side script running on the print server that exports drivers.
  2. A client-side script running on client computers that imports pre-filtered drivers of shared printers.
We will not go into configuring GPOs and shared printer setup as it is well documented on the Internet. The 7 steps above should provide sufficient information to get you started.

Following is the two-step process to set up the scripts.

I. Setting up the print server

The following is to be done on the print server:
  1. Create a folder to store the exported drivers, e.g. C:\SharedPrinterDrivers.
  2. Share the folder, e.g. Drivers$ (I like to create hidden shares for such things).
  3. Grant the Domain Computers security group Read permission at share and NTFS level.
  4. Create the driver export script and save it to folder, e.g. C:\Scripts\ExportSharedPrinterDrivers.ps1
  5. Create a scheduled task to run the driver export script on a regular basis.
My suggestion is to run the script daily, early in the morning, before users power on their computers,. This way drivers updated overnight by Windows Updates (or equivalent server maintenance tools) would be captured in the export and distributed to users when they log on.


The driver export script:
# The FQDN of your print server:
$hostName = 'SERVER.contoso.local'

# Drivers will be exported here:
$driversRootFolder = 'C:\SharedPrinterDrivers'

# UNC path to the driver root share:
$driversShare = '\\' + $hostName + '\Drivers$'

# This file lists the driver names of the shared printers only. It is used by the client-side computer startup script for the installation of the missing printer drivers:
$infFilePath = $driversRootFolder + "\" + "inffiles.txt"

# Export drivers to $driversRootFolder. We use DISM because Export-WindowsDriver proved to be unreliable.
# Unfortunately Microsoft does not allow driver export filtering by device class, therefore ALL drivers will be exported.
Invoke-Command -ArgumentList $driversRootFolder -ScriptBlock { dism /online /export-driver /destination:$args /loglevel:1 /quiet 2>&1>$null }

# Delete the driver name file if it exists as we will generate a new one in case some drivers have been updated:
if (Test-Path -Path $infFilePath) { Remove-Item -Path $infFilePath -Force }

# We store shared printer driver names in an array:
$arrInfFiles = @()

# We are only interested in shared printer driver names:
(Get-Printer | ?{$_.Shared}).DriverName | ForEach-Object {
  $drvName = $_
# We are looking for .INF files containing the names of the shared printer drivers in the llist of exported drivers.
# We also replace the local path reference (e.g. C:\DriversRoot) with the UNC path reference (e.g. \\Server\\Share):
  $infFiles = (Get-ChildItem -Path $driversRootFolder -Recurse | Select-String -Pattern $drvName -SimpleMatch).Path.Replace($driversRootFolder,$driversShare)
# We record the full path to the INF file in our array:
  ForEach ($infFile in $infFiles) {
    $arrInfFiles += "$infFile,$drvName"
  }
}

# The list of INF files are written to the $infFilePath file:
$arrInfFiles | sort -Unique  > $infFilePath

One way to improve this bit is to export relevant drivers only. Unfortunately Microsoft does not appear to provide a ready-to-user method to filter drivers in DISM or Export-WindowsDrivers.

The script uses DISM because I found that the Export-WindowsDrivers cmdlet is unreliable and it often fails with the following error:

 


II. Setting up the computer startup script

Create a PowerShell computer startup script in a GPO's Computer Configuration startup script policy (see step 5 above in the 7-step process-in-a-nutshell list). Make sure the "PowerShell Script" tab is selected. In my environment I called the file Install-PrinterDrivers.ps1:



The script:

Get-Content \\SERVER.contoso.local\Drivers$\inffiles.txt | ForEach-Object {
  $line = $_.Split(",")
  Invoke-Command -ArgumentList $line[0] -ScriptBlock { pnputil.exe -a $args[0] 2>&1>$null }
  Add-PrinterDriver -Name $line[1]
}

That's it, you should be all set.

Happy printing :-)

No comments:

Post a Comment