.deb / .rpm, containerized. Instead of a cron entry on the host, the image’s entrypoint (scan-host-users.sh) scans the host user homes that are bind-mounted into the container, reading /etc/passwd to enumerate every account and running one privilege-dropped scan per user home.
Use this when you’d rather ship a container than a native package — for example on Kubernetes nodes, or hosts already managed as container workloads. For native-package and distro context, see Linux (Detect only).
What it is
- The same Detect-only all-users scanner as the Linux package: it enumerates all passwd users with an existing (bind-mounted) home — root and service accounts included, shared homes deduped — and reports MCP servers, connectors/plugins, skills, and agents to your Runlayer tenant.
- Not a sandboxed app. Like the
.deb/cron model, it needs to read every user’s home directory, so it requires a privileged read-only mount of the host filesystem. It is not a self-contained workload that only sees its own filesystem. - Detect only. There are no hooks, no Enforce, no Sessions, and no browser extension. In the dashboard the device shows Detect = Active and Enforce / Sessions = Disabled — the expected state, not an error. (The image bakes
/etc/runlayer/aiwatch/config.jsonwithSessions/Enforcementfalse, so this state is guaranteed without any mounted config.) - Local passwd only. The container enumerates users from the mounted host
/etc/passwd. Centrally-managed accounts that exist only via NSS/SSSD/LDAP (not written to/etc/passwd) are not enumerated — even if their homes are mounted — because the container can’t resolve the host’s NSS stack. This differs from the native.deb/.rpmscanner, which runsgetent passwdand so resolves NSS/LDAP users. If your fleet uses centrally-managed accounts, generate full snapshots on the host (getent passwd > /etc/runlayer/aiwatch/passwd-snapshotandgetent group > /etc/runlayer/aiwatch/group-snapshot, via a host cron) and mount them asRUNLAYER_HOST_PASSWD/RUNLAYER_HOST_GROUP. The group snapshot matters: supplementary groups are restored from the group file before the privilege drop, so without it NSS/LDAP-only memberships are lost and scans run with the primary GID only — missing group-readable homes and shared project dirs the native scanner would read.
Image
The scanner image is published to Runlayer’s private CustomerDistribution ECR and referenced as:- amd64 only. The bundled
aiwatchbinary is x86_64 (built onmanylinux_2_28), so the whole image islinux/amd64. On an arm64 host it runs under emulation (Rosetta / qemu). - The glibc floor is moot: the runtime is
debian:12-slimand the bundle is self-contained, so there is no host glibc requirement — unlike the.deb/.rpm, which have a host glibc floor (see Linux (Detect only) for the current support matrix).
Standalone docker run
Mount only the paths the scanner reads, each read-only under /host — not the whole host root. RUNLAYER_HOST_HOME_PREFIX=/host makes the host-absolute passwd homes (/home/alice) resolve to the mounted copies inside the container. This covers users under /home and /root; homes elsewhere (service accounts under /var/lib, custom /srv homes) are not silently missed — the scanner logs each skipped user and a scanned/skipped summary each pass, so you add the prefix if you need it (see below).
RUNLAYER_SCAN_INTERVAL seconds (default 900 = 15 min), repeat. A failing pass is logged and the next pass retries — the container does not exit.
For a one-shot pass (single scan then exit — e.g. driven by a host cron entry or a systemd timer that runs docker run on a schedule), pass --once or set RUNLAYER_RUN_ONCE=1:
RUNLAYER_API_KEY the entrypoint exits 0 quietly before any scan (the unconfigured-fleet gate), so an unconfigured container produces no errors and no data.
One honesty note on :ro: read-only bind mounts are not recursive — if /home itself contains mounted filesystems (NFS/EBS/tmpfs homes), those submounts can remain writable through the mount, on both Docker and Kubernetes. The only writes the scanner performs there are its per-user ~/.runlayer state (logs) — the same writes the native .deb scanner does by design — so the exposure is benign, but don’t read :ro as a hard guarantee for submounted homes. On Kubernetes 1.30+ you can opt into recursiveReadOnly (commented in the manifests) to close this.
Need to cover homes outside /home and /root? Add the prefix as another read-only mount (e.g. -v /var/lib:/host/var/lib:ro) — keep the scope as narrow as your passwd layout allows. As a last resort you can mount the whole root (-v /:/host:ro) for maximal coverage, but that exposes far more of the host to the container than the scanner needs; prefer explicit prefixes. Either way the entrypoint logs any skipped (unmounted) user and warns when a pass finds zero scannable homes — the usual sign of a mount/prefix mismatch.
Environment
| Variable | Default | Purpose |
|---|---|---|
RUNLAYER_API_KEY | — (required) | Org API key (rl_org_..., Shadow AI Scan role). Empty ⇒ quiet exit 0. |
RUNLAYER_HOST | — | Tenant API endpoint (e.g. https://tenant.runlayer.com). |
RUNLAYER_HOST_PASSWD | /host/etc/passwd | Host passwd file to enumerate. Falls back to the container’s own getent passwd if unreadable. |
RUNLAYER_HOST_GROUP | /host/etc/group | Host group file; restores each user’s supplementary groups on the privilege drop so group-readable files stay scannable. Missing ⇒ primary group only. |
RUNLAYER_HOST_HOME_PREFIX | "" (empty) | Prefix prepended to passwd home paths. Set to /host to pair with the /host root mount. The entrypoint exports it as the strip prefix so submitted findings show real host paths, not /host/... container paths. |
RUNLAYER_MACHINE_ID_PATH | (unset) | Path to the host machine-id, so the device id matches the host. |
RUNLAYER_HOSTNAME | detected | Overrides the reported hostname for device attribution. Set this on standalone docker run (e.g. "$(hostname)") — otherwise the device shows the random container ID, not the host. K8s sets it from the node name automatically. |
RUNLAYER_DEVICE_ID | (unset) | Overrides the device id outright (takes priority over machine-id). |
RUNLAYER_SCAN_INTERVAL | 900 | Loop sleep seconds between passes (steady-state / DaemonSet). |
RUNLAYER_RUN_ONCE | (unset) | 1 ⇒ single pass then exit (same as the --once arg). |
RUNLAYER_SCAN_TIMEOUT | 600 | Per-user scan timeout in seconds. |
Kubernetes
Ready-to-apply manifests live incli/packaging/container/k8s/ (namespace, configmap, secret.example, daemonset, cronjob, kustomization, plus a README). Each pod bind-mounts the node’s host user homes read-only under /host and attributes results to the node.
RUNLAYER_HOST, RUNLAYER_HOST_PASSWD=/host/etc/passwd, RUNLAYER_HOST_HOME_PREFIX=/host, and RUNLAYER_MACHINE_ID_PATH=/host/etc/machine-id values come from the ConfigMap; RUNLAYER_API_KEY comes from the Secret. All host mounts are readOnly: true under /host, matching those env paths.
DaemonSet vs CronJob — pick ONE
- DaemonSet (recommended): one long-running pod per node that loops on
RUNLAYER_SCAN_INTERVAL(default 900s). This is the only shape that gives fleet-wide per-node coverage automatically. Scope it withnodeSelector/tolerations. - CronJob: schedules a single
--onceJob every 15 min (concurrencyPolicy: Forbid). A CronJob does not run per-node — its pod lands on one scheduler-chosen node, so only that node is scanned each tick. Use it for single-node clusters, or pin it to a node vianodeSelector(one CronJob per node you want covered).
Node attribution
RUNLAYER_HOSTNAME is set from the downward API spec.nodeName, so scans attribute to the node rather than the ephemeral pod name. RUNLAYER_MACHINE_ID_PATH=/host/etc/machine-id points device-id derivation at the bind-mounted host machine-id, so the device id also matches the node.
The container runs as root (uid 0) because the entrypoint drops to each host user’s numeric uid via setpriv per scan child; it is not privileged and needs no host network or PID. It reads only the mounted files — never the Kubernetes API — so no RBAC is required (automountServiceAccountToken: false).
Device identity — don’t run two delivery models per host
The machine-id is the dedup key: AI Watch derives the device id from the host machine-id, so every user on a host maps to one device. Bind-mount/etc/machine-id (RUNLAYER_MACHINE_ID_PATH=/host/etc/machine-id) — or set RUNLAYER_DEVICE_ID explicitly — so the container’s device id matches the host.
Verify
On a test host, seed a shadow-AI config in a user’s home and run one pass:- Shadow AI → Devices shows the host with os = Linux, Detect = Active, Enforce / Sessions = Disabled (the correct Detect-only state).
- Connectors shows alice’s shadow MCP server.
kubectl -n runlayer logs on the DaemonSet pod (or the CronJob’s Job pod) shows a scanning user=... home=/host/home/... line per host user; the node appears in Shadow AI → Devices the same way.
Related
Linux (Detect only)
Native
.deb / .rpm packages, supported distributions, and cron model.Deploy AI Watch
Full deployment overview across macOS, Windows, and Linux.