Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

SaltStack: Remote Execution and Config

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.

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.

SaltStack Remote Execution and Config ArchitectureSalt MasterStates, Pillar, Top fileMinion AWeb serverMinion BDB serverMinion CQueue workerZeroMQ 4505 / 4506 — encryptedRemote execution: salt target cmd.runConfig: state.apply enforces SLS files
SaltStack remote execution and config uses a central master that pushes jobs and state files to authenticated minions over ZeroMQ.

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.

Salt Remote Execution Job FlowOperatorsalt CLIMasterJob queuePub channelPort 4505MinionsExecute moduleCommon Execution Modulescmd.runservice.restartfile.readpkg.installResults return on port 4506 — JSON per minion
SaltStack remote execution publishes jobs from the master CLI to minions, which run execution modules and return structured results.

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

  1. Run arbitrary shell: salt 'web*' cmd.run 'systemctl reload php8.3-fpm'
  2. Install a package: salt 'db01' pkg.install mysql-client
  3. Copy a file: salt 'web01' cp.get_file salt://scripts/backup.sh /opt/backup.sh
  4. 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.

Salt Config: State Apply ConvergenceTop fileSLS statesPillar dataGrainsstate.applyHighstate runNo changeChangedFailedSchedule via cron: salt-call state.apply or master reactor
SaltStack config management merges top file, states, pillar, and grains during state.apply to converge each minion to declared desired state.

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.

CriterionSaltStackAnsiblesalt-ssh (agentless)
Agent requiredYes — minion daemonNo — SSH onlyNo — SSH only
Parallel execution speedVery fast — persistent connectionsModerate — SSH per task batchSlower — no persistent bus
Remote execution focusFirst-class — built into coreSecondary — playbooks primaryLimited — no live event bus
Learning curveSteeper — matchers, requisitesGentler — YAML playbooksSimilar to full Salt states
Best fitLarge homogenous Linux fleetsMixed OS, small-to-medium teamsBootstrap before minion install
Event-driven automationReactors + event busRequires external toolingNot 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 -L monthly.
  • 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.

Salt Production: Choose Your PathNew server to manage?No minion yetFresh VPSMinion installedKey acceptedOne-off fixEmergencysalt-ssh rosterstate.apply highstatesalt cmd.run targetDocument every path in runbooks — drift kills config reliability
Production SaltStack remote execution and config workflows branch by minion readiness: salt-ssh for bootstrap, highstate for ongoing config, cmd.run for emergencies.

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.ping before 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

SaltStack is an open-source automation platform from the Salt Project that combines remote execution and configuration management in one master-minion model. The master pushes shell commands and YAML state files to authenticated minions over an encrypted ZeroMQ bus on ports 4505 and 4506. You define desired server state once in SLS files; Salt enforces it continuously. It fits teams that outgrow manual SSH loops and one-off cron scripts on Linux fleets.

A central master holds your state tree under /srv/salt, pillar data under /srv/pillar, and the job queue. Each managed host runs a minion agent that connects outbound to the master. Minions authenticate with public keys you accept via salt-key on the master. The master publishes jobs and state files; minions run execution modules and return structured results in parallel. Grains provide static facts, pillar stores secrets, and the top file routes states to targets.

SaltStack uses ZeroMQ on port 4505 for publish and port 4506 for request/reply between master and minions.

Add the official Salt Project repository from docs.saltproject.io, then run apt update and apt install salt-master salt-minion salt-ssh salt-syndic salt-cloud. Enable and start salt-master and salt-minion with systemctl. Install the minion on the master box too so you can test locally before touching production nodes. Edit /etc/salt/minion and set master: to your master hostname, then restart the minion service after any change.

Unaccepted minion keys are the most common post-install blocker. Each minion sends its public key on first connect; you must accept it on the master with salt-key -a hostname -y or salt-key -A -y for all pending. Wrong master hostname in /etc/salt/minion is the second top cause. Verify with salt '' test.ping — every minion should return True. If states silently fail later, run salt-minion -l debug on the minion to check for stale cache or key mismatch.

