Add nono (Landlock) sandbox for Duo Workflow behind feature flag
What does this MR do and why?
Adds the nono (Landlock-based) sandbox as an alternative to SRT/bubblewrap for Duo Agent Platform ambient flow execution, behind the duo_workflow_nono_sandbox feature flag (gitlab_com_derisk, default disabled).
Why nono? SRT relies on Linux user namespaces (unshare), which require CAP_SYS_ADMIN or unprivileged user-namespace support. On hardened runner images that run as a non-root UID without those capabilities, SRT silently falls back to running the executor unsandboxed. nono uses Landlock (an unprivileged LSM available since Linux 5.13) - it needs no namespace, works for any UID, and fails closed once running rather than silently bypassing the sandbox. It also has ~15ms overhead.
Changes
-
ee/lib/gitlab/duo_workflow/sandbox.rb:wrap_commandnow branches towrap_command_nonoorwrap_command_srtbased on the flag. Both are private methods. The SRT path is byte-for-byte unchanged when the flag is off. Two new private helpers (computed_domains,build_nono_flags) reuse the existingcombine_settings/parent_settings/minimal_allowlisted_domainslogic so the nono policy stays in sync with SRT without duplicating code. -
Landlock capability pre-check:
wrap_command_nononow probes the real policy withnono run <policy> -- truebefore running the executor, mirroring the SRT path'ssrt --settings ... true. Because it exercises the actual grants it also catches a grant that overlaps one of nono's own deny rules (which makes nono refuse to start), not just a missing or too-old kernel. On failure it prints nono's own output and exits 1 instead of running the command unsandboxed. Neither the pre-check nor the executor run uses--silent, so a refused grant is distinguishable from a missing Landlock ABI and nono's grant warnings and enforcement summary reach the job trace. -
Fail closed on an empty egress allowlist: nono only switches to default-deny once at least one
--allow-domainis passed, so an empty computed allowlist would have left the network unrestricted - the opposite of SRT, which treats an emptyallowedDomainsas "allow nothing".wrap_command_nononow emits an error andexit 1instead of running unsandboxed. Raised by @allparts in the AppSec review below. -
ee/config/feature_flags/gitlab_com_derisk/duo_workflow_nono_sandbox.yml: Newgitlab_com_deriskflag,default_enabled: false, actor-scoped on project, milestone19.4, groupgroup::agent foundations. -
ee/spec/lib/gitlab/duo_workflow/sandbox_spec.rb: All existing SRT tests are wrapped in acontext 'when duo_workflow_nono_sandbox flag is disabled'withstub_feature_flags(duo_workflow_nono_sandbox: false). New nono context covers: command structure,--allow-domain/--deny-domainflag generation, shell-escaping of domain values, the empty-allowlist, pre-check and nono-missing fail-closed paths, and absence of SRT constructs.
nono policy mapping
| SRT config | nono CLI flag |
|---|---|
filesystem.allowWrite ["./", "/tmp"] |
--allow . + --allow /tmp. Deliberately not --allow-cwd, which grants the CWD read-only and would leave .git/ unwritable |
filesystem.allowGitConfig: true |
the --read-file/--read grants over git's config include chain, glab credentials and git's exec path (NONO_FS_GRANTS) |
filesystem.denyRead ["~/.ssh"] |
nono built-in sensitive-path denylist |
filesystem.denyWrite [SANDBOX_SYSTEM_DIR] |
no counterpart needed - no settings file is written under nono, and /var/tmp is not granted, so it is deny-by-default |
network.allowedDomains |
--allow-domain <domain> (shell-escaped). Wildcards are supported: *.host matches subdomains but not the bare host, which is why minimal_allowlisted_domains emits both |
network.deniedDomains |
--deny-domain <domain> (shell-escaped), evaluated before the allowlist |
Grants are leaf paths only: Landlock cannot enforce a deny nested under an allowed parent, so nono refuses to start if a grant contains one of its own deny rules - which rules out $HOME and $HOME/.config as grants.
allowAllUnixSockets has no 1:1 nono flag yet; tracked as follow-up.
Executor image note
The current executor image ships srt, not nono. When the flag is enabled, the image must provide nono on PATH (custom image or setup_script).
Both of these fail closed (exit 1), unlike the SRT path, which falls back to running unsandboxed:
nonois not on PATH.- the capability pre-check fails - Landlock unavailable (kernel < 5.13 or compiled out), or a grant that nono refuses.
These were originally fail-open for SRT parity. They were changed to fail closed in c29f17d1 at AppSec's request (!252331 (comment 3812424596)): nono is opt-in, so the duo_workflow_nono_sandbox flag is itself the opt-out, and disabling it restores the SRT path.
Hardening the install (pinned install with checksum verification) and a customer-facing opt-out apply equally to the existing SRT path and are tracked separately in #624831 (closed), so they are deliberately out of scope here.
User-facing documentation updates will follow at rollout time (flag is off by default).
References
- Feature issue: #605838 (closed)
- Prior PoC MR: !249393 (closed) (branch
aherczeg/poc-nono-sandbox, @AndrasHerczeg) - Rollout issue: #624259
- nono project: https://github.com/nolabs-ai/nono (Apache-2.0)
Follow-ups agreed during the AppSec review:
- Fail closed when no sandbox binary is available: #624831 (closed)
- Sandbox probe-corpus test framework in the executor image repo: gitlab-org/duo-workflow/default-docker-image#21
Screenshots or screen recordings
N/A - no UI changes.
How to set up and validate locally
-
Enable the flag for a project in rails console:
Feature.enable(:duo_workflow_nono_sandbox, Project.find(<id>)) -
Ensure
nonois on PATH in the executor image (or install it insetup_script).setup_script: - curl -fsSL https://nono.sh/install.sh | sh - export PATH="$HOME/.local/bin:$PATH" -
Trigger a Duo Workflow ambient session and confirm the executor runs under
nono run. -
Disable the flag and confirm the SRT path is used unchanged.
MR acceptance checklist
Evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.