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.
  • A non-root user with sudo privileges 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

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 with sudo journalctl -u salt-minion -n 50 and confirm that web1 can reach the master with nc -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. Run sudo systemctl status salt-minion on 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, restart salt-minion on the server, and accept the new key.
  • Rendering errors in a state: run sudo salt-call state.show_sls nginx on 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.