Publish a vCenter Content Library with Windows IIS
Publishing ISOs and appliances with IIS, an existing certificate, and automated VCSP catalog generation.
IIS + HTTPS · Two-hour catalog refresh · PowerShell scripts
The deployment
I built a vCenter Content Library publisher on Windows Server using IIS and HTTPS to serve ISOs and virtual appliances. This guide follows the scripts, scheduled catalog generation and download checks I used, while vCenter stores downloaded subscription content on its own selected datastore.
The scheduled catalog refresh runs every two hours. Windows holds the published files; vCenter stores downloaded subscription content on its own selected datastore.
The five scripts on GitHub are the scripts I ran on the server. The public copies retain their original behavior, with an example hostname substituted for my internal name. My server ran Windows Server 2025 Datacenter, and the Python installation used in the deployment was 3.12.10.
Completed ISO/OVA files on E:\
-> E:\ContentLibrary\<item-name>\
-> Python generates lib.json, items.json, and item.json
-> IIS serves the catalog and payloads over HTTPS
-> vCenter subscribes to /lib.jsonDownload and prepare
Download the content-library repository into a setup folder such as C:\Setup\content-library. Run the deployment commands from an elevated Windows PowerShell 5.1 console on the server.
Before starting, have DNS pointing your publisher name at the server, network access from vCenter and download clients, sufficient space on E:, and your certificate installed in Cert:\LocalMachine\My. The certificate must have its private key, cover the publisher hostname, and be trusted by clients.
Get-ChildItem Cert:\LocalMachine\My |
Select-Object Subject, Thumbprint, NotAfter, HasPrivateKey
Set-Location C:\Setup\content-libraryI use library.example.com throughout this post. Replace it and the certificate thumbprint with your own values. The original installers default to D:, while the organizer defaults to E:, so I pass the content paths explicitly.
Prepare DNS and the certificate
- Give the Windows Server a stable IP address and confirm the E: volume is available. These instructions assume Windows Server with Desktop Experience and an existing lab/domain DNS service.
- In DNS Manager on your DNS server, expand Forward Lookup Zones, select your internal zone, and choose New Host (A or AAAA). Enter
libraryas the name and the publisher server’s IPv4 address. Use your real zone, not example.com. If DNS is managed elsewhere, create the equivalent A record there. - From a management workstation using the same DNS infrastructure as vCenter, run the DNS check below. Confirm vCenter uses a DNS server that can resolve that record too. Do not rely on a hosts-file entry made only on your workstation.
- If your existing certificate is already in the server’s Local Computer personal store, do not import it again. Otherwise obtain a PFX containing the certificate and private key from your certificate administrator using your approved secure process.
- On the publisher, open
certlm.mscas administrator. Expand Personal → Certificates, right-click Certificates, and select All Tasks → Import. Select the PFX, enter its password when prompted, and import into Personal. Do not put the PFX or its password in the web root, scripts, or GitHub. - Open the certificate and confirm it has a private key, has not expired, includes the publisher FQDN in Subject Alternative Name, and permits server authentication. Ensure the issuing root/intermediate CAs are trusted by the clients and vCenter through your organization’s certificate-management process.
- On GitHub, choose Code → Download ZIP, extract it, and place the contents under
C:\Setup\content-library. Confirm that this folder contains the scripts directory. Review the scripts before running them; if Windows blocks trusted downloaded files, unblock only the reviewed files rather than changing machine-wide execution policy.
Resolve-DnsName library.example.com
Test-Path E:\
Get-ChildItem C:\Setup\content-library\scripts\*.ps1Certificate import follows the Microsoft Local Computer certificate-store workflow. Certificate issuance is handled by your CA administrator; this guide reuses an existing certificate.
1. Install the IIS publisher
Install-vCenterContentLibraryPublisher.ps1 installs IIS, creates the content directory and application pool, configures the HTTPS site, attaches the existing certificate, adds MIME types for the library files, and creates the HTTPS firewall rule.
.\scripts\Install-vCenterContentLibraryPublisher.ps1 `
-PublisherFqdn 'library.example.com' `
-ContentRoot 'E:\ContentLibrary' `
-CertificateThumbprint 'REPLACE_WITH_YOUR_CERTIFICATE_THUMBPRINT'Outcome: an IIS site named vCenter-Content-Library serving E:\ContentLibrary over HTTPS. The catalog URL will become available after generation in the next steps.
This script also creates E:\ContentLibrary\_staging. The automation installer below relocates that folder outside the published root. Run the scripts in this order, and do not rerun the publisher against an existing site. Its firewall rule applies to Domain and Private profiles; restrict remote access to the networks that need the library.
Import-Module WebAdministration
Get-Website -Name 'vCenter-Content-Library' |
Select-Object Name, State, PhysicalPath, ApplicationPool
Get-WebBinding -Name 'vCenter-Content-Library'2. Install catalog automation
Install-VCSPCatalogAutomation.ps1 looks for a working Python interpreter and skips Microsoft Store aliases. If none qualifies, its original fallback downloads Python 3.12.10, verifies the installer signature, and installs it. That version reflects the lab deployment; for a new server, prefer a maintained all-users Python installation and confirm compatibility.
.\scripts\Install-VCSPCatalogAutomation.ps1 `
-LibraryName 'Published Content Library' `
-ContentRoot 'E:\ContentLibrary' `
-StagingRoot 'E:\ContentLibrary-Staging'The installer creates a virtual environment, installs the Python dependencies, and downloads make_vcsp_2022.py from lamw/vmware-scripts with an expected SHA-256 check. AWS and Azure modules are installed because the generator imports them, even though this deployment uses local storage and does not need cloud credentials.
Outcome: C:\ProgramData\VCSPCatalog contains the Python environment, generator, generated Scripts\Run-VCSPCatalog.ps1, and logs. The task Regenerate vCenter VCSP Catalog runs as SYSTEM every two hours, starting approximately two minutes after registration. It ignores overlapping scheduled instances, has a one-hour execution limit, and removes task logs older than 30 days after successful generation.
There is no separate runner to download: the installer creates it. Rerunning automation can update dependencies and overwrite the generator, runner, and task, so back up first and avoid updating during an active run. Keep the automation directory and underlying Python installation administrator-controlled.
If the sibling staging folder already exists while the nested staging folder is still present, the installer stops rather than merging them. Reconcile those folders before continuing. Keep incomplete uploads outside the content root.
Get-ScheduledTask -TaskName 'Regenerate vCenter VCSP Catalog'
Test-Path C:\ProgramData\VCSPCatalog\PythonVenv\Scripts\python.exe
Test-Path C:\ProgramData\VCSPCatalog\Scripts\Run-VCSPCatalog.ps1
Test-Path E:\ContentLibrary-Staging
Test-Path E:\ContentLibrary\_staging3. Configure publisher access
The initial website gave me HTTP 401.3 with access denied. The application pool had permission to the directory, but anonymous authentication was not using that identity. Repair-VCSPPublisherAccess.ps1 addresses that mismatch.
.\scripts\Repair-VCSPPublisherAccess.ps1 `
-SiteName 'vCenter-Content-Library' `
-ContentRoot 'E:\ContentLibrary'Outcome: anonymous authentication uses the site’s application-pool identity, that pool receives read/execute permission on the supplied content root, and the pool is restarted. This is a separate step in the working deployment, not part of the original publisher installer.
4. Organize the ISOs and OVAs
My workflow is to copy complete ISO/OVA files to the top of E: and then run Organize-VCSPContent.ps1. It creates a folder from each file’s base name and moves the file into it. Preview the moves first:
.\scripts\Organize-VCSPContent.ps1 `
-SourcePath 'E:\' `
-DestinationRoot 'E:\ContentLibrary' `
-WhatIf
.\scripts\Organize-VCSPContent.ps1 `
-SourcePath 'E:\' `
-DestinationRoot 'E:\ContentLibrary'Outcome: E:\Windows-Server.iso becomes E:\ContentLibrary\Windows-Server\Windows-Server.iso. The script skips an already-existing destination file and reports its moves. It only scans the source directory’s top level.
The original organizer preserves filenames and can reuse an existing item folder. Use lowercase .iso/.ova extensions and distinct base names so an ISO and OVA are not grouped accidentally. Run it after copying has finished. It does not change file ACLs; check IIS access if a moved file retains restrictive source permissions.
E:\ContentLibrary\
lib.json
items.json
Windows-Server\
Windows-Server.iso
item.json
Appliance\
Appliance.ova
item.jsonThe generator treats each immediate child directory as an item. Keep an OVF and its related disks/manifest together in one item folder, without nested subdirectories.
5. Generate the catalog and subscribe in vCenter
After adding files, let the scheduled task complete or start it manually. Check the task status and log before synchronizing vCenter:
Start-ScheduledTask -TaskName 'Regenerate vCenter VCSP Catalog'
Get-ScheduledTaskInfo -TaskName 'Regenerate vCenter VCSP Catalog'
Get-ChildItem C:\ProgramData\VCSPCatalog\Logs |
Sort-Object LastWriteTime -Descending |
Select-Object -First 6
Invoke-WebRequest 'https://library.example.com/lib.json' -UseBasicParsingOutcome: the generator creates or updates lib.json, items.json, and the per-folder item.json files. In my deployment, the HTTPS request returned 200 OK with the library JSON. That was the publisher endpoint I wanted to expose to vCenter.
Wait for the Windows task to finish
$taskName = 'Regenerate vCenter VCSP Catalog'
Get-ScheduledTask -TaskName $taskName | Select-Object TaskName, State
Get-ScheduledTaskInfo -TaskName $taskName |
Select-Object LastRunTime, LastTaskResult, NextRunTime
$latestLog = Get-ChildItem C:\ProgramData\VCSPCatalog\Logs\make-vcsp-*.log |
Sort-Object LastWriteTime -Descending |
Select-Object -First 1
if ($latestLog) { Get-Content -LiteralPath $latestLog.FullName -Tail 40 }If State is Running, wait and repeat these read-only checks. When it returns to Ready, confirm LastRunTime matches the run you just requested and LastTaskResult is 0. A result of 267009 (0x41301) indicates a running task, not completion. Inspect the newest log for errors. Do not use a zero from an older run as evidence that the current run finished.
Create the subscribed library in vCenter
- Sign in to the vSphere Client with permission to create Content Libraries. Open Menu → Content Libraries, then Create or Create Content Library. Labels vary slightly by release.
- Enter a library name such as Windows Published Library, add a description if desired, and select the vCenter Server where the subscribed library will live.
- Choose Subscribed content library, not Local content library. Enter
https://library.example.com/lib.json, using your publisher’s actual hostname. - Leave subscription authentication disabled for this anonymous IIS publisher. Do not enter your Windows/domain credentials. Enable automatic synchronization if you want vCenter to refresh the subscription on its own schedule.
- Choose Download all library content immediately if you want payloads copied to vCenter storage up front. Choose Download library content only when needed to initially retrieve metadata and download payloads when used. Ensure you have enough storage for the policy you choose.
- If prompted about the publisher certificate, verify its identity/thumbprint against the certificate installed on your publisher. Resolve unexpected hostname, expiry, or trust errors rather than blindly accepting them.
- Select the datastore to hold downloaded content. This is storage available to vCenter/ESXi, not the Windows E: volume. Review the settings and click Finish.
- Open the new library and use its Actions → Synchronize action if needed. Monitor Recent Tasks, then check the library’s Templates and/or Other Types tabs for your items.
- For on-demand content, select an item and use its Synchronize/download action when available, or perform the operation that needs the payload. Confirm one ISO or appliance is usable before adding your full collection.
A synchronized item list does not necessarily mean all payloads are stored locally when you choose on-demand downloads. Broadcom describes that distinction in its subscribed-library synchronization guidance.
For each later addition: finish copying the file, run the organizer, run or wait for the Windows catalog task, confirm that run completes, and then synchronize the vCenter library. These are two separate schedules; vCenter cannot discover metadata Windows has not generated yet.
6. Browse the library and optionally enable HTTP
I also wanted to browse the server and download a specific ISO directly. Directory browsing is disabled by the publisher installer, so I enabled it separately:
Install-WindowsFeature Web-Dir-Browsing
Import-Module WebAdministration
Set-WebConfigurationProperty `
-PSPath 'MACHINE/WEBROOT/APPHOST' `
-Location 'vCenter-Content-Library' `
-Filter 'system.webServer/directoryBrowse' `
-Name enabled -Value $trueOutcome: https://library.example.com/ provides a folder listing instead of requiring a default homepage. Browse into an item folder to download its ISO or appliance. This exposes filenames to clients that can reach the anonymous site, so keep network access restricted.
For optional HTTP access, run Enable-VCSPHttp.ps1:
.\scripts\Enable-VCSPHttp.ps1 `
-SiteName 'vCenter-Content-Library' `
-HostName 'library.example.com'Outcome: an HTTP host-header binding on port 80 and an inbound firewall rule, while the existing HTTPS binding remains. Use http://library.example.com/ for HTTP browsing; keep the vCenter subscription on HTTPS. HTTP traffic is unencrypted.
If HTTP redirects or remains blocked, check Require SSL, HTTP Redirect, URL Rewrite, and browser HTTPS-only settings. The HTTP script does not remove those settings.
What caught me out
The Python discovery issue was the first hurdle. A python.exe entry on PATH could be a Store alias rather than the actual runtime. Checking the real executable with --version showed that Python was installed and working.
The next issue was quoting in an inline Python dependency check. Windows PowerShell altered the embedded quotes and Python reported an unclosed parenthesis. The working installer uses the generator’s --help option to check its imports instead.
The IIS errors had different causes. HTTP 401.3 pointed to the anonymous identity and filesystem permissions. A root URL returning 403 while lib.json returned 200 was a separate browsing/default-document issue. Fixing one did not automatically fix the other.
Finally, Restart-Website is not an IIS PowerShell cmdlet. The repair script uses Restart-WebAppPool; directory-browsing changes generally apply without restarting the website.
Final thoughts
The five scripts keep those jobs separate: build the website, install catalog automation, repair publisher access, organize incoming files, and add HTTP if it is needed. vCenter consumes the resulting catalog through its subscribed-library workflow; it does not need an iSCSI target on this server.
Build your Windows Content Library publisher
Get the five scripts and the deployment order used in this lab.
