Writing

Building a Secure Wallpaper Distribution Agent for Windows

How I used PowerShell, Microsoft Intune, privilege separation, and content validation to manage desktop and lock-screen images

12 min read
Endpoint Security Scheduled
Microsoft IntunePowerShellWindowsEndpoint SecuritySHA-256Scheduled Tasks

Distributing a corporate wallpaper may sound like a simple task.

On a single computer, it is enough to select an image and apply it. In a managed environment, however, the solution must work for multiple users, survive restarts, update content without rebuilding the package, operate with minimum privileges, and remain reliable when the network or publishing process fails.

The images are also remote content. Even when they come from an organization-controlled origin, an endpoint should not trust them without validation.

To solve this problem, I created WallpaperAgent, a PowerShell-based agent for distributing, validating, and applying desktop wallpapers and lock-screen images on managed Windows endpoints.

The project is available on GitHub:

The problem

I needed a solution that could update the visual content on managed devices without requiring a complete Microsoft Intune deployment for every change.

When I analyzed the workflow, I found several separate responsibilities:

  1. Publish new images.
  2. Deliver the agent to endpoints.
  3. Download and validate content safely.
  4. Apply the images in the signed-in user’s profile.
  5. Retain enough state for diagnostics, detection, and recovery.

Running everything through one script or one security context would create both risks and functional limitations.

A process running as SYSTEM can manage protected files and machine tasks, but it does not correctly represent each interactive user’s profile. A standard user process, on the other hand, should not control downloads, executable code, or global installation state.

The project needed to answer several questions:

  • How can images change without republishing the Win32 package?
  • How can publishing credentials remain off the endpoints?
  • How can the agent prove that a downloaded file is the expected file?
  • How can it prevent a partially published release from becoming active?
  • How can it apply settings that belong to the user context?
  • How can it keep the current release working after an update failure?

The central architectural decision became:

Separate publishing, machine-level validation, and user-context application.

Project architecture

WallpaperAgent separates the agent code from the distributed content.

The project repository contains:

  • installation and uninstallation scripts;
  • update and apply workers;
  • agent configuration;
  • Microsoft Intune detection logic;
  • Win32 packaging support;
  • validation, testing, and diagnostics;
  • operational and security documentation.

The images and manifest.json live in a separate content repository or HTTPS origin.

Publishing server / Portal
          |
          | write credential
          v
Content repository or HTTPS origin
          |
          | anonymous or controlled HTTPS read
          v
Endpoint updater (SYSTEM)
          |
          | validated files and machine state
          v
Endpoint applier (interactive user)
          |
          +--> Desktop wallpaper
          `--> Windows lock screen

This separation allows a visual campaign to change without modifying the agent code or producing a new .intunewin, provided that the existing manifest contract remains compatible.

Separating code and content

Wallpapers are operational content, not agent code.

Combining both in the same package would have several consequences:

  • every image change would require a package rebuild;
  • Intune would redistribute files that do not change the agent logic;
  • the publishing lifecycle would be coupled to the software lifecycle;
  • publishing credentials could end up too close to the endpoints.

In WallpaperAgent, the publishing process has write access to the content repository. Endpoints have only read access to the manifest and images.

When the content is not sensitive, it can be delivered through a public repository and downloaded anonymously. For private content, the recommended design is a controlled HTTPS service, proxy, object store, or CDN with appropriate read controls.

The important point is that a write credential must never be included in:

  • the Intune package;
  • the configuration file;
  • the manifest;
  • asset URLs;
  • the endpoint environment;
  • agent logs.

Two scheduled tasks, two security contexts

One of the main design decisions was to use two scheduled tasks.

Update task

The update task runs as SYSTEM.

It is responsible for:

  • downloading the manifest;
  • validating its structure and size;
  • downloading images to temporary files;
  • validating extension, size, hash, and image decoding;
  • promoting a new release only after complete validation;
  • updating machine state;
  • starting the apply task when the content changes.

This process requires machine privileges because it writes under ProgramData, owns shared state, and operates inside the protected installation tree.

Apply task

The apply task runs through the built-in INTERACTIVE group with limited privileges.

It is responsible for:

  • reading the validated machine state;
  • comparing the desired configuration with the current user profile;
  • applying the desktop wallpaper through Windows interfaces;
  • applying the lock screen through Windows Runtime APIs;
  • recording user-specific state under %LOCALAPPDATA%;
  • reapplying the managed configuration after a manual change is detected.

This process does not need to modify the agent, download files, or control global state.

SYSTEM
  |
  +-- HTTPS downloads
  +-- Content validation
  +-- ProgramData assets and state
  +-- HKLM registration
  `-- Scheduled-task definitions

