Salt Master
Overview
Salt is an event-driven configuration management and remote execution platform. A Salt master publishes commands and state to Salt minions and collects their returns. You describe how a fleet of machines should be configured, apply that description, and run ad hoc commands across the whole fleet in seconds.
This SecureAnyCloud image is a Salt master appliance that ships working. A local minion is already connected to the master and accepted, and a small state and pillar tree is in place. salt '*' test.ping and salt '*' state.apply both do something real the moment the VM boots, so you are not left with a bare install to wire up.
The image ships the free and open-source, Apache-2.0 licensed Salt 3008.2, the newest release of the current long-term-support branch. It is installed from the Salt Project's own packages and pinned by content hash, tied to an exact upstream commit. This guide covers deploying and configuring it on Ubuntu 24.04 on Azure.
What you get:
- Salt 3008.2 master and minion, from the Salt Project's own Debian packages, verified by SHA-256 content hash
- A local minion already accepted on the master, so the appliance works end to end on first boot
- A small state and pillar tree at
/srv/saltand/srv/pillarthat demonstrates the full render, publish, execute and return cycle - No key material in the image at all. The master key pair, the minion key pair and the accepted minion list are generated on your VM at its first boot
- The ZeroMQ transport bound to loopback, so SSH is the only routable listener on a freshly launched VM
auto_acceptoff, no autosign of any kind, andopen_modeoff, enforced by a guard that stops the master from starting if any of it is weakened- A fingerprint helper that makes Salt's own key verification workflow work (see Adding a remote minion)
- A full dependency licence inventory at
/usr/share/saltstack/dependency-licences.txt - 24/7 SecureAnyCloud support
Security posture, stated plainly
A Salt master is a control plane. Anything that can command it can command every machine it manages, as root. Internet-exposed Salt masters have been compromised at scale: CVE-2020-11651 and CVE-2020-11652 gave unauthenticated root remote code execution on the master and were exploited in the wild.
Salt's trust model is minion key based, not password based. There is no password on the wire, only key exchange and whatever the master has been told to accept. This image is built around that model.
What SecureAnyCloud does for you, and what remains yours, is set out in /usr/share/saltstack/SECURITY-RESPONSIBILITIES on the VM and in Access control below. Securing master-to-minion trust and the network exposure of the Salt ports is your responsibility as the operator of this VM.
Prerequisites
- An Azure subscription with permission to create virtual machines
- An SSH key pair for VM access
- A VM size of
Standard_B2s(2 vCPU, 4 GiB) or larger
Standard_B2s is a genuine recommendation, not a squeeze. The master idles at roughly 200 MB and the local minion at under 100 MB, and no swap is created at any point. It is comfortable for a master with up to roughly 50 to 100 minions at the default worker_threads of 5. Beyond that, move to a larger size and raise worker_threads in /etc/salt/master.d/.
Ports this appliance serves
- Port 22, TCP: SSH, how you administer the appliance. Reachable on a fresh VM.
- Port 4505, TCP: Salt publish bus (master to minions). Not reachable, bound to 127.0.0.1.
- Port 4506, TCP: Salt request bus (minions to master). Not reachable, bound to 127.0.0.1.
The offer advertises port 22 only, because that is the only port the appliance serves on a fresh VM. Salt's 4505 and 4506 are bound to 127.0.0.1. The local minion reaches the master over loopback, and nothing on the network can reach the transport at all. Opening it up for remote minions is a deliberate opt-in, covered in Adding a remote minion.
There is no salt-api and no web server on this image. Salt's HTTP netapi is the component through which CVE-2021-3148 and CVE-2021-3197 are reached. The appliance's control plane is the salt command line over SSH, so the netapi is not installed at all.
Launching the VM
From the Azure Portal
- Search the Azure Marketplace for Salt Master on Ubuntu 24.04 LTS by SecureAnyCloud.
- Click Create.
- Choose your subscription, resource group and region.
- Set the VM size to
Standard_B2sor larger. - Choose SSH public key authentication and supply your public key.
- Under Inbound port rules, allow SSH (22) only.
- Review and create the VM.
From the Azure CLI
az group create --name salt-master-rg --location eastus
az vm image terms accept --urn sac1647283583153:saltstack-ubuntu-24-04:default:latest
az vm create \
--resource-group salt-master-rg \
--name salt-master \
--image sac1647283583153:saltstack-ubuntu-24-04:default:latest \
--size Standard_B2s \
--admin-username azureuser \
--generate-ssh-keys \
--public-ip-sku StandardOnly port 22 needs to be open. Do not open 4505 or 4506 until you have read Adding a remote minion and decided that you need them.
What happens on first boot
The shipped image contains no key material whatsoever: no master key pair, no minion key pair, no accepted or pending minion key, and no minion id. This is deliberate. A baked-in master private key would let anyone holding the image impersonate every customer's master. A baked-in accepted minion key would be a back door into every customer's master.
So all key material is generated on your machine, at your first boot, by saltstack-firstboot.service. On first boot, the appliance:
- Verifies that the image arrived with no key of any kind, and refuses to continue if it did not
- Sets this VM's minion id from its hostname
- Starts the master, which generates this VM's own master key pair
- Confirms that the transport came up on
127.0.0.1and that nothing unexpected is listening - Pins the local minion to this master's public key (
master_finger), so the minion will not talk to any other master - Starts the local minion, which generates this VM's own minion key pair
- Accepts the local minion's key only after confirming it is byte for byte this machine's own public key, never simply whatever key arrived first
- Proves that the round trip works, then proves the two negatives: a wrong master fingerprint is refused, and a freshly generated stranger key gets nothing at all
- Writes
/root/salt-credentials.txtand its sentinel
First boot typically completes within a minute. Check that it finished:
sudo systemctl is-active saltstack-firstboot.service
sudo test -f /var/lib/sac/data/saltstack-firstboot.done && echo "first boot complete"The output looks like this:
active first boot completeRetrieving your identity material
Salt does not authenticate with passwords. First boot records identity material for you instead: the fingerprints of the key pairs this machine generated. They exist nowhere else, and you use them to verify identity when you add a minion.
sudo cat /root/salt-credentials.txtThe file is 0600 root:root. Among its explanatory notes, it contains:
SALT_MASTER_KEY_FINGERPRINT=<SALT_MASTER_KEY_FINGERPRINT> SALT_MINION_ID=<SALT_MINION_ID> SALT_MINION_KEY_FINGERPRINT=<SALT_MINION_KEY_FINGERPRINT> SALT_MASTER_INTERFACE=127.0.0.1 SALT_AUTO_ACCEPT=FalseRunning your first commands
The local minion is already accepted, so everything below works immediately. List the minion keys:
sudo salt-key -LThe output shows one accepted key, which is this machine, and nothing pending:
Accepted Keys: <SALT_MINION_ID> Denied Keys: Unaccepted Keys: Rejected Keys:Now run the round trip. test.ping asks the minion to answer. It is the simplest proof that the publish and return buses both work.
sudo salt '*' test.ping<SALT_MINION_ID>: TrueRemote execution takes arguments and returns real data:
sudo salt '*' grains.item os osrelease saltversion num_cpus
sudo salt '*' cmd.run 'uptime'
sudo salt '*' disk.usagegrains are facts that the minion reports about itself, such as the operating system, CPU count and Salt version. You target and template on them.
Applying state
State is the configuration management half of Salt. You describe the desired end state, Salt makes it so, and running it again changes nothing. The appliance ships a small, harmless, idempotent example so that you can see the whole cycle:
cat /srv/salt/top.sls
cat /srv/salt/sac/welcome.slstop.sls assigns sac.welcome to every minion. Always preview first. test=True shows what would change without changing it:
sudo salt '*' state.apply test=TrueThen apply it for real:
sudo salt '*' state.applyThe output ends with a summary like this:
Summary for <SALT_MINION_ID> ------------ Succeeded: 1 Failed: 0 ------------ Total states run: 1The state wrote a file, rendered on the master from the minion's own grains and from pillar data:
cat /var/lib/sac/config/sac-salt-demo.txtPillar is the master's private, per-minion data store. It is rendered on the master and delivered only to the minions it is targeted at. This is why secrets belong in pillar, and why /srv/pillar is mode 0750.
sudo salt '*' pillar.itemsSalt 3008 redacts pillar values in returns, showing ********** rather than the value, so that pillar data does not end up in job returns and logs. The value itself is delivered to the minion normally. The file that the state above wrote contains the greeting in full.
When you are ready, replace /srv/salt/top.sls and /srv/salt/sac/welcome.sls with your own state tree.
Access control
auto_accept is off, and the master will not start without it
The most dangerous setting on a Salt master is auto_accept. With it on, the master trusts any key that introduces itself. On a reachable master, that is unauthenticated remote code execution across your whole estate.
On this appliance the setting is False. There is no autosign_file and no autosign_grains_dir, and open_mode, which disables authentication outright, is False.
These settings are enforced, not just written into a config file. /usr/local/sbin/salt-preflight.sh reads the effective merged configuration through Salt's own loader. It runs as ExecStartPre on salt-master.service, so a master that has been edited into an unsafe state does not start at all:
sudo salt-preflight.sh --master && echo "configuration accepted"Verifying the listening surface yourself
You do not need to take our word for it. Read the listening sockets from the kernel:
ss -lntuH sudo salt-socket-audit.sh --expect-appThe audit enumerates every listening socket and refuses if any routable listener exists outside the allowed set (TCP 22 and the DHCP client on UDP 68). It also confirms that the Salt transport is on loopback.
Adding a remote minion
This is the security decision that matters, and it is yours. Every minion you accept is a machine over which you grant root-equivalent control.
Step 1 - Open the transport. Edit /etc/salt/master.d/10-sac-network.conf, change interface: 127.0.0.1 to interface: 0.0.0.0, and restart:
sudo systemctl restart salt-masterStep 2 - Allow the ports, narrowly. In the Azure portal or CLI, allow inbound TCP 4505 and 4506 from the narrowest source that works. Use the subnet or address range your minions live in, never Any. Do not expose these ports to the public internet. Prefer a private VNet, a peered network or a VPN.
Step 3 - Point the minion at this master. On the minion machine, install Salt, set master: <your-master-ip> in /etc/salt/minion, and start salt-minion. Its key arrives at your master as unaccepted.
Step 4 - Verify the fingerprint before you accept. On the minion, run:
sudo salt-call --local key.fingerOn this master, run the next command with the id that the minion registered under. sudo salt-key -L lists it:
sudo salt-fingerprint.sh <minion-id>Accept only if the two values are identical:
sudo salt-key -a <minion-id>Use salt-fingerprint.sh, not salt-key -f. This is a trap in Salt itself, not a SecureAnyCloud quirk.
Salt's pem_finger hashes only the key body when it is given a file path, which is what salt-call key.finger does on the minion. It hashes the whole PEM blob when it is given raw bytes, which is what salt-key -f and salt-key -F do on the master. The two commands therefore print different strings for the same key, every time, for every key.
An operator who follows the widely documented salt-key -f comparison sees a mismatch on a perfectly good minion. They may conclude that the check is meaningless and start accepting keys blind. That is exactly the risk that auto_accept: False exists to prevent. salt-fingerprint.sh prints the same form that the minion does, so the comparison is real.
Do not turn auto_accept on, and do not configure an autosign file or autosign grains directory. A grain is self-reported by the minion, so it is not a secret and cannot be an authentication factor.
Day-to-day operation
Services
sudo systemctl status salt-master
sudo systemctl status salt-minionBoth are enabled and start automatically on reboot.
Logs
sudo journalctl -u salt-master -n 50 --no-pager sudo journalctl -u saltstack-firstboot -n 50 --no-pagerSalt also writes to /var/log/salt/.
Backup
Back up two things: your state and pillar tree, and the master's key material and accepted keys:
sudo tar czf /root/salt-backup.tar.gz /srv/salt /srv/pillar /etc/salt/etc/salt/pki contains this master's private key. Treat that archive as a secret, because anyone who holds it can impersonate your master.
Operating system updates
Ubuntu security updates are applied unattended. Salt itself is pinned at the version that this image was built and verified against:
cat /etc/apt/preferences.d/salt-pin.prefThe pin stops an unattended run from replacing a verified artifact with an unverified one. To move to a newer Salt deliberately, remove that file, then upgrade and restart the services. Read the Salt release notes first, because SecureAnyCloud's proofs apply to the version SecureAnyCloud built.
Provenance and licences
cat /usr/share/saltstack/source-provenance.txt
cat /usr/share/saltstack/SECURITY-RESPONSIBILITIES head -20 /usr/share/saltstack/dependency-licences.txtEvery Python distribution in both of the image's Python environments is enumerated with its licence in /usr/share/saltstack/dependency-licences.txt. An executable gate produces the inventory and fails the build on any network-copyleft, source-available or non-commercial licence.
Troubleshooting
salt '*' test.ping returns "Minion did not return". The minion is not running or its key is not accepted. Check sudo systemctl status salt-minion and sudo salt-key -L.
A remote minion's key never appears. The transport is still on loopback, or the network security group is closed. Check interface: in /etc/salt/master.d/10-sac-network.conf, check that you restarted salt-master, and check that 4505 and 4506 are allowed from the minion's address.
salt-master will not start. Run the guard by hand to see which invariant it refuses on:
sudo salt-preflight.sh --masterIt refuses on auto_accept: True, open_mode: True, any autosign configuration, permissive_pki_access: True, key material readable beyond the master's own account, and any file in /etc/salt/master.d/ that is not valid YAML.
The last check matters. Salt does not abort on a malformed drop-in file. It silently discards the whole file, taking every setting in it along. The guard refuses to start rather than let your edit vanish unnoticed.
The fingerprints do not match when adding a minion. You are almost certainly comparing salt-key -f on the master with salt-call key.finger on the minion, which never match. Use sudo salt-fingerprint.sh <minion-id> on the master instead.
First boot did not complete. Inspect the journal directly:
sudo journalctl -u saltstack-firstboot -n 80 --no-pagerSupport
SecureAnyCloud provides 24/7 support for this image. Contact sales@secureanycloud.com.
Salt is an independent open-source project. SecureAnyCloud produces this image, which is not affiliated with, endorsed by, or sponsored by the Salt Project or Broadcom. It ships the free and open-source, Apache-2.0 licensed software, unmodified.
Deploy on Azure
Launch Salt Master on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com