> ## Documentation Index
> Fetch the complete documentation index at: https://docs.runlayer.com/llms.txt
> Use this file to discover all available pages before exploring further.

# SaltStack (Detect only)

> Deploy the Detect-only Linux AI Watch scanner with SaltStack — install the .deb / .rpm from your own Salt fileserver and manage the two config files from pillar.

SaltStack deploys AI Watch to Linux fleets the config-management way: Salt
installs the `runlayer-aiwatch` package on each minion and manages the two
config files, so a `state.apply` both rolls out and continuously asserts the
Detect-only contract. This is the same [Linux (Detect only)](/shadow-ai/deploy/linux)
package — see that page for supported distributions, package internals, and the
scan cadence. This page covers only the Salt-specific delivery.

An example formula ships in the repo at `cli/packaging/linux/salt/`
(`runlayer-aiwatch/init.sls`, the config templates, and `pillar.example.sls`).
It is a **starting point**, not a supported product — copy it into your own Salt
tree and adapt it to your conventions.

## What it does

* Installs the `.deb` / `.rpm` from **your own Salt fileserver**
  (`salt://runlayer-aiwatch/`), selecting the right filename by OS family.
* Manages `/etc/runlayer/aiwatch/config.json` (0644, non-secret) and
  `/etc/runlayer/aiwatch/credentials` (0600, root-only) from pillar, so Salt is
  the source of truth for both.
* Ensures the cron daemon is enabled. Cron re-reads the packaged
  `/etc/cron.d/runlayer-aiwatch` on its own; the config files trigger nothing.

Linux is **Detect-only**: cron-scheduled scans across all users, no Enforce,
Sessions, or browser extension. In the dashboard the device shows **Detect =
Active** and **Enforce / Sessions = Disabled** — the expected state, not an error.

## Prerequisites

* A Salt master with the minions enrolled, plus a cron daemon on each minion
  (the package pulls one in automatically — see [Linux](/shadow-ai/deploy/linux)).
* The `.deb` and `.rpm` on your Salt fileserver under `salt://runlayer-aiwatch/`.
  The Runlayer CDN is **token-gated**, so fetch the packages **once** with your
  download token and serve them from `salt://` — Salt itself never talks to the
  CDN:

  ```bash theme={null}
  VER=1.2.3
  DEST=/srv/salt/runlayer-aiwatch
  mkdir -p "$DEST"
  # -f: fail on HTTP >= 400 — a wrong/expired token must fail HERE, not write
  # an error page into the package file (which would only surface later as a
  # confusing "not a Debian/RPM package" error from pkg.installed).
  for f in runlayer-aiwatch_${VER}_amd64.deb runlayer-aiwatch-${VER}-1.x86_64.rpm SHA256SUMS; do
    curl -fsS -H "x-runlayer-download-token: <token>" -o "$DEST/$f" \
      "https://downloads.runlayer.com/ai-watch/${VER}/$f"
  done
  # Verify integrity before serving via salt:// (SHA256SUMS also covers the
  # tarball, which isn't downloaded here — check just the two packages).
  (cd "$DEST" && grep -E '\.(deb|rpm)$' SHA256SUMS | sha256sum -c -)
  ```

  Contact your Runlayer account team if you don't have a download token yet.

## Pillar keys

The formula reads a single `runlayer_aiwatch` pillar. Copy `pillar.example.sls`,
fill it in, and target it at the right minions:

| Key             | Goes to                         | Notes                                                                        |
| --------------- | ------------------------------- | ---------------------------------------------------------------------------- |
| `host`          | `config.json` → `Host`          | Tenant URL. World-readable, non-secret.                                      |
| `api_key`       | `credentials` (0600)            | Org `rl_org_...` key, **secret** — never in `config.json`.                   |
| `version`       | package filename                | Version to install (no leading `v`); must match the file on your fileserver. |
| `host_override` | `credentials` → `RUNLAYER_HOST` | Optional backend-host override.                                              |

<Warning>
  **Never put the org API key in `config.json`, and never commit it in plaintext.**
  `config.json` is world-readable by design so unprivileged per-user scan children
  can read managed settings; the key belongs only in the 0600 `credentials` file.
  Keep the plaintext key in a **GPG-encrypted pillar** (Salt's `#!gpg|yaml`
  renderer decrypts it on the master at compile time) or your existing secrets
  backend. The formula writes the credentials file with `show_changes: False` so
  the key is never echoed into Salt job returns or master logs.
</Warning>

`config.json` always renders `"Sessions": false` and `"Enforcement": false` —
the formula hard-sets them, so the Detect-only contract holds no matter what the
pillar says.

## Apply

```bash theme={null}
salt '*' state.apply runlayer-aiwatch
```

The first apply installs the package and writes both config files; cron fires
the first scan on the next 15-minute tick.

## Verify

In the Runlayer dashboard, go to **Shadow AI → Devices**: the device appears
with OS = Linux, **Detect = Active**, and **Enforce / Sessions = Disabled**
within about 15 minutes. Disabled Enforce/Sessions is the correct Detect-only
state on Linux. For on-host verification (`sudo /usr/lib/runlayer/run-aiwatch-scan.sh`),
see [Linux (Detect only)](/shadow-ai/deploy/linux#verify).

## Idempotency

A second `state.apply` is a **no-op** once the installed package and the two
config files match the pillar — Salt reports no changes. To upgrade, bump
`version` in the pillar (and place the new `.deb` / `.rpm` on your fileserver);
the next apply installs the newer package in place. Your managed `config.json`
and `credentials` are re-asserted from pillar on every run.

## Other config managers

This generalizes directly: install the `.deb` / `.rpm`, manage `config.json`
(0644) and `credentials` (0600), and ensure cron runs — the same three steps
port to Ansible, Puppet, or Chef.