Interactive user
  |
  +-- Read validated assets
  +-- HKCU configuration
  +-- Lock-screen API
  `-- LocalAppData state and logs

The installation tree grants full control only to SYSTEM and Administrators. Standard Users receive read and execute access.

This allows the interactive process to consume validated files without being able to replace the updater, scripts, or managed assets.

The manifest as a publishing contract

The manifest.json file is the contract between the publishing process and endpoints.

It describes one combined release containing a desktop wallpaper and a lock-screen image.

A simplified example:

{
  "schemaVersion": 1,
  "release": "2026.07.30.1",
  "publishedAt": "2026-07-30T10:00:00Z",
  "desktop": {
    "version": "2026.07.30.1",
    "fileName": "wallpaper_desktop_20260730.jpg",
    "url": "https://example.invalid/windows/desktop/wallpaper.jpg",
    "sha256": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
  },
  "lockScreen": {
    "version": "2026.07.30.1",
    "fileName": "lockscreen_20260730.jpg",
    "url": "https://example.invalid/windows/lockscreen/lockscreen.jpg",
    "sha256": "abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789"
  }
}

The current agent accepts schema version 1.

Each asset must declare:

  • a version;
  • a leaf file name;
  • an absolute HTTPS URL;
  • the SHA-256 digest of the published file.

The fileName value cannot include a path. Values such as ../wallpaper.jpg, C:\Temp\wallpaper.jpg, or windows/desktop/image.jpg are rejected.

This prevents a manifest from controlling arbitrary local write locations.

Treating remote content as untrusted input

WallpaperAgent treats the manifest and images as untrusted input until every required validation succeeds.

For each candidate release, the updater checks:

  • that the manifest is within the configured size limit;
  • that the JSON is valid;
  • that the schema version is supported;
  • that the release identifier is not empty;
  • that desktop and lock-screen definitions exist;
  • that URLs are absolute HTTPS URLs;
  • that file names contain no paths;
  • that extensions are .jpg, .jpeg, or .png;
  • that hashes match the required SHA-256 format;
  • that files remain within the configured size limit;
  • that the calculated hash matches the manifest;
  • that Windows can decode the content as an image;
  • that the image width and height are greater than zero.

HTTPS protects transport, but it does not define which exact file was expected.

SHA-256 validation binds each manifest entry to the exact bytes the publisher intended to distribute.

A file with an accepted extension and matching hash must still pass image decoding. This prevents the agent from accepting content that has the correct name but cannot be processed as a valid image.

Temporary files and atomic promotion

The agent does not download a new image directly over the active file.

Each asset is downloaded to a uniquely named temporary file. Only after size, hash, and decoding validation does the agent move it to the final destination.

The primary state is handled carefully as well.

The current.json file represents the currently validated release. It is updated only after both the desktop and lock-screen assets are available and approved.

JSON state files are written atomically, reducing the chance that another process reads partially written content during an update.

The workflow is:

Download manifest
      |
Validate schema and fields
      |
Download both assets to temporary files
      |
Validate size, SHA-256, and image decoding
      |
Promote the assets
      |
Atomically write current.json
      |
Trigger the user task

This prevents a hybrid state where the desktop belongs to one release and the lock screen belongs to another.

Failing without destroying the working release

A central project principle is that an update failure must not remove the current release.

If any of the following occurs:

  • a network outage;
  • malformed manifest JSON;
  • incompatible schema;
  • insecure URL;
  • oversized file;
  • incorrect hash;
  • corrupted image;
  • missing asset;

…the candidate release is rejected and the previous validated release remains available.

The failure is recorded in status.json and update.log, but the agent does not intentionally clear current.json or remove the working assets.

This makes the update process a controlled promotion instead of an optimistic replacement.

Applying content in the user profile

After the machine has a validated release, the interactive task compares the desired state with the user’s current state.

For the desktop, the agent uses:

  • settings under HKCU\Control Panel\Desktop;
  • the Win32 SystemParametersInfo function.

For the lock screen, it uses Windows Runtime APIs.

The agent checks both recorded hashes and the actual Windows configuration. Therefore, when a user manually changes the desktop wallpaper or lock screen, a later run can detect the difference and restore the managed release.

The two operations are isolated.

If the desktop application succeeds but the lock-screen operation fails, the desktop result is retained and the lock-screen error is logged. The process does not automatically undo a valid result because the other operation failed.

Machine state and per-user state

The project stores machine data separately from each user’s data.

An example machine layout:

C:\ProgramData\{Organization}\WallpaperAgent\
|-- Agent\
|-- Assets\
|   |-- Desktop\
|   `-- LockScreen\
|-- State\
|   |-- current.json
|   |-- manifest.json
|   `-- status.json
`-- Logs\
    |-- install.log
    `-- update.log

Per-user state:

%LOCALAPPDATA%\{Organization}\WallpaperAgent\
|-- current.json
`-- Logs\apply.log

Machine state describes the validated release and local asset paths. User state records the applied hashes and paths, lock-screen URI, and application timestamp.

This separation allows different users on the same computer to be evaluated independently.

Concurrency control

Scheduled tasks can be triggered through startup, sign-in, periodic repetition, or a newly published update.

To prevent overlapping executions, the project uses two layers of control:

  • MultipleInstances IgnoreNew in scheduled-task settings;
  • named mutexes in the code.

The updater uses an organization-specific global mutex. The apply worker uses a mutex derived from the organization and the current user’s SID.

This reduces the risk that two executions modify the same state or apply the same release to one profile simultaneously.

