skip to content

In Salt, what is the difference between grains and pillars, and which of the two should hold a database password?

level: juniorimportance: should knowfreq 58%

answer

  1. one is discovered, one is delivered
  2. minion-supplied versus master-rendered
  3. top.sls decides who sees which pillar
  4. secrets where the minion cannot forge them
  5. refresh_pillar after a master-side edit

basics

~20 s

Grains are facts the minion discovers about itself (OS, CPU, IP) and reports upward. Pillars are data the master compiles and sends down to specific minions. Secrets belong in pillar, because pillar is master-controlled and delivered only to the minions it targets.

solid answer

~50 s

Grains flow **up**: the minion collects them at start-up — `os`, `osfinger`, `fqdn`, `cpuarch`, `ipv4` — and you can add static ones in `/etc/salt/grains`. They describe the host, so they are ideal for targeting and for branching inside a state (`salt -G 'os:Ubuntu' ...`). Pillars flow **down**: the master renders them from `pillar_roots`, using the pillar `top.sls` to decide which minion sees which data, then ships each minion only its own slice. That makes pillar the right home for a database password, an API token or any per-environment value: the minion cannot forge it, and other minions never receive it. Read them with `salt '*' grains.items` and `salt '*' pillar.items`, or in a state with `salt['pillar.get']('db:password')`. Two gotchas: grains are minion-supplied, so grain targeting is convenience rather than authorization, and after editing pillar on the master you run `saltutil.refresh_pillar` for minions to pick it up.

code

yaml · 6 lines
yaml
base:
  'web*':
    - webserver
  'os:Ubuntu':
    - match: grain
    - apt

go deeper

for a junior

Be able to state the direction plainly: grains come from the minion, pillar comes from the master, and secrets go in pillar. Know the two commands that show them, grains.items and pillar.items.

for a middle

Explain how the pillar top.sls scopes data to specific minions, how to read a nested key with salt['pillar.get']('a:b'), and why a cached pillar needs saltutil.refresh_pillar after a master-side edit.

for a senior

Show where the trust boundary actually sits: grains are minion-asserted so they cannot authorize anything, and pillar controls distribution but not what the minion does with the value afterwards — file modes, job-cache leakage through cmd.run, and GPG or ext_pillar for at-rest protection.

for a principal

Own the data model for the estate: what belongs in pillar versus an external secret store, how pillar is reviewed and version-controlled when it holds environment truth, and who can edit the pillar tree — since that is effectively production configuration authority.

## Two data planes pointing in opposite directions Salt has two ways to attach data to a minion, and the whole distinction is direction and ownership. **Grains flow up.** They are facts about the host, gathered by the minion itself when `salt-minion` starts: operating system and version, kernel, CPU architecture and count, memory, FQDN, IP addresses, virtualisation type. You can add your own static grains in `/etc/salt/grains`, in the minion config under a `grains:` key, or at runtime with `grains.setval`. Inspect them with: ```bash salt 'web1' grains.items salt 'web1' grains.get os ``` **Pillars flow down.** They are data the **master** owns, rendered from files under `pillar_roots` (conventionally `/srv/pillar`), and delivered to minions individually over the return port. Which minion gets which data is decided by the pillar `top.sls`, using the same matchers as ordinary targeting: ```yaml base: 'web*': - webserver 'os:Ubuntu': - match: grain - apt ``` Inspect with `salt 'web1' pillar.items`, and read a nested key in a state or template with `salt['pillar.get']('db:password')`. ## Using each in states Grains answer "what am I?", so they drive branching: ```yaml {% if grains['os_family'] == 'Debian' %} nginx_pkg: pkg.installed: - name: nginx-full {% endif %} ``` Pillars answer "what should I be configured with?", so they carry the values: ```yaml /etc/myapp/app.conf: file.managed: - source: salt://myapp/app.conf.jinja - template: jinja - mode: '0640' - context: db_password: {{ salt['pillar.get']('db:password') }} ``` The same SLS file then serves every environment; only the pillar changes. ## Why the password goes in pillar Three reasons, and interviewers want at least two of them. 1. **Ownership.** Grains are generated on the minion and can be set on the minion. A host that has been tampered with can claim any grain it likes — including a grain you use for targeting. Pillar is compiled on the master; the minion has no way to assert what its pillar should be. 2. **Scoping.** Pillar is targeted. Only minions matched by the pillar `top.sls` receive that data, and each minion's payload is encrypted for it on the way down. Put the secret in a grain and it is on the host in plain view, sitting in `grains.items` for anyone who can run a command against it. 3. **Layering.** Because the master owns pillar, you can plug in stronger sources: the GPG renderer keeps values encrypted at rest in the pillar files and decrypts them on the master at render time, and `ext_pillar` pulls from an external system entirely. That said, be honest about the limit: once rendered, the value is in memory on the minion and typically written into a config file. Pillar controls *distribution*, not what happens after delivery — so keep managed files' modes tight and never pass a secret as an argument to `cmd.run`, where it lands in the job cache and the logs. ## Refreshing A minion caches its pillar. Edit `/srv/pillar/...` on the master and existing minions do not magically see it; you refresh explicitly: ```bash salt '*' saltutil.refresh_pillar ``` Do not confuse this with `saltutil.sync_all`, which syncs custom modules, states, grains and returners to minions — a different kind of payload entirely. Grains refresh with `saltutil.refresh_grains`, and a minion restart re-collects them anyway. ## The mental shortcut "Grains are what the host tells you; pillar is what you tell the host." Target and branch on grains, configure and secret with pillar. Where the two overlap — for example, you can target minions by pillar with `-I 'role:db'` — prefer pillar for anything you would be unhappy for a compromised host to control, and grains for anything the host is genuinely the authority on.

  • You need per-environment values but no secrets — is pillar still the right place, or would grains do?
    Pillar. Environment values are something you *assign*, not something the host discovers, so the master should own them; that also keeps one SLS serving dev, staging and prod. Grains are for what the host genuinely knows about itself — OS family, architecture, virtualisation — which is why targeting and conditional branching read grains.
  • What does the GPG renderer add on top of putting a password in pillar?
    It keeps the value encrypted at rest in the pillar files, so the secret can live in version control alongside the rest of the pillar tree. The master holds the private key and decrypts during pillar rendering, so minions still receive plaintext. It protects the repository and backups, not the delivered value.
  • A colleague targets a privileged state with `-G 'role:admin'`. Why is that a bad idea?
    Because grains are set on the minion. A host that has been compromised, or simply misconfigured, can declare `role: admin` and pull itself into that run. Grain targeting is a convenience for slicing a fleet, not an authorization decision — target on pillar or an explicit list when the consequences matter.

saying these in an interview costs you the question

  • Says pillar data is collected on the minion like grains
  • Puts secrets in grains because they are 'per host'
  • Assumes editing pillar on the master updates minions immediately
  • Thinks grain targeting is an access-control mechanism
  • Confuses saltutil.sync_all with refreshing pillar data

context