diff --git a/docs/design/dann-deployment-threat-model.md b/docs/design/dann-deployment-threat-model.md index 7397db7ae..d4f65052e 100644 --- a/docs/design/dann-deployment-threat-model.md +++ b/docs/design/dann-deployment-threat-model.md @@ -29,13 +29,14 @@ operator-managed end to end: - The audit ledger is service-owned, and root can re-chain it from genesis (the chain has no external anchor), so **audit is not tamper-evident against the operator.** -- sudoers applies tags last-match-wins, so the `SETENV` tag on the wheel rule - (`%wheel ALL=(ALL:ALL) NOPASSWD:SETENV: ALL`) wins for the operator regardless - of the module's own NOPASSWD grant lines (which carry no `SETENV` tag). The - real route is `sudo -u env VAR=… $BROKER …`, which reaches the broker - with caller-chosen variables that `purge_ambient_env` does not cover. This is - moot while the operator is root — which is the point — and it is a reason - never to deploy this pattern on a host where the operator is *not* already +- sudoers applies tags last-match-wins. For the broker command itself the + module's line matches last and carries no `SETENV`, so a `sudo VAR=… -u + $BROKER …` is refused; the wheel rule's `SETENV` + (`%wheel ALL=(ALL:ALL) NOPASSWD:SETENV: ALL`) still covers the `env` form, + `sudo -u env VAR=… $BROKER …`, which reaches the broker with + caller-chosen variables that `purge_ambient_env` does not cover. This is moot + while the operator is root — which is the point — and it is a reason never to + deploy this pattern on a host where the operator is *not* already root-equivalent. So on dann, sudo-secretspec provides exactly two things, and it is worth naming diff --git a/nix/module.nix b/nix/module.nix index 233977af6..722ed5d86 100644 --- a/nix/module.nix +++ b/nix/module.nix @@ -229,19 +229,19 @@ in # The whole policy — the `Defaults!` line *and* the operator's NOPASSWD # grant lines — comes from `sudoersText`, the IFD derivation that runs the # binary's own `print-sudoers`. `security.sudo.extraConfig` is - # `types.lines`, merged by plain `concatStringsSep "\n"` with no - # `sortProperties` pass, so `mkOrder`/`mkAfter` carry no weight here; what - # actually orders it is nixpkgs' `sudo.nix`, which appends `extraConfig` - # LAST — after the stock `%wheel` `extraRules` block. sudoers is - # last-match-wins, so these grants are the final lines and take effect over - # `%wheel`'s password prompt. The only residual is ordering against a - # *second* module that also sets `extraConfig`: nixpkgs emits both in the - # order its `concatStringsSep` sees them, and whichever lands later wins. - # That is an availability-only risk (a later rule could re-prompt for a - # verb), never an escalation — it cannot add a grant this module did not - # already emit. There is no `timestamp_timeout=0` line and no `doctor` grant - # on Linux: the former has no client path to attach to, the latter refuses - # on Linux. - security.sudo.extraConfig = builtins.readFile sudoersText; + # `types.lines`; `lib.mkAfter` sorts this definition after other modules' + # `extraConfig` lines (nixpkgs runs `sortProperties` whenever any + # definition carries `_type == "order"`, before the type's merge), and + # nixpkgs' `sudo.nix` appends `extraConfig` LAST — after the stock `%wheel` + # `extraRules` block. sudoers is last-match-wins, so these grants are the + # final lines and take effect over `%wheel`'s password prompt. The only + # residual is ordering against a *second* module that also sets + # `extraConfig` with `mkAfter` (or a later `mkOrder`): whichever sorts + # later wins. That is an availability-only risk (a later rule could + # re-prompt for a verb), never an escalation — it cannot add a grant this + # module did not already emit. There is no `timestamp_timeout=0` line and + # no `doctor` grant on Linux: the former has no client path to attach to, + # the latter refuses on Linux. + security.sudo.extraConfig = lib.mkAfter (builtins.readFile sudoersText); }; }