Windows unattended deployment in air-gapped / closed-environment networks
You're deploying Windows 11 in a network that can't reach chocolatey.org, the winget index, or Microsoft Update. Every install
source is an internal UNC path. Every policy comes from an internal share. The
WSUS mirror is your only update channel. The good news: the unattend.xml side
hasn't changed. Every online step has an offline equivalent — you just need to
know which. This page is that map.
What "air-gapped" means here
The term air-gapped covers a spectrum of real-world isolation postures. This guide uses it loosely to mean "the machine being provisioned cannot reach the public internet during install or first logon." That covers two distinct cases that need slightly different handling.
| Posture | LAN access | Internet access | Implication for unattend |
|---|---|---|---|
| True air-gap | None outside the secure enclave | None | Everything ships on removable media. UNC paths must point at a fileserver inside the enclave. No GitHub, no hosts-file imports from third-party repos. |
| Closed network with internal LAN | Internal fileservers, DNS, WSUS, internal CA | None or proxied with deny-by-default | Same as above plus internal WSUS for OS patching and an internal root CA for certificate trust. The common enterprise case. |
| Restricted-internet | Yes | Only allow-listed hosts via proxy | Treat as closed-network. Do not assume Chocolatey, winget, or any direct Microsoft host is reachable. If you need an exception, document the allow-listed hosts in the unattend's first-run script. |
The unattend snippets below assume the second case — a closed network with a working internal fileserver, internal DNS, an internal WSUS, and an internal CA. The advice extends to the true-air-gap case if you swap UNC paths for local-disk paths (everything pre-staged on the install media or a USB key).
first-run snippets in the builder assume an
internet-connected target: install-chocolatey pulls a script
from chocolatey.org, install-netfx35 with no /source falls back to Windows Update, capability installs use the online component
store. Each of those fails — usually silently — on a closed network. This
page documents the offline equivalents.Software install from a local share
In a closed network, the canonical install source is a UNC path on an internal SMB fileserver. The two questions to settle before writing the unattend are who reads the share (which determines the security context) and when the install runs (which determines whether the Windows network stack is even up yet).
MSI install from UNC
MSIs are the easiest case. Stage the .msi on an internal share, then call msiexec from SetupComplete.cmd or a RunSynchronousCommand in the appropriate pass. The exact
command from the audit:
msiexec /i \\fileserver\share\app.msi /qn /norestart The risk note attached to this snippet: "SYSTEM context can read computer-account-permitted shares only." At setup_complete the command runs as NT AUTHORITY\SYSTEM — the machine account on a domain-joined
device, which is not necessarily the same set of shares your interactive
user can read. Grant Domain Computers at least Read on the share's NTFS and share permissions before you
expect this to work.
MSU updates from UNC
Standalone update packages are .msu files installed with wusa.exe. The pattern is identical to MSIs — point at the UNC
path:
wusa.exe \\fileserver\updates\KB1234567.msu /quiet /norestart Our audit flags this one: "wusa being phased out post-24H2;
verify on next major build." If you maintain images across multiple
Windows versions, plan a migration path off wusa toward DISM /Add-Package for .cab updates (next snippet) before the
tool disappears.
DISM package install (.cab)
Many Microsoft updates and OEM packages ship as .cab rather
than .msu. Use DISM's online add-package mode:
dism /online /add-package /packagepath:"\\fileserver\packages\package.cab" /norestart DISM understands UNC paths natively. Always pass /norestart from an unattend context — letting the package decide whether to reboot
mid-install will desync the rest of your FirstLogonCommands.
Chocolatey from an internal repo
Many sites already standardise on Chocolatey for package management. Chocolatey supports private repositories — host the packages on an internal share (or a private Nexus / ProGet feed) and point the install command at it:
choco install pkgname --source=\\fileserver\choco-repo --confirm Per the audit: "Choco must be pre-installed; chain after
install-chocolatey OR stage choco.exe via setup_complete." You cannot use the public chocolatey.org bootstrap on a
closed network. Stage the Chocolatey install package on the same internal
share and install it as a regular MSI or .nupkg in SetupComplete.cmd before you call choco.
Capability and feature install from offline source
Windows capabilities — OpenSSH server, RSAT tools, the Active Directory module, .NET Framework 3.5, additional language packs — normally pull from Windows Update. On a closed network they need an explicit offline source.
DISM /Add-Capability with /source
dism /online /add-capability /capabilityname:OpenSSH.Server~~~~0.0.1.0 ^
/source:"\\fileserver\sxs" /limitaccess The /limitaccess flag tells DISM not to attempt the Windows
Update fallback when the local source is missing a payload — without it,
DISM will spend a long timeout reaching for the network before failing.
Use it on every air-gapped capability install.
The audit note on this one is short and important: "Capability name suffix is version-locked." The trailing ~~~~0.0.1.0 changes between Windows builds.
Pull the exact capability name with dism /online /get-capabilities on a reference machine of
the same build, then hard-code that string in your unattend.
.NET Framework 3.5 from SxS
The single most common air-gapped capability install is .NET Framework
3.5, which is still required by a long tail of legacy LOB applications.
The Windows install media carries the SxS payload at \sources\sxs. The builder's existing install-netfx35 snippet already uses an offline /source path; the audit recommends tagging it as air-gapped in the picker.
dism /online /enable-feature /featurename:NetFx3 /all ^
/source:D:\sources\sxs /limitaccess Substitute the drive letter for wherever the Windows install media is
mounted at setup_complete time. If you have removed the
media by then, copy \sources\sxs to the internal fileserver
during the build and reference it via UNC.
Language packs from offline CAB
dism /online /add-package ^
/packagepath:"\\fileserver\lp\Microsoft-Windows-Client-Language-Pack_x64_de-de.cab" Verbatim from the audit: "Language pack CAB must match exact OS build; mismatched returns 0x800f081e." This is the single most common failure mode for language-pack installs on a closed network — the CAB you have is from the previous quarterly build and the install fails with the cryptic 0x800f081e CBS_E_NOT_APPLICABLE. Mirror the language-pack share by build number on the fileserver: one folder per build, the unattend references the right one for the target image.
GPO and security baseline
Domain-joined machines pick up GPO from the DC at first boot. For
workgroup machines, off-domain laptops, or images deployed before the
domain-join step, you need to apply local policy directly. There are
three tools: LGPO.exe from the Microsoft Security
Compliance Toolkit, secedit for INF-formatted policy, and
provisioning packages.
LGPO.exe for local Group Policy
\\fileserver\tools\LGPO.exe /g \\fileserver\gpo-backup\baseline Verbatim from the audit: "LGPO.exe ships separately as part of MS Security Compliance Toolkit." It is not in the box on a stock Windows install. Download the Security
Compliance Toolkit once on an internet-connected staging machine, copy LGPO.exe to \\fileserver\tools\, and the
unattend can call it from SetupComplete.cmd.
The /g argument points at a GPO-backup folder produced by
the Group Policy Management Console
(Backup all GPOs) or by LGPO.exe /b on a
reference machine. Both produce the same on-disk format.
secedit for security baselines (.inf)
secedit /configure /db %WINDIR%\security\local.sdb ^
/cfg "\\fileserver\policies\baseline.inf" /quiet Verbatim from the audit: "Some baseline settings can lock the machine out of remote management; test first." The CIS and DISA baselines in particular enforce things like NTLMv2-only, signed SMB, and restricted PowerShell remoting. Run any new baseline on a throwaway VM and verify you can still RDP/WinRM into it before pushing to fleet.
Provisioning packages (.ppkg)
powershell -NoProfile -Command ^
"Install-ProvisioningPackage -PackagePath 'C:\install\corp.ppkg' ^
-ForceInstall -QuietInstall" Provisioning packages (built with Windows Configuration Designer) wrap Wi-Fi, certs, admin scripts, and policy into a single .ppkg blob. Verbatim risk note: "Some packages (cert installs especially) require a reboot." Plan one in your post-install sequence if your .ppkg includes certificates or device-installation policy.
Network and WSUS
Once the machine is on the network, three things have to happen before it can update itself safely: it has to point at internal WSUS, it has to stop reaching for the public Microsoft Update servers, and it has to trust the internal root CA.
Block internet Windows Update lookups
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" ^
/v UseWUServer /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" ^
/v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f Verbatim risk note from the audit: "Pair with point-to-wsus or device has no update source." These two registry writes only block the public path — they do not by
themselves point the device at internal WSUS. Combine with the
existing point-to-wsus snippet (which sets WUServer and WUStatusServer) so the device
has a working internal update channel.
Internal root CA
certutil -addstore -f Root "\\fileserver\certs\corp-root-ca.cer" Verbatim: "Must be .cer (DER or base64) — .p7b uses different syntax." If you have a .p7b bundle of root + intermediates, either
convert each cert to .cer and import in sequence, or use certutil -addstore -enterprise Root file.p7b. For
intermediate CAs use CA as the store name instead of Root.
Static internal DNS
On a closed network you cannot rely on DHCP option 6 being right —
especially if the DHCP service is shared with a less-trusted segment.
Pin the DNS servers explicitly using the existing set-static-dns snippet (which the audit recommends
tagging air-gapped):
powershell -NoProfile -Command ^
"Set-DnsClientServerAddress -InterfaceAlias 'Ethernet' ^
-ServerAddresses 10.1.1.10,10.1.1.11" Corporate Wi-Fi profile
netsh wlan add profile filename="\\fileserver\wifi\corp.xml" interface="Wi-Fi" Verbatim: "Interface name 'Wi-Fi' is localized — use interface='*' for all." A German-locale install ships with the interface named WLAN, French Wi-Fi as well but with a
different separator; interface="*" applies to every
wireless interface and avoids this trap.
Considerations and gotchas
SYSTEM vs user context when reading shares
Snippets at target: setup_complete run as NT AUTHORITY\SYSTEM. Snippets at target: first_logon run as the first interactive user.
Each has a different identity on the network: SYSTEM authenticates as
the computer account (e.g. DOMAIN\PC01$), the user
authenticates as themselves. Make sure whichever identity you target
can actually read the share.
Computer-account permissions on the fileserver
A common production accident is granting your own user account Read on the install share, testing the build interactively,
and then watching every other deployment fail because the deployments
run as SYSTEM and that account has no permission. Grant Domain Computers (or a specific group containing your
build VMs) Read on both share and NTFS ACL.
.ppkg reboot behaviour
Provisioning packages that include certificates, device-class install
policy, or modern provisioning instructions usually require a reboot to
apply fully. Install-ProvisioningPackage -ForceInstall
-QuietInstall will apply what it can immediately, but expect a
reboot before everything is in place. If your SetupComplete.cmd chain installs a .ppkg, leave room for a reboot step
before the next dependent command runs.
Language pack CAB build-number matching
Build mismatch is the loudest error in the air-gapped Windows world.
DISM returns 0x800f081e (CBS_E_NOT_APPLICABLE) when a CAB
targets a different build than the running OS. Mirror your language
packs by build, name folders unambiguously
(\\fs\lp\26100\, \\fs\lp\27000\), and reference
the matching folder from your unattend.
WSUS server certificate
If your WSUS uses HTTPS with a certificate signed by your internal CA,
the root CA install must happen before the WSUS point. Order
matters in SetupComplete.cmd: certutil first, registry
writes for WUServer/UseWUServer second, otherwise the first wuauclt /detectnow tries to fetch a metadata file over a
TLS connection it cannot validate.
Worked example: end-to-end deployment
A concrete walkthrough. The scenario: a mid-sized closed network with
one fileserver (fs01), an internal WSUS
(wsus01), and an internal CA. We need to deploy Windows 11
Enterprise to a fleet of identical laptops with OpenSSH server,
.NET 3.5, the CIS Level 1 baseline, the internal root CA, an MSI for
the corporate VPN client, and the corporate Wi-Fi profile — all without
the machines ever reaching the internet.
Step 1: Fileserver layout
\\fs01\deploy\
tools\
LGPO.exe (from MS Security Compliance Toolkit)
msi\
vpnclient-7.2.msi
choco-2.4.msi
packages\
KB5036000.cab (cumulative update)
sxs\26100\
... (copied from install.wim sources\sxs)
lp\26100\
Microsoft-Windows-Client-Language-Pack_x64_de-de.cab
gpo-backup\
cis-l1-baseline\ (LGPO backup folder)
policies\
cis-l1.inf (secedit INF baseline)
certs\
corp-root-ca.cer
wifi\
corp.xml (exported with netsh wlan export profile)
choco-repo\
7zip.24.06.nupkg (private chocolatey repo)
...
Share permissions: Domain Computers -- Read on share + NTFS
Domain Admins -- Full Control Step 2: Unattend.xml decisions
In the builder we set:
- Edition + locale: Windows 11 Enterprise,
en-US, German keyboard. - Product key: KMS client setup key, KMS host configured on
kms01. - Disk: GPT, wipe-and-create, single C: partition.
- Computer name:
DESKTOP-%RANDOM%(replaced post-deploy). - OOBE bypass: skip privacy, skip Microsoft account, skip network requirement.
- Win 11 bypass: SkipMachineOOBE-style flags only as needed for the target build (see bypass status).
- FirstLogonCommands and SetupComplete.cmd: the offline-source snippets below.
Step 3: SetupComplete.cmd command sequence
@echo off
:: -- 1. Trust the internal root CA FIRST (everything else may depend on it)
certutil -addstore -f Root "\\fs01\deploy\certs\corp-root-ca.cer"
:: -- 2. Pin WSUS and kill the internet-Update fallback
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" ^
/v WUServer /t REG_SZ /d "https://wsus01.corp.example/" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" ^
/v WUStatusServer /t REG_SZ /d "https://wsus01.corp.example/" /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate\AU" ^
/v UseWUServer /t REG_DWORD /d 1 /f
reg add "HKLM\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate" ^
/v DisableWindowsUpdateAccess /t REG_DWORD /d 1 /f
:: -- 3. Install offline cumulative update
dism /online /add-package ^
/packagepath:"\\fs01\deploy\packages\KB5036000.cab" /norestart
:: -- 4. .NET 3.5 from offline SxS
dism /online /enable-feature /featurename:NetFx3 /all ^
/source:"\\fs01\deploy\sxs\26100" /limitaccess
:: -- 5. OpenSSH server capability (verify the version suffix on the build)
dism /online /add-capability ^
/capabilityname:OpenSSH.Server~~~~0.0.1.0 ^
/source:"\\fs01\deploy\sxs\26100" /limitaccess
:: -- 6. German language pack
dism /online /add-package ^
/packagepath:"\\fs01\deploy\lp\26100\Microsoft-Windows-Client-Language-Pack_x64_de-de.cab"
:: -- 7. Apply CIS Level 1 baseline
\\fs01\deploy\tools\LGPO.exe /g \\fs01\deploy\gpo-backup\cis-l1-baseline
:: -- 8. Apply security baseline INF
secedit /configure /db %WINDIR%\security\local.sdb ^
/cfg "\\fs01\deploy\policies\cis-l1.inf" /quiet Step 4: FirstLogonCommands
:: -- 9. Stage Chocolatey from internal MSI, then prime from internal repo
msiexec /i \\fs01\deploy\msi\choco-2.4.msi /qn /norestart
choco install 7zip --source=\\fs01\deploy\choco-repo --confirm
:: -- 10. Install the VPN client MSI
msiexec /i \\fs01\deploy\msi\vpnclient-7.2.msi /qn /norestart
:: -- 11. Import the corporate Wi-Fi profile (interface=* for locale safety)
netsh wlan add profile filename="\\fs01\deploy\wifi\corp.xml" interface="*" Step 5: Verify
After the deployment finishes and OOBE completes, the validation steps:
certutil -store Root | findstr /i "corp"
dism /online /get-capabilities | findstr OpenSSH
gpresult /h C:\temp\gpresult.html
slmgr /dlv :: KMS activation against kms01
netsh wlan show profiles :: corp.xml listed
sc query sshd :: SSH service exists, running