Non-Persistent Virtual Device Infrastructure (NP-VDI)
NP-VDI enables your IT team to rapidly provision virtual desktops alongside physical devices, all from a single unified platform. The built-in automated management saves time, reduces manual overhead, and gains comprehensive endpoint control in cloud environments.
Feature Workflow
Agent Deployment on Base Image:
-
The agent is installed (not registered) on a clean VM.
-
The agent is then manually registered as a “Base Image” using the provided command-line tooling.
Sealing the base image:
-
Once the base image agent has registered and the configuration is validated, the VM is “sealed” via the command line.
Spawning Instances:
-
New NP-VDI instances can be spun up from the base image, typically generating a new MAC address for each session.
-
Ivanti Neurons detects and tracks these as unique, short-term endpoints managed under the base image.
Policy Management:
-
Policy changes can be made at the base image level. All spawned instances inherit the latest applied policy upon the next check-in.
Step 1: Prepare the Base Image
To prepare the base image, follow these steps:
- Navigate to Agent > Agent Deployment > Manual installation.
- On the Manual installation tab, choose an agent policy and enrollment key.
- Select Windows. Then, choose an Installation Mode and click Download.
The agent installer will be downloaded.This action downloads two files. Save the installer with file name "IvantiCloudAgent.exe.options". This file will be used for installation.
- Install the downloaded NP-VDI agent.
To install the agent:- Rename the associated IvantiCloudAgent.exe.options file before installation.
- Run the installer. When the Activation key or Cookie option appears and click Cancel.
This installs the agent on the endpoint without registering it to the tenant.
- Register the agent as a base image. To register as a base image, run the following command, as an administrator, from the agent install location:
STAgentCtl.exe register --asBaseImage <Base Image Name> --instanceIdType MacAddress --enrollmentkey "%enrollmentkey%" --baseurl https://agentreg.ivanticloud.com
In the above command:
<Base Image Name> is the name of the base image device.
%enrollmentkey% is a combination of tenantid_activationkey.
instanceIdType can be "MacAddress", "HostName" or “Custom”. This is used as the unique flag to identify the instance.
If "Custom" is used, a tagList option containing comma-separated list of custom tags can be specified to identify that agent. For more information about custom tags, see the Custom Tag List and Update Tags section.
Both the tenantid and activationkey can be found in the associated .options file, downloaded with the agent as shown in the example below:
tenantid = 11111111-1111-1111-1111-4gc02d5b0f0d
activationkey = 9aI7SOQNybweiM2QKI7kcqIG8ufUFU21AvZwJwAEx6C-SAO5UGK62YkEgm89kvC2m-xwLesZA4t1_TBtEjIQMVaBU0KsVe-V60ZLBwWu83YREcUp3diAFvHjBvLtkxT3
cloudhost=https:agentreg.ivanticlouddev.comThe enrollment key for the above code is:
%enrollmentkey% = 11111111-1111-1111-1111-4gc02d5b0f0d_9aI7SOQNybweiM2QKI7kcqIG8ufUFU21AvZwJwAEx6C-SAO5UGK62YkEgm89kvC2m-xwLesZA4t1_TBtEjIQMVaBU0KsVe-V60ZLBwWu83YREcUp3diAFvHjBvLtkxT3The command line to register a device with the example tenant for a base image named “Win11” will be as follows:
STAgentCtl.exe register --asBaseImage Win11 --instanceIdType MacAddress --enrollmentkey “11111111-1111-1111-1111-4gc02d5b0f0d_9aI7SOQNybweiM2QKI7kcqIG8ufUFU21AvZwJwAEx6C-SAO5UGK62YkEgm89kvC2m-xwLesZA4t1_TBtEjIQMVaBU0KsVe-V60ZLBwWu83YREcUp3diAFvHjBvLtkxT3" --baseurl https://agentreg.ivanticloud.com
- To update the base image:
- Download the PowerShell script from Ivanti community portal: Ivanti-Neurons -Non-Persistent-VDI - Agent Script.
Before running the PowerShell script, check that the admin has the relevant permissions to run this on the base image, as the execution policy on the endpoint may block this:
To check, use "Get-ExecutionPolicy" command.
To change, use "Set-ExecutionPolicy RemoteSigned" command.
- If the script runs without any parameters, the script will attempt the following:
- Unseal the agent.
- Run updates for both agents and engines.
- Seal the agent again.
- If the agent is already unsealed, and this should remain unsealed after the update. Pass the "DoNotSeal" parameter to perform all the same steps, except sealing the image.
- The default timeout for updates is 300 seconds. You can change the time by passing the “-TimeoutSec” argument.
For additional reference, copy of the script can be accessed under Appendix section.
- To prepare the base image:
- Run "STAgentCtl.exe prepareImage" command to make a sealed, non-persistent base image ready for NP-VDI.
This command is mandatory and stops all services and deletes any data related to actual Agent ID of the base image. When the non-persistent instance boots up, the services are restarted.The Agent ID will change with every non-persistent instance spun up from the base image.
- Use the following commands to manually seal the base image (outside of the upgrade script):
- To manually seal the base image, enter "STAgentCtl.exe seal".
Use the "unseal" command to unseal the image. This remains active until a system restart or the "Seal" command is used.
- To manually seal the base image, enter "STAgentCtl.exe seal".
- Run "STAgentCtl.exe prepareImage" command to make a sealed, non-persistent base image ready for NP-VDI.
- (Optional) Prepare the image for use with "sysprep".
If the non-persistent desktop preparation includes a sysprep stage, it will remove the private key of the agent certificate from the certificate store. To address this issue, follow these steps:The admin must supply their own certificate export password; otherwise, this process will fail.
- Locate the certificate serial number using the certutil -store “Ivanti Cloud Agent Agent” command.
This lists the certificate in the agent certificate store. - After running the command “stagentctl prepareImage” at Step 7, you can export the agent certificate using the following command:
certutil -exportPFX -privatekey -p "%pfxpass%" “Ivanti Cloud Agent Agent” %serialnumber% "%ProgramFiles%\Ivanti\Ivanti Cloud Agent\agent.pfx" NoChain,NoRoot
In the above command:
%pfxpass% is a password supplied by the admin.
%serialnumber% is the located serial number of the agent certificate.
- Download the SetupComplete.cmd script available from Ivanti’s community website here: Restore private key from exported .pfx file script.
- Edit the SetupComplete.cmd script and update the password to match the password used to export the certificate used in Step 2.
- Store the script at %WINDIR%\Setup\Scripts\SetupComplete.cmd on the gold image.
The script will automatically run once the Windows setup has been completed (which happens the first time the device is booted after sysprep).This will restore the private key from the exported .pfx file, then delete both the .pfx file and the script itself, so that the secret won’t leak into the non-persistent instances.
The certificate with the subject OU “ST Agent” (e.g. "Subject: OU=ST Agent, CN=011472EFB12BABF06122F2903BBE12AC4E32EBFE5082”) should be exported, using the corresponding serial number from that entry.
- Locate the certificate serial number using the certutil -store “Ivanti Cloud Agent Agent” command.
Step 2: Create and Test NP-VDI Instances
To spin a new instance from the base image, follow these steps:
-
Ensure a new MAC address is assigned to each new instance (to ensure the instanceIdType MacAddress option will work).
Alternatively, ensure a different hostname or tag is utilized. -
Start the virtual machine instance and confirm the instance appears in the Neurons UI as a unique NP-VDI endpoint. Go to Agents > Agent Management section of Ivanti Neurons interface to do this.
A new VDI Type column is available which indicates whether the device is a Base Image, Non-Persistent, or Persistent device. -
Each agent endpoint is now linked from the Agent Management page to the Devices page. Click on the Agent Endpoint name to launch a new window showing the device in the Devices view.
-
Click the Linked instances option to show all the instances spun up from this base image.
The Non-Persistent Instances will be tagged as Non-persistent in the Devices view.
Step 3: Reassigning Policy with NP-VDI
To reassign a policy to the base image directly from the Ivanti Neurons interface, follow these steps:
-
Navigate to Agents > Agent Policies.
-
Select the base image and choose Reassign Policy from the Actions menu.
-
Choose the policy to reassign and click Save.
This reassigns the policy. After the policy is reassigned to the base image, all the non-persistent VDI instances will inherit the policy the next time they check-in (which by default is every 200 minutes). -
Validate that new instances inherit the updated policy after check in, either using the Ivanti Neurons Agents or Agent Management interface or from the non-persistent image itself, using the command line "Stagentctl status".
It is not possible to perform reboot actions on a NP-VDI instance. This would reset the device to its initial state, potentially causing the user to lose work they completed during their session.
In addition, Actions available from the Devices view for non-persistent instances do not contain reboot actions.
Additional STAgentCtle.exe Commands
You can use the following additional commands:
-
To open the base agent for updates, enter the "Stagentctl.exe unseal" command.
-
To force the command line to wait until the update has completed before returning control, enter the "Stagentctl.exe update –wait" command.
The "wait" command may encounter problems when the agent is updated, as it requires the agent executables to be running, which leads to the need for a reboot after the update. To avoid this issue, Ivanti offers a PowerShell script that allows for a synchronous update command to be executed.
-
To close the base agent for updates, enter the "Stagentclt.exe seal" command.
Custom Tag List
A Custom Tag List is a user-defined collection of tags that can be assigned to endpoints in Ivanti Neurons, including NP-VDI instances. Custom tags help classify, organize, and identify devices based on business, operational, or technical requirements.
For example, using the tag "Sales-[MacAddress]" generates a unique tag for each device by replacing [MacAddress] with the device's actual MAC address. This functionality is particularly useful for generating unique tags for non-persistent VDI instances.
Use the following syntax to reference device attributes and environment variables:
-
To reference the MAC address, use "[MacAddress]"
-
To reference the hostname, use "[HostName]"
-
To reference an environment variable, use "[env:<name>]"
Where <name> is the case-sensitive name of the environment variable.
Update Tags
Tags assigned during base image registration remain static for all associated instances. If you need to update tags, re-register/seal the base image, or use automation to assign tags based on current criteria after VM deployment.
Appendix
.SYNOPSIS
Updates a non-persistent agent, waiting for the update to complete, then optionally sealing the agent.
.PARAMETER TimeoutSec
Total timeout in seconds.
.PARAMETER UpgradeCode
Upgrade code of the agent being updated.
.PARAMETER DoNotSeal
This parameter specifies not to seal the agent after updating.
#>
param
(
[int] $TimeoutSec = 300,
[guid] $UpgradeCode = "{0C5D7E9C-0424-462D-A570-2A7469EE3739}", # Neurons Agent
[switch] $DoNotSeal
)
function Check-UpdatersComplete
{
$updaters = Get-Process -Name "STAgentUpdater" -ErrorAction SilentlyContinue
return ($updaters.Count -eq 0)
}
function Wait-UpdatersComplete([DateTime] $timeoutDateTime, [int] $checkIntervalSec)
{
while ((Get-Date) -lt $timeoutDateTime)
{
if (Check-UpdatersComplete)
{
return
}
Start-Sleep -Seconds $checkIntervalSec
}
throw "Timeout waiting for all updaters to complete."
}
function Get-AgentVersionFromManifest([string] $manifestPath, [string] $platformKey)
{
if (-not (Test-Path $manifestPath))
{
throw "Could not find manifest at '$manifestPath'."
}
[xml]$manifestXml = Get-Content $manifestPath
$agentFile = $manifestXml.manifest.files.file | Where-Object { $_.key -eq $platformKey }
if (-not ($agentFile))
{
throw "Agent entry with key='$platformKey' not found."
}
if (-not ($agentFile.version))
{
throw "Unable to find agent version in manifest."
}
return [version]::Parse($agentFile.version)
}
function Get-CurrentAgentVersion([guid] $agentUpgradeCode)
{
$agentProductCode = Get-ProductCode -UpgradeCode $agentUpgradeCode
$agentObject = Get-WmiObject -Class Win32_Product | Where-Object { [guid]::Parse($_.IdentifyingNumber) -eq $agentProductCode }
if (-not ($agentObject))
{
throw "Installed agent not found."
}
if (-not ($agentObject.Version))
{
throw "Installed agent version not found."
}
return [version]::Parse($agentObject.Version)
}
function Check-AgentUpToDate([string] $manifestPath, [guid] $agentUpgradeCode, [string] $platformKey)
{
$manifestVersion = Get-AgentVersionFromManifest -ManifestPath $manifestPath -PlatformKey $platformKey
$currentVersion = Get-CurrentAgentVersion -AgentUpgradeCode $agentUpgradeCode
return ($currentVersion -ge $manifestVersion)
}
function Wait-AgentUpdated([DateTime] $timeoutDateTime, [int] $checkIntervalSec, [string] $manifestPath, [guid] $agentUpgradeCode, [string] $platformKey)
{
while ((Get-Date) -lt $timeoutDateTime)
{
if (Check-AgentUpToDate -ManifestPath $manifestPath -AgentUpgradeCode $agentUpgradeCode -PlatformKey $platformKey)
{
return
}
Start-Sleep -Seconds $checkIntervalSec
}
throw "Waiting for agent update timed out."
}
function Update-Agent([DateTime] $timeoutDateTime, [string] $manifestPath, [guid] $agentUpgradeCode, [string] $agentCtlPath, [string] $platformKey)
{
$timeoutSec = [math]::Ceiling(($timeoutDateTime - (Get-Date)).TotalSeconds)
if ($timeoutSec -le 0)
{
throw "Can't run check-in because of small timeout."
}
& $agentCtlPath update --checkin --wait $timeoutSec
if (-not $?)
{
throw "Check-in operation failed."
}
& $agentCtlPath update --all
if (-not $?)
{
throw "Update operation failed."
}
Wait-AgentUpdated -TimeoutDateTime $timeoutDateTime -CheckIntervalSec 1 -ManifestPath $manifestPath -AgentUpgradeCode $agentUpgradeCode -PlatformKey $platformKey
Wait-UpdatersComplete -TimeoutDateTime $timeoutDateTime -CheckIntervalSec 1
$timeoutSec = [math]::Ceiling(($timeoutDateTime - (Get-Date)).TotalSeconds)
if ($timeoutSec -le 0)
{
throw "Can't run final update because of small timeout."
}
& $agentCtlPath update --all --wait $timeoutSec
if (-not $?)
{
throw "Final update operation failed."
}
}
function Get-ProductCode([guid] $upgradeCode)
{
$upgradeCodeStr = $upgradeCode.ToString("B") # Format as {xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}
$wmiFilter = "Property='UpgradeCode' AND Value='$upgradeCodeStr'"
$productCode = @(Get-WmiObject -Class Win32_Property -Filter $wmiFilter).ProductCode
if ($productCode.Count -eq 0)
{
throw "Upgrade code '$upgradeCode' not found."
}
if ($productCode.Count -gt 1)
{
throw "Multiple products with the same upgrade code '$upgradeCode' were found."
}
return [guid]::Parse($productCode)
}
function Get-AgentEnvironment([string] $installDirPath)
{
$configPath = Join-Path $installDirPath "AgentEnvironment.config"
if (-Not (Test-Path $configPath))
{
throw "Config file not found at $configPath."
}
[xml]$agentEnvironment = Get-Content $configPath
return $agentEnvironment
}
function Get-InstallLocation([guid] $productCode)
{
$productObject = Get-WmiObject -Class Win32_Product | Where-Object { [guid]::Parse($_.IdentifyingNumber) -eq $productCode }
if ($null -eq $productObject)
{
throw "The product GUID was not found."
}
return $productObject.InstallLocation
}
$timeoutDateTime = (Get-Date).AddSeconds($TimeoutSec)
Write-Host "Gathering information..."
try
{
$agentProductCode = Get-ProductCode -UpgradeCode $UpgradeCode
$installDirPath = Get-InstallLocation -ProductCode $agentProductCode
Write-Host " Install directory: $installDirPath"
$agentEnvironment = Get-AgentEnvironment -InstallDirPath $installDirPath
$dataDirPath = $agentEnvironment.agentEnvironment.agentDataDirectory
Write-Host " Data directory: $dataDirPath"
$regPath = Join-Path "HKLM:" $agentEnvironment.agentEnvironment.registryPath
Write-Host " Registry key: $regPath"
$platformKey = Get-ItemProperty -Path $regPath -Name "PlatformManifestKey" -ErrorAction Stop | Select-Object -ExpandProperty "PlatformManifestKey" -ErrorAction Stop
Write-Host " Platform key: $platformKey"
$agentCtlPath = Join-Path $installDirPath "STAgentCtl.exe"
$manifestPath = Join-Path $dataDirPath "manifest.xml"
}
catch
{
Write-Error "ERROR: $_"
exit -1
}
if ((Get-Date) -gt $timeoutDateTime)
{
Write-Error "ERROR: The timeout expired before the agent update could begin."
exit -1
}
Write-Host "Unsealing the agent..."
& $agentCtlPath unseal
if (-not $?)
{
Write-Error "ERROR: can't perform unseal operation."
exit -1
}
Write-Host "Updating the agent..."
$result = -1
try
{
Update-Agent -TimeoutDateTime $timeoutDateTime -ManifestPath $manifestPath -AgentUpgradeCode $UpgradeCode -AgentCtlPath $agentCtlPath -PlatformKey $platformKey
$result = 0
}
catch
{
Write-Error "ERROR: $_"
}
if ($DoNotSeal)
{
if ($result -eq 0)
{
Write-Host "The agent has been successfully updated."
}
else
{
Write-Error "ERROR: agent update failed."
}
exit $result
}
Write-Host "Sealing the agent..."
& $agentCtlPath seal
if (-not $?)
{
Write-Error "ERROR: can't seal the agent!"
exit -1
}
if ($result -eq 0)
{
Write-Host "The agent has been successfully updated and sealed."
}
exit $result
(@ECHO OFF
set pw=supersecret
set logfile=C:\ProgramData\Ivanti\Ivanti Cloud Agent\Logs\agentkeyrestore.log
set certFile=C:\Program Files\Ivanti\Ivanti Cloud Agent\agent.pfx
set scriptPath=%0%
ECHO %date% %time% Agent key restore started >> "%logfile%"
IF EXIST "%certFile%" (
certutil -f -p "%pw%" -privatekey -importPFX "Ivanti Cloud Agent Agent" "%certFile%" NoCert,NoExport >> "%logfile%" 2>&1
IF %ERRORLEVEL% NEQ 0 (
ECHO Failed to import agent key >> "%logfile%"
)
ECHO Removing agent key file >> "%logfile%"
del "%certFile%" >> "%logfile%" 2>&1
IF %ERRORLEVEL% NEQ 0 (
ECHO Failed to delete agent key file >> "%logfile%"
)
ECHO %date% %time% Agent key restore complete >> "%logfile%"
) ELSE (
ECHO %date% %time% Agent key restore failed: no agent key file found >> "%logfile%"
)
ECHO Removing script >> "%logfile%"
del "%scriptPath%" >> "%logfile%" 2>&1
"REM no further commands can be executed from this point because script file has been deleted"