Salt (also known as SaltStack) is an open source configuration management and remote execution tool. A central master sends commands and desired-state definitions to agents called minions over an encrypted ZeroMQ channel, so you can run a command on hundreds of servers at once or keep them all in a known configuration. In this tutorial you will install a Salt master and a minion on Ubuntu 24.04, authenticate the minion, run remote commands, and deploy Nginx with a state file that reads its settings from pillar data.
Prerequisites
To follow this tutorial, you will need:
- Two servers running Ubuntu 24.04 LTS, for example two CubePath VPS instances:
- salt-master: at least 2 GB of RAM. Its IP address is referred to as
your_master_ip. - web1: the managed server (minion). Its IP address is referred to as
your_minion_ip.
- salt-master: at least 2 GB of RAM. Its IP address is referred to as
- A non-root user with
sudoprivileges on both servers. - UFW enabled on both servers with SSH allowed.
The minion only makes outbound connections to the master, so it does not need any Salt port open.
Step 1 - Adding the Salt Project repository
Salt packages are published by the Salt Project in its own APT repository. Run these commands on both servers.
Download the repository signing key:
sudo mkdir -p /etc/apt/keyrings
curl -fsSL https://packages.broadcom.com/artifactory/api/security/keypair/SaltProjectKey/public | sudo tee /etc/apt/keyrings/salt-archive-keyring.pgp > /dev/null
Download the official repository definition, which already references that keyring:
curl -fsSL https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.sources | sudo tee /etc/apt/sources.list.d/salt.sources > /dev/null
Refresh the package index and confirm that APT sees the Salt packages:
sudo apt update
apt policy salt-common
salt-common:
Installed: (none)
Candidate: 3008.2
Version table:
3008.2 500
500 https://packages.broadcom.com/artifactory/saltproject-deb stable/main amd64 Packages
The candidate version may be newer when you read this. The master and minions should run the same release, so upgrade them together.
Step 2 - Installing the Salt master
On salt-master, install the master package:
sudo apt install salt-master
Enable and start the service:
sudo systemctl enable --now salt-master
sudo systemctl status salt-master
● salt-master.service - The Salt Master Server
Loaded: loaded (/usr/lib/systemd/system/salt-master.service; enabled; preset: enabled)
Active: active (running)
Minions connect to the master on TCP ports 4505 (publish) and 4506 (return). Allow them only from the minion's address:
sudo ufw allow from your_minion_ip to any port 4505:4506 proto tcp
When you add more minions, repeat the rule for each address or allow your private network range instead.
Step 3 - Installing and configuring the minion
On web1, install the minion package:
sudo apt install salt-minion
Tell the minion where the master is and give it a stable ID. Drop-in files in /etc/salt/minion.d/ are easier to maintain than editing the main configuration file:
sudo nano /etc/salt/minion.d/master.conf
master: your_master_ip
id: web1
Enable the minion and restart it so it reads the new configuration:
sudo systemctl enable salt-minion
sudo systemctl restart salt-minion
On startup the minion generates a key pair and sends its public key to the master, where it waits for approval.
Step 4 - Accepting the minion key
Salt uses public key authentication: the master does not send commands to a minion until you accept its key. On salt-master, list the keys:
sudo salt-key -L
Accepted Keys:
Denied Keys:
Unaccepted Keys:
web1
Rejected Keys:
Before accepting, make sure the key really belongs to your server by comparing fingerprints. On salt-master:
sudo salt-key -f web1
On web1:
sudo salt-call --local key.finger
If both fingerprints match, accept the key on salt-master:
sudo salt-key -a web1
Answer Y at the prompt. Now test the connection:
sudo salt '*' test.ping
web1:
True
WarningAvoid
auto_accept: Truein the master configuration. It lets any machine that can reach ports 4505 and 4506 register itself as a minion.
Step 5 - Running remote commands
With the key accepted you can execute modules on any minion. The first argument is the target; '*' means all minions, and 'web*' would match every minion whose ID starts with web.
Run a shell command:
sudo salt 'web1' cmd.run 'uptime'
web1:
10:42:17 up 2 days, 3:11, 0 user, load average: 0.00, 0.01, 0.00
Query grains, the static facts each minion reports about itself:
sudo salt 'web1' grains.item os osrelease num_cpus
web1:
----------
num_cpus:
2
os:
Ubuntu
osrelease:
24.04
Grains can also be used for targeting. For example, sudo salt -G 'os:Ubuntu' test.ping targets every Ubuntu minion.
Remote execution is handy for one-off tasks, but it does not describe how a server should look. For that, Salt uses states.
Step 6 - Storing configuration data in pillar
Pillar is data that the master sends only to the minions you target, which makes it the right place for per-host settings and secrets. The default pillar directory is /srv/pillar. On salt-master, create it:
sudo mkdir -p /srv/pillar
Create the pillar data file:
sudo nano /srv/pillar/site.sls
site:
title: Managed by Salt
server_name: example.com
Create the pillar top file, which assigns data to minions:
sudo nano /srv/pillar/top.sls
base:
'web*':
- site
Refresh pillar on the minions and check what web1 receives:
sudo salt '*' saltutil.refresh_pillar
sudo salt 'web1' pillar.items
web1:
----------
site:
----------
server_name:
example.com
title:
Managed by Salt
Step 7 - Writing a state for Nginx
States are YAML files (.sls) that declare the desired configuration. Salt's file server reads them from /srv/salt by default. On salt-master, create a directory for the Nginx state:
sudo mkdir -p /srv/salt/nginx/files
Create the state file:
sudo nano /srv/salt/nginx/init.sls
nginx:
pkg.installed: []
service.running:
- enable: True
- require:
- pkg: nginx
/var/www/html/index.html:
file.managed:
- source: salt://nginx/files/index.html.jinja
- template: jinja
- user: root
- group: root
- mode: '0644'
- require:
- pkg: nginx
This declares three things: the nginx package is installed, the service is running and enabled at boot, and the home page is rendered from a template. The require lines guarantee the order.
Create the template. It combines pillar data with a grain:
sudo nano /srv/salt/nginx/files/index.html.jinja
<!DOCTYPE html>
<html>
<head><title>{{ pillar['site']['title'] }}</title></head>
<body>
<h1>{{ pillar['site']['title'] }}</h1>
<p>Served by {{ grains['id'] }} running {{ grains['osfinger'] }}.</p>
</body>
</html>
Finally, create the state top file, which maps states to minions:
sudo nano /srv/salt/top.sls
base:
'web*':
- nginx
Step 8 - Applying the state
Always preview changes first. With test=True, Salt reports what it would change without touching the minion:
sudo salt 'web1' state.apply test=True
Each state that would change something is listed with Result: None and a comment such as The following packages would be installed/updated: nginx. Nothing is installed yet.
Apply the state for real:
sudo salt 'web1' state.apply
Summary for web1
------------
Succeeded: 3 (changed=3)
Failed: 0
------------
Total states run: 3
Run the same command again and you will see changed=0: states are idempotent, so Salt only acts when the server drifts from the declared configuration.
On web1, allow HTTP traffic so you can check the result:
sudo ufw allow 'Nginx HTTP'
From your workstation or from the master, request the page:
curl http://your_minion_ip
<!DOCTYPE html>
<html>
<head><title>Managed by Salt</title></head>
<body>
<h1>Managed by Salt</h1>
<p>Served by web1 running Ubuntu-24.04.</p>
</body>
</html>
To change the title on every web server, edit /srv/pillar/site.sls and run sudo salt 'web*' state.apply again.
Troubleshooting
- The minion key never appears in
salt-key -L: check the minion log withsudo journalctl -u salt-minion -n 50and confirm thatweb1can reach the master withnc -zv your_master_ip 4506. A missing UFW rule on the master is the usual cause. Minion did not return. [No response]: the minion is stopped or cannot reach port 4505. Runsudo systemctl status salt-minionon the minion.- You rebuilt a minion and it no longer authenticates: the old key is still accepted on the master. Delete it with
sudo salt-key -d web1, restartsalt-minionon the server, and accept the new key. - Rendering errors in a state: run
sudo salt-call state.show_sls nginxon the minion to see the rendered YAML and the exact line that fails.
Conclusion
You installed a Salt master and minion on Ubuntu 24.04, authenticated the minion with key fingerprints, ran remote commands, and used a state with pillar data to deploy a templated Nginx site. Next, you can split states into reusable formulas, store secrets in pillar per environment, or schedule state.apply with the Salt scheduler so drift is corrected automatically.