Installation and Microsoft Intune packaging

The installer prepares the complete endpoint structure.

It:

  • confirms that it is running in 64-bit Windows PowerShell;
  • requires administrative privileges;
  • validates required package files;
  • creates machine directories;
  • copies scripts, the helper module, and configuration;
  • applies restrictive ACLs;
  • creates the scheduled-task folder;
  • registers update and apply tasks;
  • writes detection values to HKLM;
  • attempts the first content update.

The project also includes a build script that creates a clean packaging workspace, generates a detection script containing the expected package version, and can optionally produce the .intunewin file.

Recommended Intune settings:

FieldValue
Install commandInstall.cmd
Uninstall commandUninstall.cmd
Install behaviorSystem
Restart behaviorNo specific action
Architecture64-bit
Detection ruleCustom detection script
Run as 32-bitNo

Intune detection

Detection does not rely only on the existence of a directory.

The script validates:

  • ProductCode = WallpaperAgent in the registry;
  • the expected package version;
  • the registered installation path;
  • the installed scripts, configuration, and helper module;
  • the existence of both scheduled tasks;
  • that neither task is disabled.

The generated detection artifact embeds the expected version while discovering organization-specific paths and task names from the registry.

This separates installation state from content state.

Intune confirms that the correct agent is installed. status.json and the diagnostic tooling indicate whether the latest content update succeeded.

Diagnostics and observability

The project includes a diagnostic tool that gathers operational information without requiring manual inspection of every file.

It can report:

  • resolved configuration;
  • registry detection values;
  • scheduled-task state;
  • current agent version;
  • active content release;
  • the most recent update result;
  • local asset paths;
  • relevant log entries.

Updater and per-user logs rotate based on configured size limits.

This avoids unbounded growth while preserving enough evidence to investigate download, validation, and application failures.

Security decisions

The main security controls include:

  • isolating publishing credentials from endpoints;
  • HTTPS-only downloads;
  • file-name validation against directory traversal;
  • a restricted extension allowlist;
  • manifest and asset size limits;
  • SHA-256 verification;
  • real image decoding;
  • temporary download files;
  • promotion only after complete validation;
  • atomic state writes;
  • separation between machine and user privileges;
  • restrictive ACLs;
  • mutex-based concurrency control;
  • preservation of the last working release;
  • staged pilot and expansion deployment.

The solution does not assume that an internal network, a repository, or a file ending in .jpg is automatically trustworthy.

Uninstallation and asset preservation

By default, the uninstaller removes the agent, scheduled tasks, and operational configuration while preserving the currently used assets.

This is deliberate.

Windows may continue referencing an image path after the agent is removed. Deleting the file immediately could leave the user profile pointing to a missing asset.

When full cleanup is required, the -RemoveAssets parameter also deletes downloaded images.

The uninstaller supports PowerShell -WhatIf, allowing administrators to preview the removal before execution.

Limitations and operational considerations

Lock-screen behavior can vary according to:

  • Windows edition and version;
  • device state;
  • existing organization policies;
  • Windows Runtime API availability;
  • endpoint personalization restrictions.

For that reason, validation must include representative devices and multiple user profiles.

It is also important to distinguish three separate states:

  1. The agent is installed.
  2. A content release has been validated by the machine.
  3. That release has been applied to the current user.

These states can change at different times and should be observed separately.

Rollout strategy

The recommended rollout uses progressive rings:

Local validation
      |
Pilot devices
      |
IT and support staff
      |
Representative user group
      |
General deployment

Before general deployment, I consider it important to test:

  • Intune installation and upgrade;
  • both scheduled-task security contexts;
  • multiple users on the same endpoint;
  • sign-in, restart, and periodic execution;
  • malformed manifest JSON;
  • an incorrect hash;
  • an invalid image;
  • a network failure;
  • preservation of the previous release;
  • manual wallpaper changes;
  • agent upgrade and uninstallation.

What I learned

This project reinforced that endpoint automation is not only about executing one action on many computers.

The difficult part is defining which components may trust each other, which privilege each component actually needs, and how the system should behave when it receives incomplete or invalid data.

I also learned that content and software have different lifecycles.

The agent should change when its logic or contract changes. Images should change through an independent content publication. Separating those lifecycles reduces operational work and limits the distribution of privileges.

The main lesson was:

A safe update does not replace the current state until the new release has been proven complete and valid.

Next steps

Possible future improvements include:

  • signing the PowerShell scripts;
  • automated tests for manifest parsing, validation, and state promotion;
  • metrics for monitoring platforms;
  • notifications for persistent failures;
  • optional support for a private authenticated origin using endpoint-safe read credentials;
  • richer per-user application telemetry;
  • an automated pipeline for validating and publishing new content releases;
  • additional documentation for different Windows editions;
  • sanitized evidence from a pilot deployment.

WallpaperAgent already provides a structured foundation for distributing visual content without turning a seemingly simple task into a process with excessive privileges, exposed credentials, or fragile updates.

Source code

The complete project, installation scripts, documentation, and packaging instructions are available on GitHub: