Skip to content

Configuration

cyb3rjerry edited this page May 23, 2026 · 1 revision

Configuration

Three layers, narrowest-wins:

  1. CLI flags — set explicitly when starting the binary. Highest priority.
  2. Environment variables — when supported.
  3. config/orchestrator.yaml — loaded once at startup.
  4. Hardcoded defaults — what you get when nothing else is set.

The config file is OPTIONAL. A fresh checkout with no -config flag still runs with hardcoded fallbacks for every section.

YAML config

Default path: config/orchestrator.yaml (relative to the orchestrator's cwd). Override with -config /etc/fangs/orchestrator.yaml.

Missing file is silently OK. Missing sections within the file fall back to hardcoded defaults section-by-section.

Restart the orchestrator to pick up changes — SIGHUP reload is not wired.

watched_paths:

The filesystem prefixes the eBPF sensor reports for every sandbox scan. Stamped onto every job whose watched_paths arrived empty: the autonomous watcher, fangs scan submit, the kickoff scan fangs package add triggers.

watched_paths:
  - prefix: "/etc/"               # observe but don't tag
  - prefix: "/etc/shadow"
    cred: true                    # match → EVENT_TAG_CRED_ACCESS
  - prefix: "/root/.ssh/"
    cred: true
  - prefix: "/tmp/"
  - prefix: "/usr/"

Semantics:

  • The kernel-side LPM trie picks the longest match per event. A path tagged cred: true always wins over its parent prefix.
  • cred: true events render with a red row + CRED badge in the UI; severity bumps to high automatically when one becomes a deviation.
  • Per-job customization remains possible — HTTP POST /v1/scans callers can include their own watched_paths and the orchestrator passes those through unchanged.

allow:

Global allowlist entries — IPs, SNIs, and path exclusions. The Differ uses these to suppress noise BEFORE it becomes a deviation. The hardcoded DefaultCDNAllowlist (Cloudflare/GitHub/Google/Fastly/CloudFront) applies underneath — entries here are ADDITIVE.

Per-package scoping is intentionally NOT in YAML — that's what fangs allow add -package P is for. Config is global only.

allow:
  cidrs:
    - value: "10.0.0.0/8"
      note: "internal RFC1918"
    - value: "172.16.0.0/12"

  snis:
    - value: "telemetry.internal.example"
      note: "operator metrics intake"

  paths:
    - value: "/usr/lib/"
      note: "shared library loads — high volume, low signal"
    - value: "/usr/lib64/"
    - value: "/usr/share/zoneinfo/"
      note: "timezone DB reads on every process start"

How each kind maps to a Differ category:

Kind Suppresses category Match semantic
cidr net_new_destination IP ∈ CIDR (lib net.IPNet.Contains)
sni net_new_https_host strings.EqualFold(SNI, value)
path fs_new_path_read, fs_new_path_write path.HasPrefix(normalized, value)

Config-managed entries get deterministic IDs: cfg + sha256(kind + "|" + value)[:12]. Same YAML across restarts always touches the same rows, so re-apply is idempotent.

CLI-added entries coexist; fangs allow remove cfg… works but the entry comes back on next restart unless you also delete the YAML line.

Orchestrator flags

fangs-orchestrator [flags]
Flag Default Purpose
-config config/orchestrator.yaml path to YAML config; missing file OK
-addr 127.0.0.1:8443 HTTP listen address
-id fangs-orchestrator identifier returned to runners on register
-storage sqlite sqlite | postgres | none
-sqlite-path var/lib/fangs/fangs.db sqlite DB file path
-postgres-dsn postgres DSN; also reads $FANGS_PG_DSN
-watch true enable the npm registry watcher
-watch-interval 5m how often to poll the registry per package
-ui true serve the read-only dashboard at /ui/
-metrics true expose Prometheus metrics at /metrics
-notifiers-file optional JSON file of webhook targets; upserts into DB at startup
-retention-days 90 delete events older than this; 0 disables
-retention-interval 24h how often the pruner runs
-tls-cert PEM server cert; with -tls-key, switches to HTTPS
-tls-key PEM server private key
-tls-client-ca PEM CA for verifying runner certs; enables mTLS

Runner flags

fangs-runner [flags]
Flag Default Purpose
-orchestrator http://127.0.0.1:8443 base URL of the orchestrator
-runner-id hostname stable identifier the orchestrator addresses jobs to
-tls-ca PEM CA bundle to verify the orchestrator's server cert
-tls-cert PEM client cert (for mTLS)
-tls-key PEM private key paired with -tls-cert

The runner reads its heartbeat + job-poll intervals from the orchestrator's register response — no local cadence flags.

CLI global flags

fangs [global flags] <subcommand> [args]
Flag Default Purpose
-storage sqlite backend to read from
-sqlite-path var/lib/fangs/fangs.db sqlite DB path
-postgres-dsn postgres DSN
-json false machine-readable output

See CLI-Reference for every subcommand.

Environment variables

Variable Purpose
FANGS_PG_DSN postgres DSN; overridden by -postgres-dsn if both set
$SECRET_ENV per notifier HMAC secret value; per-target secret_env: NAME references this

Notifier JSON config file

Optional alternative to fangs notifier add. Path passed via -notifiers-file=/etc/fangs/notifiers.json.

{
  "notifiers": [
    {
      "name": "soc-slack",
      "url": "https://hooks.slack.com/services/T.../B.../...",
      "template": "slack",
      "enabled": true
    },
    {
      "name": "siem",
      "url": "https://intake.internal/fangs",
      "template": "generic",
      "secret_env": "FANGS_HMAC",
      "min_severity": "high",
      "headers": {"X-API-Key": "${INTAKE_KEY}"}
    }
  ]
}

Each entry is upserted into the notifiers table at startup. Entries managed via fangs notifier add coexist — the file is additive, not authoritative.

Sandbox spec

When submitting a scan via fangs scan submit (or POST /v1/scans), each Job carries a sandbox spec. Defaults via watcher.BuildSandboxScan:

Field Default Meaning
image node:20-slim container image (digest-pinned by the driver)
command npm install <pkg>@<ver> shell command run as PID 1 inside
network_mode bridge bridge | none
pull_policy missing missing | always
user 0:0 UID:GID inside the container
grace_period 2s wait after main exit before SIGTERM
duration 60s max sandbox lifetime

Non-overridable HostConfig (baked into the Docker driver):

Setting Value
CapDrop ALL
SecurityOpt no-new-privileges:true
Memory 512 MB
NanoCpus 1
PidsLimit 256
Ulimits nofile 1024/2048, nproc 256, fsize 256 MB
Tmpfs /tmp size=256m noexec, /run size=16m
LogConfig json-file max-size=10m max-file=3
CgroupParent /fangs/<run_id> (runner-managed)
RestartPolicy no
Volumes none
Devices none

Per-package memory / CPU overrides are a v2 item — currently the only escape is a custom sandbox spec in the scan submission.

Probe defaults

The eBPF sensor's behavior is mostly hardcoded but a few knobs are exposed:

Knob Default Where
Ringbuf size 64 MB per CPU bpf/sensor.bpf.c RINGBUF_SIZE macro
Max watched cgroups 256 MAX_WATCHED_CGROUPS macro
Max watched paths 1024 MAX_WATCHED_PATHS macro
Ancestry depth 5 levels ANCESTORS_DEPTH macro
Argv capture 8 args × 64 B ARGV_NUM × ARGV_LEN macros
TLS dedup window 5 s DedupWindow in sensor.Options
Connect dedup window 250 ms hardcoded in connectDedup

These require a rebuild to change. See Sensor-Probes.

Verifying what's loaded

Run-time inspection of the orchestrator's effective config:

# UI page — shows YAML on disk + effective values + config-managed allowlist entries
http://127.0.0.1:8443/ui/config

# CLI — config-managed entries have cfg-prefixed IDs
fangs allow list

# Log line at startup
"config loaded path=config/orchestrator.yaml watched_paths=14"

Clone this wiki locally