- Docs
- Security Model
Security Model
Validated structure, quoted path substitutions, a documented recipe-shell contract, an auditable network policy for fetch(), an access policy for env(), and hard budgets on manifest evaluation and file reads. A Netsukefile deserves the same care as a Makefile: Netsuke reduces common quoting and exhaustion mistakes, but it is not a sandbox.
Network Policy
The fetch() standard-library function downloads content from URLs at template evaluation time.
Because this happens before a build runs, Netsuke provides configurable network-policy controls
so that teams can audit and restrict what remote resources a manifest is allowed to contact.
--fetch-allow-scheme <SCHEME>
Allow additional URL schemes beyond the defaults. Repeat the flag to allow multiple schemes.
--fetch-allow-host <HOST>
Allow the provided hostname when default-deny mode is active. Wildcards such as *.example.com are supported.
--fetch-block-host <HOST>
Always block the provided hostname, even if it appears on the allowlist. Wildcards are supported.
--fetch-default-deny
Deny all hosts by default. Only hosts explicitly allowed by --fetch-allow-host may be contacted.
--trust-project-fetch-policy
Since v0.1.0-beta4
Allow project configuration to widen fetch-policy grants. The same opt-in is trust_project_fetch_policy = true in system or user configuration, or NETSUKE_TRUST_PROJECT_FETCH_POLICY=true. It is read only from operator-controlled layers, so no project file can authorize itself.
Project configuration only narrows
Breaking in v0.1.0-beta4Fetch policy does not follow ordinary configuration precedence. System and user configuration, NETSUKE_ variables, and CLI flags are operator policy; the primary project .netsuke.toml is an untrusted project request.
- A project
fetch_default_deny = trueturns default-deny on;falsecannot turn an operator's default-deny off. - Project
fetch_block_hostentries accumulate with every other layer, and a block always beats an allow. - Project
fetch_allow_schemeandfetch_allow_hostentries are ignored unless the operator has opted in. With the opt-in they are appended to the operator's own. - Files loaded through
extendsare not quarantined and keep ordinary file-layer precedence.
Every redirect is rechecked
Since v0.1.0-beta4fetch() applies the policy to the URL it was given and to every redirect destination before opening a connection. An HTTPS-to-HTTP redirect succeeds only when http is explicitly allowed and the destination host passes too.
301,302,303,307, and308are followed with GET semantics; relativeLocationvalues resolve against the current URL.- Chains stop after five redirects. A repeated destination, a missing or invalid
Location, and a refused destination each fail with their own localized diagnostic. - URL credentials are dropped when a redirect changes origin, and redirect diagnostics never print userinfo.
- With
cache=truethe entry is keyed on the original URL; a miss rechecks every hop before storing the body, and a hit makes no request.
$ netsuke \
--fetch-default-deny \
--fetch-allow-host releases.example.com \
--fetch-block-host internal.example.com \
build
Manifest environment access
Since v0.1.0-beta4
env() reads process environment variables while a manifest renders. Two lists of exact variable names decide which it may read. With neither list configured, env() stays default-allow, so existing manifests behave as before.
Configuration file
env_allow_var = ["CI", "PACKAGE_REGISTRY_TOKEN"]
env_block_var = ["AWS_SECRET_ACCESS_KEY"]
Environment (JSON arrays)
NETSUKE_ENV_ALLOW_VAR='["CI"]'
NETSUKE_ENV_BLOCK_VAR='["GITHUB_TOKEN"]'
CLI (one name, repeatable)
$ netsuke --env-allow-var CI \
--env-block-var GITHUB_TOKEN build
How the lists combine
- One effective
env_allow_varentry makes the allowlist restrictive: only listed names resolve. - An
env_block_varlist on its own blocks only its listed names. A block always overrides an allow. - Every entry is one exact name. There are no globs or patterns.
-
env_allow_varentries in the primary project.netsuke.tomlare removed before the policy is composed, so a checkout cannot grant itself access or switch on default-deny. Projectenv_block_varentries still count, because they can only restrict. Files loaded throughextendskeep ordinary precedence. - A refused lookup fails with “Access to an environment variable is blocked.” The
netsuke_manifest_env_lookups_totalcounter records each lookup byoutcome(success,blocked,not_present,not_unicode) and never the name.
The policy reduces how many secrets reach rendered manifest fields. It does not make rendered manifests safe to log; an allowed value still needs the usual output discipline. Full detail is in the upstream users' guide.
Resource budgets
Since v0.1.0-beta4
Netsuke bounds every manifest evaluation before it runs a build command or answers help targets. Each limit is a configuration key with a matching NETSUKE_ variable (the key in upper case) and a --manifest-* flag.
| Key and flag | Default | Bounds |
|---|---|---|
manifest_evaluation_fuel--manifest-evaluation-fuelNETSUKE_MANIFEST_EVALUATION_FUEL |
1,000,000 | MiniJinja instructions for one evaluation |
manifest_fuel--manifest-fuelNETSUKE_MANIFEST_FUEL |
100,000,000 | MiniJinja instructions for the whole manifest |
manifest_rendered_value_bytes--manifest-rendered-value-bytesNETSUKE_MANIFEST_RENDERED_VALUE_BYTES |
1 MiB | Bytes emitted by one rendered value |
manifest_rendered_manifest_bytes--manifest-rendered-manifest-bytesNETSUKE_MANIFEST_RENDERED_MANIFEST_BYTES |
16 MiB | Bytes emitted across all rendered values |
manifest_source_bytes--manifest-source-bytesNETSUKE_MANIFEST_SOURCE_BYTES |
4 MiB | Template and macro-import source consumed |
manifest_foreach_cardinality--manifest-foreach-cardinalityNETSUKE_MANIFEST_FOREACH_CARDINALITY |
10,000 | Values consumed from one foreach iterator |
manifest_expanded_entries--manifest-expanded-entriesNETSUKE_MANIFEST_EXPANDED_ENTRIES |
50,000 | Targets and actions emitted by expansion |
Who may change a limit
Set budgets in trusted configuration, the environment, or on the command line. A project .netsuke.toml, including its extends chain, may lower a ceiling but never raise one set by the defaults, user configuration, the environment, or the CLI. Zero, negative, or unparsable values are configuration errors, not silently ignored.
What a breach looks like
The failure reads Manifest resource budget exhausted with the stage and the numeric limit — never template text, context values, or rendered secrets. In JSON output its code is netsuke::runner::manifest_budget_exceeded.
Budgets reduce exhaustion exposure; they do not remove it. MiniJinja materializes a macro result before its size can be checked, so a macro can allocate past the per-value ceiling before the limit is reported. ADR-018 records the gap. Separately, MiniJinja 2.24.0 refuses a single repeated string above 100 MB with repeated string is too large.
Bounded file reads
Breaking in v0.1.0-beta4
The contents, linecount, hash, and digest filters share one read policy, so a checkout entry cannot exhaust Netsuke's memory or wedge a render.
8 MiB per read
The default budget matches the fetch() response limit. A call may pass max_bytes to lower it; a larger value is clamped to the ceiling. An oversized file fails with a diagnostic naming the path and the limit, never the contents. There is no configuration key or flag for the ceiling in v0.1.0-beta4; programs embedding Netsuke set it with StdlibConfig::with_file_max_read_bytes.
No final symlink
A symbolic link as the last path component is refused; a symlinked directory earlier in the path is fine. On Windows every reparse point is refused: junctions, mount points, and placeholder tags too. follow_symlinks=true waives only that check. A link whose target is written as an absolute path is still refused, because the workspace capability tests the link target, not where it resolves.
Regular files only
A FIFO or device node is refused once opened; on Unix the open is non-blocking, so a FIFO cannot stall the render first. contents and linecount also require UTF-8; hash and digest accept any bytes.
vars:
version: "{{ 'generated/version.txt' | contents(follow_symlinks=true) }}"
header_sum: "{{ 'fixtures/header.bin' | hash(max_bytes=1024) }}"
Recipe shells
Recipes are shell strings in v0.1.x. Which interpreter runs them is a fixed contract per platform, not a property of the shell that launched netsuke.
Structured executable arguments (RFC 0001) are planned for v0.2.0 and do not change the contract below.
Unix: /bin/sh -e
Each command list entry runs in its own brace group, joined with &&, and is passed to eval as a shell-quoted payload so a trailing # or & cannot break the chain. Groups run in the current shell, so a cd or variable assignment carries to later entries. An entry may start at most one background job; structured or nested exec forms are rejected during Ninja generation.
Windows: PowerShell by default
Every legacy recipe is handed to Windows PowerShell (powershell.exe, not pwsh) as an encoded, non-interactive, no-profile command. A command list shares one process and is checked with $LASTEXITCODE after every entry. Use PowerShell syntax: $name is a variable and $env:NAME reads the environment. {{ ins }} and {{ outs }} render as single-quoted PowerShell arguments. Recipes over Windows' command-line limit and up to 1 MiB are routed through a self-deleting .ps1 response file.
Windows: explicit Bash route
Set NETSUKE_WINDOWS_SHELL=bash to keep POSIX interpretation through Git Bash or MSYS2 (bash.exe on PATH). Netsuke verifies bash.exe --version before build or Ninja-tool execution and stops with install instructions otherwise. In CI, install Git explicitly, prepend its bin directory to PATH, and launch Netsuke from a pwsh step rather than relying on a workflow-wide shell: bash.
choco install git --yes --no-progress
$env:PATH = "C:\Program Files\Git\bin;$env:PATH"
$env:NETSUKE_WINDOWS_SHELL = "bash"
netsuke build
Markers and shell dollars
{{ ins }} and {{ outs }} are the only Netsuke markers for input and output paths, and the only values that are automatically shell-quoted. Everything else a template renders is ordinary command text. Since v0.1.0-beta3, shell dollar expressions are written normally: Netsuke lowers the markers first and only then escapes literal dollars as $$ in the generated Ninja file, so the shell receives them unchanged. Since v0.1.0-beta4 that holds in script: recipes too: $in, $out, $ins, and $outs are ordinary shell variables in both command: and script:.
Write this
targets:
- name: build/app
sources: src/main.c
command: >-
${CC:-cc} $CFLAGS {{ ins }} -o {{ outs }}
&& echo "built on $HOSTNAME"
$CC, $CFLAGS, and $HOSTNAME reach /bin/sh as literal shell variables. On the PowerShell route, write $env:CC instead; PowerShell does not perform POSIX ${VAR:-default} expansion.
Migrating a beta2 or beta3 manifest
- Replace the historical spelling
$$PATHwith$PATH. On POSIX and Bash routes$$is the shell's process identifier; in PowerShell it is the last token of the previous command, so the extra dollar now changes the result. - Replace any
$inor$outpath placeholder with{{ ins }}or{{ outs }}. Beta3 still rewrote them inscript:recipes; since v0.1.0-beta4 they reach the shell as ordinary variables there too, as$insand$outsalways have. - On POSIX and Bash routes a marker may appear unquoted, single-quoted, or double-quoted, but not inside a command substitution or backticks. In PowerShell, use markers unquoted; quoted and command-substitution sites are rejected.
- Build and default-target paths escape a space as
$, Ninja's own path escape. They reject|and control characters, which Ninja cannot represent, and also$and colons, which Ninja could escape but Netsuke does not yet accept. - A literal
$in adescription,depfile,deps, orpoolis doubled, so it prints as written: a description ofCC $outshowsCC $out, not the output path. Newline, carriage return, and NUL are rejected in those fields.
Security Model
Safe by DefaultNetsuke validates every manifest against a schema before template expansion runs, quotes the path markers it owns, and rejects the characters Ninja cannot represent unambiguously. Diagnostics are typed, so validation failures point to the exact manifest location rather than surfacing as opaque errors.
YAML first
The manifest must be valid YAML before any template expansion happens. Structural mistakes fail early, before commands are generated.
Quoted path markers
{{ ins }} and {{ outs }} are quoted as path arguments for the selected recipe shell. Other rendered Jinja values are not automatically shell-quoted, and the shell_escape filter described in older drafts is not implemented in v0.1.0-beta4: handwritten shell fragments remain the author's responsibility.
Typed diagnostics
CLI errors are designed to carry context, hints, and localized output rather than dropping raw parser failures on the floor.
Scoped glob authority
Since v0.1.0-beta2, the filesystem capability used for glob() metadata checks is opened at the pattern's longest literal directory prefix — not at the filesystem root or the working directory — so glob expansion holds only the authority its pattern can reach. Since v0.1.0-beta3, the Jinja helper also rejects any matched path that is not a portable unquoted shell word, so an untrusted checkout filename cannot become shell syntax when item is interpolated into a recipe.
A pattern whose literal prefix is missing, or names something other than a directory, expands to no matches. A match reached through a symbolic link that resolves outside the prefix, or dangles, is skipped rather than failing the expansion; a cyclic symbolic link still fails it, and a literal prefix that is itself a symbolic link cannot establish the capability, so the expansion fails. Glob tracing redacts caller-controlled path fields as <redacted>.
Manifest content stays out of logs
Since v0.1.0-beta4At DEBUG and TRACE, graph-generation tracing no longer emits the manifest AST, target or dependency names, recipe commands, variable values, macro bodies, or paths. What remains is bounded: a span, fixed recipe_shell, outcome, and error_category fields, and count-only metrics. There is no replacement log, because rendered values can carry secrets that env() interpolated. Anyone who scraped --verbose output for manifest detail needs another route.
Vulnerability Mitigation
Qualitative comparison based on documented safety features; no numeric mitigation claim is implied.
Manifest schema validation, quoted path markers with shell-safe glob results, and typed diagnostics give Netsuke stronger qualitative mitigation than ad hoc scripts on each documented boundary. Interactive charting requires JavaScript.