Salt on Windows

Salt on Windows. Production patterns, not theory.

Practical guidance for the people actually managing Windows fleets with Salt. Install the minion, navigate the quirks, manage software fleet-wide with winrepo. One page. Distilled from production.

v1.1 · Updated 2026-05-09

Why this page exists. Salt's official Windows docs are mostly auto-generated module references and one giant winrepo doc. Getting started, the gotchas, real production patterns — those gaps are what we're filling here. Each section links to the official docs for the full reference, but the practical guidance lives on this page.

Install the Salt minion on Windows.

Get the minion onto a Windows box, point it at your master, accept its key. Day-one task. Open PowerShell as Administrator on the Windows box. The whole thing takes about five minutes.

01

Download the MSI

Get the Windows Salt minion installer. Pick the version that matches your masters (RaaS 8.18 supports Salt 3006.x and 3007.x).

# official release portal — pick AMD64 or x86
https://repo.saltproject.io/salt/py3/windows/

# or via PowerShell to a known path
$url = "https://repo.saltproject.io/salt/py3/windows/" +
       "Salt-Minion-3007.1-Py3-AMD64-Setup.exe"
Invoke-WebRequest -Uri $url `
  -OutFile C:\Windows\Temp\salt-minion.exe

Air-gapped? Get the MSI from your offline bundle, copy via SMB / SCP / USB, skip the URL.

02

Install silently

The installer takes /master= and /minion-name= as command-line args. Bake them in at install time — no config file editing afterwards.

# silent install with master + minion ID set
C:\Windows\Temp\salt-minion.exe /S `
  /master=salt-master.example.com `
  /minion-name=<your minion id>

/S = silent. Drop it for an interactive install. Multiple masters: /master=master1,master2.

03

Start the service

The MSI registers salt-minion as a Windows service. First start may need a manual nudge.

Start-Service salt-minion
Get-Service salt-minion        # should be Running
Set-Service salt-minion `
  -StartupType Automatic        # persists across reboots
04

Verify on the master

Switch to your Salt master. The new minion's key should appear in the pending list. Accept it.

# on the master
sudo salt-key -L           # list pending
sudo salt-key -a <your minion id>

Smoke test

sudo salt '<your minion id>' test.ping

→ Returns True. If not, see troubleshooting below.

05

First useful command

Confirm everything works end-to-end with a real query.

# grain inspection — what does Salt know about this box?
sudo salt '<your minion id>' grains.items

# installed software (uses winrepo if configured)
sudo salt '<your minion id>' pkg.list_pkgs

Field-tested patterns

From production

Bake the minion into your golden image

Don't install on every new VM. Install once into your Windows template. New VMs come up with the minion already there — first boot picks up the master config from a sysprep specialize step or unattend file.

Trick: ship the template with master: salt-master.example.com as a CNAME. Cuts over with DNS, no reinstall, no re-key. The blue/green pattern from the install guide works the same way for minions as it does for masters.

From production

salt-cloud (saltify driver) for fleet rollouts

If the boxes already exist and you can SMB to them, push the MSI from the master via salt-cloud with the saltify driver. No need to log into each Windows host.

# /etc/salt/cloud.profiles.d/win.conf
win-server-2022:
  driver: saltify
  ssh_host: win01.example.com
  win_username: Administrator
  win_password: <encrypt-with-pillar-gpg>
  master: salt-master.example.com
  win_installer: /etc/salt/cloud.deploy.d/Salt-Minion-3007.1-AMD64-Setup.exe

From production

Pick a sensible minion ID convention

Default is the hostname. That's fine for one-off boxes. For fleets, use something role-aware: app-web01, db-prod-01. Lets you target with globs (salt 'app-*') and grains (os_family:Windows) cleanly. Don't change minion IDs after the fact — Salt treats it as a new minion and you lose history.

If test.ping doesn't return

Firewall. The minion needs to reach the master on TCP 4505 and 4506. Check Windows Firewall on the minion AND any network firewall between minion and master. Test-NetConnection -ComputerName salt-master.example.com -Port 4505 tells you if you can reach.

Key not accepted. sudo salt-key -L on the master — is the minion still in Unaccepted Keys? Accept it. If it's Rejected, delete and re-register.

Wrong master in config. On the Windows box, check C:\ProgramData\Salt Project\Salt\conf\minion. The master: line should match your master FQDN. Edit, then Restart-Service salt-minion.

