Docs
← Back to the builder

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.

Isolation postures and what each implies for unattend
PostureLAN accessInternet accessImplication for unattend
True air-gapNone outside the secure enclaveNoneEverything 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 LANInternal fileservers, DNS, WSUS, internal CANone or proxied with deny-by-defaultSame as above plus internal WSUS for OS patching and an internal root CA for certificate trust. The common enterprise case.
Restricted-internetYesOnly allow-listed hosts via proxyTreat 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).

Why the regular doc snippets do not work here. The default 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
Internal links. If you have not yet sealed the image for capture, you may also want the sysprep workflow guide (Workflow B for first-build, Workflow D for refresh) and the driver injection guide if your hardware needs OEM drivers added to the WIM. For ad-hoc snippets that fit elsewhere in this sequence, see first-run snippets, and for the cmd / PowerShell / VBS execution semantics referenced throughout, see running PowerShell, cmd & VBS.