
September 11, 2026
14 min read
By Kokil Thapa | Last reviewed: September 2026
SaltStack: Remote Execution and Config is how you stop SSH-ing into forty Ubuntu boxes to patch PHP-FPM or fix permissions. Salt gives you a master that pushes commands and declarative state files to minions over an encrypted bus. You define what each server should look like once. Salt keeps it that way. This guide covers architecture, remote execution, state files, and production patterns I've seen on real Linux fleets — including sites that share a Git-based server config workflow.
salt '*' cmd.run for ad-hoc commands and applies YAML state files (SLS) so minions converge to declared packages, files, and services. Pillar stores secrets; the top file maps states to targets.What Is SaltStack Remote Execution and Config Management?
Salt is an open-source automation platform from the Salt Project. It solves two problems at once. First, remote execution — run a shell command on one minion or ten thousand in parallel. Second, configuration management — describe desired server state in YAML and let Salt enforce it continuously.
The core pieces are straightforward. A master holds your state tree, pillar data, and job queue. Each managed host runs a minion agent that connects outbound to the master. Communication uses ZeroMQ on ports 4505 (publish) and 4506 (request). Minions authenticate with keys you accept on the master.
Salt fits teams that outgrow manual SSH and one-off cron scripts. It sits alongside tools covered in our Ansible vs Terraform comparison. Terraform provisions infrastructure. Salt (like Ansible) configures what already exists. On fleets I maintain with Linux system administration work, Salt shines when you need fast parallel execution and event-driven reactions.
Core Components You Must Know
- Grains — static facts about a minion (OS, hostname, IP). Available in Jinja templates inside states.
- Pillar — encrypted or restricted key-value data (DB passwords, API keys). Never stored in Git plain text.
- States (SLS) — YAML files declaring packages, files, services, and commands in desired-state form.
- Top file — maps which states apply to which minions via target expressions.
- Reactors — event handlers that trigger states or commands when something happens on the bus.
How Do You Install and Bootstrap a Salt Master-Minion Fleet?
Start on Ubuntu 24.04 LTS — the stack I use on most production servers. Install the master on one dedicated host. Install minions everywhere you want control. Official packages live in the Salt Project repository documented at docs.saltproject.io.
Master Installation
# Add Salt repo and install master (Ubuntu 24.04 example)
curl -fsSL https://github.com/saltstack/salt-install-guide/releases/latest/download/salt.sources \
| sudo tee /etc/apt/sources.list.d/salt.sources
sudo apt update
sudo apt install salt-master salt-minion salt-ssh salt-syndic salt-cloud
sudo systemctl enable --now salt-master
sudo systemctl enable --now salt-minion The master minion on the same box lets you test locally before touching production nodes. Edit /etc/salt/minion and set master: salt.example.com. Restart the minion service after any change.
Accept Minion Keys
Each minion sends its public key on first connect. You must accept it on the master. Unaccepted keys are a common post-install blocker.
# List pending keys
sudo salt-key -L
# Accept one minion
sudo salt-key -a web01.prod.internal -y
# Accept all pending (use carefully in production)
sudo salt-key -A -y Verify connectivity with a ping test. Every minion should return True.
sudo salt '*' test.ping Directory Layout on the Master
Salt expects state files under /srv/salt and pillar under /srv/pillar. Many teams symlink these to a Git checkout — the same pattern as managing dotfiles and server config with Git.
/srv/salt/
top.sls
common/
init.sls
packages.sls
web/
init.sls
nginx.sls
php-fpm.sls
/srv/pillar/
top.sls
secrets.sls For sister sites on a shared EC2 pipeline — like Translation Nepal and related legal-tech portals — I keep one Salt master per environment. Staging and production never share pillar secrets.
How Does SaltStack Remote Execution Work in Practice?
Remote execution is Salt's original killer feature. You target minions with glob patterns, grains, or compound matchers. Then you call an execution module. Results stream back in parallel within seconds.
This is faster than looping SSH for ad-hoc tasks. Restart PHP-FPM on all web nodes after a deploy. Check disk space everywhere. Dump a config file from one box. One command, many hosts.
Targeting Minions
Salt target syntax is expressive. Master it early — wrong targets have caused real outages.
# All minions
sudo salt '*' cmd.run 'uptime'
# Glob by hostname
sudo salt 'web*' service.status nginx
# Grain match — all Ubuntu minions
sudo salt -G 'os:Ubuntu' pkg.list_pkgs
# Compound — web AND production
sudo salt -C 'G@roles:web and G@env:prod' cmd.run 'df -h' Set custom grains in /etc/salt/grains on each minion or via a state. Roles like web, db, and queue make top-file mapping clean.
Execution Module Examples
- Run arbitrary shell:
salt 'web*' cmd.run 'systemctl reload php8.3-fpm' - Install a package:
salt 'db01' pkg.install mysql-client - Copy a file:
salt 'web01' cp.get_file salt://scripts/backup.sh /opt/backup.sh - Schedule a highstate:
salt 'G@env:prod' state.apply
Use --out=yaml or --out=json for scripting. Pipe output through our JSON formatter when debugging complex returns.
For lighter automation on a handful of Laravel servers, Laravel Envoy may suffice. Salt wins when the fleet grows past SSH loops and you need grain-based targeting at scale.
How Do You Write Salt States for Configuration Management?
Remote execution is imperative — you tell minions what to do right now. States are declarative. You describe the end state. Salt calculates the diff and applies only what changed. This is the config half of SaltStack: Remote Execution and Config.
A Minimal State File
Create /srv/salt/common/packages.sls:
common_packages:
pkg.installed:
- pkgs:
- vim
- htop
- curl
- ufw
- fail2ban
ntp_service:
service.running:
- name: systemd-timesyncd
- enable: True Apply it to one minion:
sudo salt 'web01' state.apply common.packages Salt reports Succeeded, Changed, or Failed per resource. Idempotency is built in. Running the same state twice should change nothing the second time.
Top File Mapping
The top file (/srv/salt/top.sls) is your routing table. It decides which states each minion receives during state.apply.
base:
'*':
- common.packages
'G@roles:web':
- web.nginx
- web.php-fpm
'G@roles:db':
- db.mysql
'G@env:staging':
- staging.debug-tools Order matters when states depend on each other. Use include in SLS files for shared baselines. Use require, watch, and onchanges requisites to chain resources correctly.
Real-World Web Stack State
On production Laravel hosts I manage with Apache or Nginx, a typical web/php-fpm.sls might look like this:
include:
- web.nginx
php_fpm_packages:
pkg.installed:
- pkgs:
- php8.3-fpm
- php8.3-mysql
- php8.3-redis
- php8.3-xml
- php8.3-mbstring
php_fpm_pool_config:
file.managed:
- name: /etc/php/8.3/fpm/pool.d/www.conf
- source: salt://web/files/www.conf.jinja
- template: jinja
- context:
pm_max_children: {{ pillar['php']['max_children'] }}
- require:
- pkg: php_fpm_packages
php_fpm_service:
service.running:
- name: php8.3-fpm
- enable: True
- watch:
- file: php_fpm_pool_config The Jinja template reads pillar['php']['max_children'] so staging and production differ without duplicating state logic. This mirrors patterns from Nginx vs Apache config work — only the state layer changes.
Pillar for Secrets
Never put database passwords in state files committed to Git. Pillar is the right place. Restrict visibility with pillar top matchers so only DB minions see DB credentials.
# /srv/pillar/top.sls
base:
'G@roles:db':
- match: grain
- db_secrets
'G@roles:web':
- match: grain
- app_config # /srv/pillar/db_secrets.sls
mysql:
root_password: {{ salt['vault'].read_secret('secret/mysql/root') }}
app_user: laravel_app
app_password: {{ salt['vault'].read_secret('secret/mysql/app') }} If you lack HashiCorp Vault, encrypted pillar via GPG works for smaller teams. Store ciphertext in Git. Decrypt only on the master at render time. Treat pillar with the same care as a .env file on a Laravel app — see multi-environment config patterns for parallel thinking.
SaltStack vs Ansible vs Agentless SSH — Which Should You Choose?
Teams evaluating automation often compare Salt with Ansible. Both configure servers. The operational model differs sharply. Pick based on fleet size, network topology, and team habits — not blog rankings.
| Criterion | SaltStack | Ansible | salt-ssh (agentless) |
|---|---|---|---|
| Agent required | Yes — minion daemon | No — SSH only | No — SSH only |
| Parallel execution speed | Very fast — persistent connections | Moderate — SSH per task batch | Slower — no persistent bus |
| Remote execution focus | First-class — built into core | Secondary — playbooks primary | Limited — no live event bus |
| Learning curve | Steeper — matchers, requisites | Gentler — YAML playbooks | Similar to full Salt states |
| Best fit | Large homogenous Linux fleets | Mixed OS, small-to-medium teams | Bootstrap before minion install |
| Event-driven automation | Reactors + event bus | Requires external tooling | Not available |
My practical rule: use Ansible when you have twenty heterogeneous hosts and one part-time ops person. Use Salt when you run fifty-plus similar Linux boxes and need sub-minute parallel command fan-out. Use Terraform (see Terraform state management) for cloud resources — not for installing PHP extensions.
For Kubernetes-native config, Kustomize overlays handle container manifests. Salt still manages the nodes underneath. The layers complement each other.
How Do You Run SaltStack Safely in Production?
Production Salt requires hardening beyond a lab install. I've hit these issues on real deployments — keys left unrotated, open master ports, and highstate cron without notification.
Security Checklist
- Firewall the master: allow 4505–4506 only from minion subnets and admin IPs.
- Reject unknown minion keys by default. Audit
salt-key -Lmonthly. - Separate pillar environments. Production secrets never appear in staging top files.
- Run the master as a dedicated user where your OS packaging supports it.
- Enable master logging to a central sink alongside server monitoring.
Scheduling Highstate
Ad-hoc state.apply fixes drift after manual SSH edits. Scheduled highstate prevents drift from accumulating. Add to minion config:
# /etc/salt/minion
schedule:
highstate:
function: state.apply
minutes: 30
splay: 300 The splay value spreads runs across five minutes. Without splay, three hundred minions hitting MySQL at once can spike load. Small detail. Large impact.
salt-ssh for Bootstrap and Edge Cases
When you cannot install a minion yet — fresh VPS, locked-down network — salt-ssh runs states over SSH with no agent. Configure /etc/salt/roster:
web01:
host: 203.0.113.10
user: deploy
priv: /home/deploy/.ssh/id_ed25519 sudo salt-ssh 'web01' state.apply web.nginx
sudo salt-ssh 'web01' cmd.run 'hostname' Use salt-ssh to install the minion package and accept keys. Then switch to the full bus for ongoing remote execution and config.
Integrating Salt with CI/CD and Deploy Pipelines
Salt does not replace GitLab CI or Deployer. It complements them. After Deployer symlinks a new Laravel release, a reactor or CI webhook can trigger service.reload php8.3-fpm on web minions. On sister sites sharing Deployer 7 + GitLab CI — like Notary Kathmandu — Salt handles OS-level baseline while the app pipeline handles code.
Keep application deploy and OS config in separate repos when possible. App teams should not need Salt access to ship a Blade template fix. Role separation reduces blast radius.
For teams building custom platforms, enterprise application development often includes this ops layer from day one. Waiting until forty servers exist costs more than baking Salt in at ten.
Common Gotchas
Stale minion cache. Run salt-minion -l debug on the minion when states silently fail. Key mismatch and wrong master hostname are the top two causes.
Jinja render errors. Test rendering on the master with salt-call state.show_sls web.php-fpm before applying to production. A missing pillar key crashes the entire highstate.
Package name drift. Ubuntu 22.04 and 24.04 sometimes differ on package names. Use grains to branch:
{% if grains['osrelease'] == '24.04' %}
php_fpm_pkg: php8.3-fpm
{% elif grains['osrelease'] == '22.04' %}
php_fpm_pkg: php8.2-fpm
{% endif %} Validate YAML with a regex tester or CI lint step. Broken indentation in SLS files produces cryptic errors at apply time.
Developers in Nepal running global infra can study remote DevOps career paths and remote work setups. Salt skills transfer directly to SRE and platform roles.
Key Takeaways
- Install a Salt master, accept minion keys, and verify with
salt '*' test.pingbefore writing states. - Use remote execution (
cmd.run,service.restart) for ad-hoc fleet ops; use states for durable config. - Keep secrets in pillar, map states through the top file, and tag minions with grains for clean targeting.
- Schedule highstate with splay to prevent thundering herds against shared services like MySQL or Redis.
- Start with salt-ssh on new hosts, then install minions for full SaltStack remote execution and config at scale.
- Layer Salt under app deploy tools (Deployer, GitLab CI) — do not merge OS baselines with application release pipelines.
People Also Ask
Is SaltStack still maintained in 2026?
Yes. The Salt Project continues under the Salt Open Source Foundation with active releases and documentation at docs.saltproject.io. Broadcom still sells commercial Salt products, but the open-source core remains viable for production Linux automation.
What is the difference between salt and salt-call?
salt runs commands from the master against remote minions. salt-call runs modules locally on the minion itself — useful for debugging grains, testing pillar, or applying states during minion-side troubleshooting without touching the master CLI.
Can Salt manage Windows minions?
Salt supports Windows minions with a different package and service module set. Most production Salt fleets I see are Linux-heavy. Mixed-OS shops often use Ansible for Windows and Salt for Linux, or standardise on one tool to reduce operational overhead.
How does Salt compare to Puppet and Chef?
Puppet and Chef predated Salt and use their own DSL-heavy models. Salt prioritises fast remote execution and YAML states with Jinja — closer to Ansible's readability with better parallel performance when minions are connected. Greenfield teams in 2026 typically choose Salt or Ansible; Puppet and Chef appear mostly in legacy estates.
Build a Fleet You Can Trust
SaltStack: Remote Execution and Config earns its place when SSH loops stop scaling and you need declarative baselines plus instant parallel commands. Start small: one master, three minions, a common packages state, and a scheduled highstate. Expand grain targeting and pillar as the fleet grows. If you want help designing Linux automation alongside Laravel deploy pipelines or multi-site hosting, review our support and maintenance services or see how we run production infrastructure. Contact us to discuss your server fleet — whether you are at ten boxes or heading toward a hundred.
Frequently Asked Questions
0 Comments
Leave a comment
Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