Time skew. Salt requires the minion clock to be within a few minutes of the master. If the Windows box is on stale time, w32tm /resync usually fixes it.

Still nothing? Stop the service, run the minion in foreground debug: salt-minion -l debug. The output tells you exactly what's failing — DNS, TCP, key auth, all of it.

Windows quirks.

Salt was written for Unix first. Windows came later. These are the corners where that history shows. Read this once, save yourself a debugging session.

The mental model. When Salt has to choose between Unix idioms and Windows reality, it usually picks Unix and ignores the rest. Most "weird Windows behavior" is Salt doing nothing where you expected it to do something.

QUIRK 01

group on file states does nothing

Windows files do have a "primary group" property, but it's basically vestigial — leftover from Services for Unix / NFS compatibility. Salt knows this and silently ignores the group parameter on file states. No error, no warning that matters, just nothing.

If you actually want to set the primary group (you probably don't), use file.get_pgid, file.get_pgroup, or the pgroup arg on file.chown.

Skip group: in your file states on Windows. Use ACLs via win_dacl if you need real permissions.

QUIRK 02

Casing — Windows preserves it, Salt expects it

Windows is case-insensitive but case-preserving — Administrator and administrator work for login, but the system always returns the case it was created with. Salt assumes case-sensitive everywhere, so a state with user: administrator can fail in bizarre ways when the actual account is Administrator.

Pretend Windows is case-sensitive. Match the case of the real account name. Always.

QUIRK 03

Username forms — only the raw value works

A Windows user can be referred to three ways: alice, DOMAIN\alice, [email protected]. They all log into the same account. Salt only understands the raw username — no domain, no host.

Pass DOMAIN\alice to most module functions and Salt will probably do the wrong thing or throw a confusing error. UPN form ([email protected]) is even worse.

Always use the raw username (alice) in correct case. Domain-qualify only in group.present.addusers with double-backslash: DOMAIN\\alice.

QUIRK 04

The None group needs quotes

Every Windows system has a built-in group literally named None — the default primary group for non-domain accounts. Python uses None as its null value. When Salt sees None in your state, it interprets it as "no group" instead of "the group named None."

Wrap it in nested quotes: salt '*' file.chpgrp C:\path\to\file "'None'"

QUIRK 05

Symlink loops are fatal on Windows

If Salt detects a symlink loop or more than 64 levels of symlinks while traversing a path, it raises an error — always. On Unix some functions tolerate this. On Windows they don't.

Rare in practice unless you've got something exotic like nested junction points or DFS namespaces. If your state suddenly fails with a symlink error, check the path for loops.

Don't create symlink loops. To debug: (Get-Item path).LinkType and fsutil reparsepoint query.

QUIRK 06

Path separators — backslash vs forward-slash

Not in the official quirks list but bites everyone. YAML treats backslash as an escape character in double-quoted strings. "C:\path\to\file" has bugs you can't see. Salt itself accepts forward slashes on Windows everywhere — they get converted under the hood.

Three options that work: bare path C:\path\to\file, single-quoted 'C:\path\to\file', or forward slashes C:/path/to/file. Don't double-quote unless you double-escape.

The compounding rule. If a state behaves weirdly on Windows and you're sure the YAML is right, the issue is probably one of these six. Cross-reference before you blame the module.

winrepo — the Windows package manager.

Like apt on Linux. Like yum on RHEL. Salt for Windows software, fleet-wide. Install, upgrade, remove with one command — no logging into individual boxes.

The mental model

winrepo isn't magic. It's a Git repo of YAML files. Each file describes how to install a piece of Windows software silently. Salt reads the files, builds a database, and uses that database when you say pkg.install firefox.

Three pieces: a Git repo of package definitions (the community one is salt-winrepo-ng), a local cache on each minion (built from the repo), and the installer URLs the package definitions point at (vendor download URLs, your own SMB share, or anywhere reachable from the minion).

No dependency resolution. Unlike apt, winrepo won't auto-install dependencies. If X requires Y, you install Y first. Manage the order yourself with require: in your states.

Quickstart — four steps

Run all four on the master. Steps 03 and 04 also work in masterless mode — swap salt for salt-call --local on the minion.

01

Install the Git library (optional)

If your master doesn't have it: GitPython or pygit2. Salt uses one of these to clone winrepo on the master.

sudo salt-pip install GitPython
# OR
sudo salt-pip install pygit2
02

Clone winrepo to the master

Pulls salt-winrepo-ng into /srv/salt/win/repo-ng/ by default.

sudo salt-run winrepo.update_git_repos

After this, you have hundreds of package definitions ready to use.

03

Build package database on minions

Each Windows minion parses the YAML and builds a local database. Slow first time, fast after.

sudo salt -G 'os:windows' pkg.refresh_db

Output: success: 301, failed: 0 or similar. If anything fails, you've got malformed YAML.

04

Install something

Latest version of Firefox on every Windows minion, in one command:

sudo salt -G 'os:windows' pkg.install firefox_x64

Pin a version: pkg.install firefox_x64 version=74.0.

Day-2 operations

Once winrepo is set up, these are the four commands you'll actually run.

List installed

What's on the box?

Returns short names for software Salt manages, full names for everything else.

salt 'win01' pkg.list_pkgs

List versions

What can I install?

All versions of a package available in winrepo.

salt 'win01' pkg.list_available firefox_x64

Install

Install or upgrade

Latest version, or pin to a specific one with version=.

salt 'win01' pkg.install firefox_x64
salt 'win01' pkg.install firefox_x64 version=74.0

Remove

Uninstall

Runs the uninstaller defined in the package definition.

salt 'win01' pkg.remove firefox_x64

Write your own package definition

The community winrepo covers hundreds of packages. For internal software (or anything missing), you write the YAML yourself. Three things you need: the full name as Add/Remove Programs shows it, the exact version, and the silent-install command-line switches.

Here's the canonical Firefox example, annotated:

# /srv/salt/win/repo-ng/firefox.sls
firefox_x64:                   # short name — used in pkg.install
  '74.0':                      # version (always quoted — preserves trailing zeros)
    full_name: Mozilla Firefox 74.0 (x64 en-US)
    installer: 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/74.0/win64/en-US/Firefox%20Setup%2074.0.exe'
    install_flags: '/S'        # silent install switch
    uninstaller: '%ProgramFiles(x86)%/Mozilla Firefox/uninstall/helper.exe'
    uninstall_flags: '/S'

  '73.0.1':                    # multiple versions in one file
    full_name: Mozilla Firefox 73.0.1 (x64 en-US)
    installer: 'https://download-installer.cdn.mozilla.net/pub/firefox/releases/73.0.1/win64/en-US/Firefox%20Setup%2073.0.1.exe'
    install_flags: '/S'
    uninstaller: '%ProgramFiles(x86)%/Mozilla Firefox/uninstall/helper.exe'
    uninstall_flags: '/S'

Three rules

Common parameters

After writing your .sls file, run pkg.refresh_db again to pick it up.

Field-tested patterns

From production

Host the installers yourself

Pointing installer at vendor URLs (download.mozilla.org, etc.) works for testing. In production it's fragile — vendors move URLs, change naming conventions, and rate-limit. Mirror installers to your own SMB share or salt:// file root. Cuts download time, removes a third-party dependency, gives you version control.

installer: 'salt://win/installers/firefox/Firefox Setup 74.0.exe'
cache_file: 'salt://win/installers/firefox/Firefox Setup 74.0.exe'
source_hash: 'sha256=<your hash>'

From production

Multi-master rsync for installers

If you run multiple Salt masters (the 3-server stack), don't store installers in Git — they bloat the repo. Store them on master 1's filesystem and rsync to the others nightly. Salt's file_roots will serve them via salt:// the same way regardless.

From production

Version pinning isn't optional

"Latest" sounds great until a vendor pushes a breaking release on a Tuesday morning. Always pin versions in production. Pin via state files (pkg.installed: name: firefox_x64, version: 74.0). Bump in pillar when you've tested. Roll back by changing pillar.

Coming next.

Patterns from Saltify's production deployments that don't exist anywhere in the official docs. Each becomes its own section on this page when ready.

Active Directory join — domain-join with grain detection, idempotent retries, post-join state targeting
IIS deployment — Web-Server role, app pools, sites, certificates via win_servermanager + win_iis
Service management — installing, starting, watching, recovery options, dependency chains
Scheduled tasks — Windows Task Scheduler from Salt, with examples that actually run
PowerShell deep divecmd.run, cmd.script, idempotent unless: patterns, returning data via Salt
Hardening & compliance — registry tweaks, security baselines, drift detection

Built from salt/doc/topics/windows/ (Apache 2.0) + Saltify field experience.

Want a specific topic faster?

Tell us which Windows + Salt scenario you're stuck on. We'll move it up the queue, or write something custom.

[email protected]