How to Enforce Chainsaw Org-Wide
Layer CI checks, MDM policy, and network egress controls so every package install across the org routes through Chainsaw — without Chainsaw trying to be an endpoint agent.
Where Chainsaw Stops and the Rest of Your Security Stack Starts
Chainsaw is a supply chain security tool, not a system-control tool. It inspects, scores, and gates packages at the proxy layer. It does not — and will not — ship an endpoint agent, an MDM profile daemon, or a firewall driver. Those jobs belong to tools already purpose-built for them.
That split is deliberate. Customers already run MDM (Intune, Jamf, Kandji), SWG/SASE (Zscaler, Netskope, Cloudflare Gateway, Palo Alto), and CI platforms (GitHub Actions, GitLab CI, Jenkins) in production. A proxy that tries to duplicate those controls competes poorly and adds attack surface. A proxy that integrates with them cleanly is strictly additive.
So Chainsaw-wide enforcement is not “install the Chainsaw daemon everywhere.” It’s:
Internal controls inside Chainsaw (policy evaluation, client guides, attestation API, config-drift checks in the CLI) combined with third-party enforcement tools (MDM for endpoint config, network firewall/SASE for egress, CI for repo-level preflight).
This tutorial walks through the combination.
The three enforcement layers
| Layer | What it blocks | What enforces it | Chainsaw’s role |
|---|---|---|---|
| CI-enforced | PRs that commit bypass files (project .npmrc, pinned public --index-url, etc.), runners whose config drifted | Your CI platform + branch protection | The chainsaw/enforcement GitHub Action runs chainsaw scan-repo + chainsaw doctor --strict --attest and exits non-zero on drift. |
| Endpoint-managed | Drift on fleet-managed laptops and build agents between attestations | Your MDM (Intune, Jamf, Kandji, Workspace ONE, etc.) | Chainsaw ships reference MDM scripts; the MDM owns provisioning and compliance reporting to the device fleet. |
| Network-mandatory | Any direct connection to public registries from anywhere on your network | Your SWG/SASE/firewall | Chainsaw publishes an authoritative upstream-host deny-list at GET /api/compliance/egress-allowlist; your SWG owns the block. |
No single layer is sufficient on its own. An admin who deploys only the CI layer stops bypass files in PRs but can’t catch a developer running npm install --registry=https://registry.npmjs.org on their laptop. An admin who deploys only MDM misses BYOD and any runner outside their fleet. An admin who deploys only the network layer covers everything on the network but is blind to which endpoints are drifting inside the tunnel.
Combine at least two. Regulated environments combine all three.
Prerequisites
- A running Chainsaw deployment accessible from both your CI and your endpoints.
- A client credential or API key with the
client-setupscope for CLI attestation, and an admin withcompliance:readto view the dashboard. - Optional but recommended: your MDM (Intune, Jamf, Kandji) and SASE vendor (Zscaler, Netskope, Cloudflare Gateway) accessible for the endpoint / network steps.
main, and gives you immediate bypass-file detection across every repo.Layer 1 — CI-enforced
CI is the fastest layer to stand up because it requires no vendor integration beyond your existing CI platform and no endpoint changes. It catches two of the most common bypass patterns:
- A committed project-scope config override (a
.npmrcin the repo, a--index-urldirective inrequirements.txt, a<repository>block inpom.xml). - Drift on the CI runner itself — for example, a self-hosted runner where a prior job left
PIP_INDEX_URLexported.
Step 1: add the Chainsaw GitHub Action
Copy the reference workflow into your repo:
# .github/workflows/chainsaw.yml
name: Chainsaw enforcement
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
chainsaw-preflight:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Chainsaw enforcement
uses: ./enforcement/github-actions
with:
server: ${{ secrets.CHAINSAW_SERVER }}
token: ${{ secrets.CHAINSAW_TOKEN }}
attest: true
scan-repo: true
Set CHAINSAW_SERVER and CHAINSAW_TOKEN as repository (or organization) secrets.
CHAINSAW_TOKEN here is a management-API key — the c305_pa_… value minted at Access → API keys, or a session JWT. The action exports it to chainsaw doctor, which sends Authorization: Bearer $CHAINSAW_TOKEN to /api/attestations, and that endpoint accepts only an API key or a JWT. It is a different credential from the CHAINSAW_CLIENT_ID / CHAINSAW_CLIENT_SECRET pair used further down this page to pull packages through the registry proxy: a client-credential pair never authenticates against /api/…, and an API key never authenticates a package fetch. Never base64-encode either one. The full map is in configuration reference section B30.
Step 2: mark it as a required status check
On the branch protection rule for main:
- Enable Require status checks to pass before merging.
- Add
chainsaw-preflightto the required list. - Enable Require branches to be up to date before merging so the check always runs against current code.
From this point, any PR that commits a bypass file or whose runner fails the doctor --strict check blocks merge until it’s resolved.
Step 3 (optional): add GitLab CI / Jenkins equivalents
For GitLab:
chainsaw-preflight:
stage: test
image: alpine:3.20
before_script:
- apk add --no-cache curl bash
- curl -fsSL https://chain305.com/install.sh | bash
script:
- chainsaw scan-repo .
- chainsaw doctor --strict --attest
variables:
CHAINSAW_SERVER: "$CHAINSAW_SERVER"
CHAINSAW_TOKEN: "$CHAINSAW_TOKEN"
For Jenkins, wrap the same two commands in a sh stage. The exit codes (0, 10, 30, 40) are stable across platforms so the job failure surfaces cleanly.
Layer 2 — Endpoint-managed (MDM)
CI enforcement catches repo-level drift but cannot see what happens on a developer laptop between CI runs. That is the MDM layer’s job.
Chainsaw does not ship an endpoint agent. Instead, we publish reference scripts that your MDM runs on enrollment. The MDM owns the lifecycle; Chainsaw provides the payload.
What the MDM script does
The reference scripts (enforcement/mdm/jamf/macos.sh, enforcement/mdm/intune/macos.sh, .../linux.sh, .../windows.ps1) are the same scripts chainsaw harden generates, with nothing baked in: you supply the server URL and org slug at run time. A hardening bundle (tutorial 54) gives you copies with both pre-filled. Each script:
- Installs the CLI with
install.sh/install.ps1when it is not already on the machine (checksum-verified over TLS; no signature check runs on the client), and drops a marker beside the binary so the uninstall script knows it may remove it. - Runs
chainsaw install-hook <manager> --scope userfor all eleven managers (npm, yarn, bun, pip, cargo, maven, gradle, sbt, nuget, go, docker), as the signed-in user.--scopetakes onlyuserorproject, so the config lands in that user’s home directory (~/.npmrc,~/.cargo/config.toml, …).- macOS (Jamf, Intune): run as root; the script finds the console user and runs the CLI as them with
sudo -u. - Linux (Intune): there is no console-user convention, so a root run needs
CHAINSAW_TARGET_USER, or run the policy as the user. - Windows (Intune): set the script policy to Run this script using the logged on credentials. The script refuses to run as SYSTEM, because it would wire the SYSTEM profile and protect nobody.
- Only that one account is wired. Other accounts on the machine, and users who sign in later, are not covered until the policy runs for them. A developer can delete the block from their own config; the next
chainsaw doctor --strictrun reports that as drift.
- macOS (Jamf, Intune): run as root; the script finds the console user and runs the CLI as them with
- Exits non-zero, without attesting, if
install-hookfailed for any manager, or if the server URL, client ID, client secret or org slug is missing. - Runs
chainsaw doctor --strict --attestonce, which POSTs the endpoint’s posture to/api/attestations(a bundle-generated copy adds--bundle-id=<id>, which reports the bundle as applied). It does not schedule a recurring check; schedule one through your MDM if you want it.
On Linux build hosts running dockerd, the user-scope docker wiring (~/.docker/daemon.json) is not read by the daemon; add the mirror to /etc/docker/daemon.json by hand and restart dockerd.
Step 1: deploy the enrollment script
Intune (Linux/macOS/Windows): upload enforcement/mdm/intune/<platform>.{sh,ps1} as an Intune script policy and assign it to the developer-device group. Supply CHAINSAW_SERVER, CHAINSAW_CLIENT_ID, CHAINSAW_CLIENT_SECRET and CHAINSAW_ORG (your org slug, e.g. acme-corp) through the script environment. On Windows, set Run this script using the logged on credentials to Yes.
Jamf (macOS): upload enforcement/mdm/jamf/macos.sh as a policy script. Set $4/$5/$6/$7 to your Chainsaw server, client ID, client secret and org slug (Jamf’s standard parameter convention).
Kandji / Workspace ONE / other MDMs: the scripts are plain shell / PowerShell. Use your MDM’s custom-script payload; inject CHAINSAW_SERVER, CHAINSAW_CLIENT_ID, CHAINSAW_CLIENT_SECRET and CHAINSAW_ORG via its secret store.
Step 2: verify attestation arrives
Open Admin → Enforcement in the dashboard. The table populates as endpoints run their initial attestation — usually within minutes of the MDM policy running. Each row shows:
device_id(defaults tohostname/USER; add--device-idto the script’sdoctorcall if you need stable IDs across re-enrollment).- Per-ecosystem status (compliant / drifted / unconfigured / unsupported).
direct_registry_egress— whether the endpoint can still reachregistry.npmjs.org/pypi.org/repo.maven.apache.orgdirectly. This is the signal for Layer 3 below.
Step 3: alert on drift
/api/compliance/overview exposes drifted_count and recent_bypass_events_24h. Wire those into whatever you already use for alerting (Slack incoming webhook, PagerDuty, your existing monitoring stack) — Chainsaw does not ship an alert router.
Layer 3 — Network-mandatory
The only layer that prevents bypass rather than detecting it. It’s also the one that requires cooperation from your network team.
Chainsaw does not terminate, route, or firewall traffic. We publish the upstream registry host list; your SWG/SASE/firewall owns the block.
Step 1: generate the deny-list
Either download it from your running deployment:
curl -H "Authorization: Bearer $CHAINSAW_TOKEN" \
https://chain305.com/chainproxy/api/compliance/egress-allowlist \
| jq -r '.entries[].host'
Or generate it statically from the seed config (useful for air-gapped review):
go run ./tools/egress-allowlist-gen \
-config configs/seed.yaml \
-out enforcement/network
Both produce the same host list. The enforcement/network/ output contains vendor-formatted files:
zscaler-urls.txt—.domain.tldformat, paste as a Zscaler URL Category.netskope-urls.txt— bare hosts, paste as a Netskope URL List.generic-deny.txt— bare hosts with comments, for any firewall.
Step 2: load it into your SASE / firewall
Zscaler Internet Access: Administration → URL Categories → add a custom category from the zscaler-urls.txt list → add a URL Filtering rule that blocks that category for all users except Chainsaw’s outbound IP.
Netskope SWG: Policies → Real-time Protection → URL List → import netskope-urls.txt. Create a block rule for everyone except Chainsaw’s egress identity.
Palo Alto NGFW: import generic-deny.txt as an External Dynamic List → deny in an outbound security policy with Chainsaw’s host as the only exception.
AWS / Cloud egress: add the hosts to your VPC egress security group deny rules; keep your Chainsaw NAT IP on the allow path.
Kubernetes NetworkPolicy: for build-pod subnets, a egress: rule that only permits to: Chainsaw achieves the same result.
Step 3: verify
From a developer workstation (not Chainsaw), curl -I https://registry.npmjs.org/ should time out or be explicitly refused. The dashboard’s direct_registry_egress column flips to blocked on every endpoint that re-attests after the network change lands.
Phased rollout
Flipping everything at once causes breakage. We recommend five weeks:
- Week 0 — Monitor. Install the CI Action with
attest: truebut without adding it to branch protection. Compliance data starts flowing. Nothing is blocked. - Week 1 — CI required. Flip the GitHub Action to a required status check on
main. Drifted PRs stop merging. - Week 2 — Endpoint pilot. Roll the MDM script to a 5–10 person pilot cohort. Confirm the compliance dashboard populates.
- Week 3 — Fleet-wide MDM. Expand MDM coverage to all managed hosts. Direct-egress reachability on the dashboard is still “reachable” at this point — that’s expected until Week 4.
- Week 4 — Network mandatory. Load the firewall deny-list. The dashboard shows
direct_registry_egress: blockedon every compliant endpoint. Chainsaw is now the only path.
Why the split matters
The friction point customers raise early is: “Why doesn’t Chainsaw just enforce this itself?”
The honest answer is that it can’t, and any proxy claiming otherwise is over-promising. A proxy knows only about requests that reach it. The moment a developer’s shell resolves registry.npmjs.org directly, the proxy is out of the loop regardless of how careful its policy engine is. Closing that gap requires either:
- MDM-level configuration lock (endpoint can’t reconfigure without root, and the policy is re-enforced on every boot), or
- Network-level egress control (the DNS / TCP connection fails before any HTTP conversation starts).
Both already exist as mature products. Rebuilding them inside Chainsaw would make the proxy worse at its actual job (supply-chain inspection and policy) while being worse at endpoint / network control than the vendors who specialize there.
So the story we tell customers is the story this tutorial walks through: use Chainsaw for what it’s good at — supply-chain decisions on every package request — and use your existing security stack for the layers it already enforces.
Related Topics
- Integrate Chainsaw with CI/CD pipelines (tutorial 21) — the deeper CI integration guide beyond the enforcement action.
- Manage Policy Precedence and Exceptions (tutorial 19) — once traffic is fully captured, tighten the policies that run against it.
- Use Audit Logs to Track Consumption and Policy Changes (tutorial 12) —
compliance_reported,compliance_drift, andbypass_attemptedevents show up in the same audit stream.