Remote execution is imperative — you tell minions what to do right now via commands like salt 'web' cmd.run 'systemctl reload php8.3-fpm'. States are declarative YAML SLS files describing desired end state for packages, files, and services. Salt calculates the diff and applies only what changed. Running the same state twice should change nothing the second time because idempotency is built in. Use cmd.run for emergencies; use states for durable config.

Salt target syntax is expressive and worth mastering early because wrong targets have caused real outages. Use glob patterns like salt 'web*' for hostname matching, grain matches with salt -G 'os:Ubuntu', or compound matchers like salt -C 'G@roles:web and G@env:prod'. Set custom grains such as roles and env in /etc/salt/grains on each minion or via a state. The top file then maps states to these grain-based targets during state.apply.

Never put database passwords or API keys in state files committed to Git. Pillar is the correct place for restricted key-value data. Restrict visibility with pillar top matchers so only DB minions see DB credentials — for example mapping G@roles:db to db_secrets.sls. If you have HashiCorp Vault, pull secrets at render time with salt['vault'].read_secret. Smaller teams can use GPG-encrypted pillar stored as ciphertext in Git, decrypted only on the master. Treat pillar with the same care as a .env file on a Laravel app.

The top file at /srv/salt/top.sls is your routing table. It decides which states each minion receives during state.apply or scheduled highstate. A typical layout applies common.packages to all minions, web.nginx and web.php-fpm to G@roles:web, db.mysql to G@roles:db, and staging.debug-tools to G@env:staging. Order matters when states depend on each other. Use include in SLS files for shared baselines and require, watch, and onchanges requisites to chain resources correctly.

Both configure servers but the operational model differs. Salt requires a minion agent and delivers very fast parallel execution over persistent ZeroMQ connections. Ansible is agentless via SSH, gentler to learn with YAML playbooks, and suits mixed OS environments. My practical rule from production fleets: 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 for cloud resource provisioning, not for installing PHP extensions.

Firewall the master to allow ports 4505–4506 only from minion subnets and admin IPs. Reject unknown minion keys by default and audit salt-key -L monthly. Separate pillar environments so production secrets never appear in staging top files. Run the master as a dedicated user where OS packaging supports it. Enable master logging to a central sink alongside server monitoring. Schedule highstate on minions with splay to spread runs and prevent thundering herds against shared services like MySQL or Redis.

salt-ssh runs states and commands over SSH with no minion agent installed. Configure targets in /etc/salt/roster with host, user, and private key path. Use it when you cannot install a minion yet — fresh VPS, locked-down network, or initial bootstrap. Run salt-ssh 'web01' state.apply web.nginx to configure the host, or salt-ssh to install the minion package and accept keys, then switch to the full ZeroMQ bus for ongoing remote execution and config management.

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 plus GitLab CI, Salt handles OS-level baseline while the app pipeline handles code. Keep application deploy and OS config in separate repos when possible so app teams do not need Salt access to ship a Blade template fix. Role separation reduces blast radius.

Jinja render errors are frequent — test rendering on the master with salt-call state.show_sls web.php-fpm before applying to production because a missing pillar key crashes the entire highstate. Package names differ between Ubuntu 22.04 and 24.04; branch with grains in Jinja templates for php8.2-fpm versus php8.3-fpm. Broken YAML indentation produces cryptic errors at apply time, so validate SLS files with a CI lint step. Stale minion cache and wrong master hostname round out the top troubleshooting causes.

Yes, for production drift control. Ad-hoc state.apply fixes drift after manual SSH edits, but scheduled highstate prevents drift from accumulating. Add a schedule block to /etc/salt/minion calling state.apply every 30 minutes with splay: 300 to spread runs across five minutes. Without splay, three hundred minions hitting shared services like MySQL at once can spike load. Small detail, large impact on fleet stability.

Share this article

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.

Quick Contact Options
Choose how you want to connect me: