Skip to content

Drop the sh and runuser -c sudo grants: the file's own TODO has never been filed #417

Description

@vladimirrott

packaging/sysknife-sudoers:111 carries this, and the file says plainly what it
means:

$ sed -n '100,111p' packaging/sysknife-sudoers
# SECURITY NOTE — this is the widest grant in this file, and it means sudoers is NOT the
# security boundary. `sudo sh -c '<script>'` has no argument constraint, so the sysknife
# daemon is effectively root-capable. The real boundary is the daemon itself: IPC
# caller-role authorization (SO_PEERCRED), the one-time approval interlock, per-argument
# input validation (actions/validate.rs), and the signed audit chain. The narrow grants
# above limit the blast radius of an *argv*-construction bug in a specific action; they
# cannot contain a `sh -c` script — every script the daemon passes to `sh -c` is built
# only from values already run through the validators in validate.rs.
# TODO(post-launch hardening): refactor ConfigureFirewall + the ssh key ops to shell-free
# argv forms so this grant (and the runuser `-c` grant below) can be dropped.
sysknife ALL=(root) NOPASSWD: /usr/bin/sh

The runuser grant twenty lines below says the -c form "adds no privilege
beyond the existing sh grant", which is true and is the other half of the same
knot.

This issue is that TODO. It has never been filed, so nothing tracks it and it
does not appear in any release checklist.

Nothing here is a disclosure: the grant is deliberate, documented in the file it
lives in, and the compensating controls are named. What is missing is a work item
with the remaining call sites enumerated.

What the grant is currently holding up

Two families, per the note:

  • ConfigureFirewall
  • the ssh key operations, AddAuthorizedKey and RemoveAuthorizedKey, which go
    through runuser -u <user> -- sh -c '<script>' sh <key> <path> so the edit
    happens as the target user and a symlink planted at ~/.ssh/authorized_keys
    cannot redirect a root write. That property was won in AddAuthorizedKey follows an attacker symlink, appending as root anywhere #145 and must survive
    any refactor: whatever replaces sh -c still has to drop privilege to the
    owning user before touching the file.

Start by enumerating every site rather than trusting this list. The grant is
whole-binary, so grep for the shell invocation in crates/sysknife-daemon
rather than for the two action names.

Scope

  • Enumerate every daemon call site that reaches sh -c or runuser -c, and say
    in the issue thread what the real list is before writing code. If it is longer
    than the note claims, that is the finding.
  • Convert each to a shell-free argv form. Where a pipeline or redirection is the
    reason for the shell, a small helper in packaging/ with a fixed argv and an
    argument-restricted grant is the pattern; packaging/sysknife-firewall-state
    and its two nft list ruleset grants show the shape.
  • Keep the drop-to-target-user property for the ssh operations. A refactor that
    reintroduces a root write to a user-owned path re-opens AddAuthorizedKey follows an attacker symlink, appending as root anywhere #145.
  • Only then remove the two grants, and add a test that fails if either returns.
    The removal is the deliverable; the refactor is the work.

Difficulty

Hard, and the largest security item on this tracker. It is also the one that
changes what the sudoers file means: today it documents that it is not the
boundary, and finishing this is what would let it become one.

Activity

  1. added
    enhancementNew feature or request
    help wantedExtra attention is needed
    hardDifficulty: crosses a trust boundary or needs hardware
    on Sep 10, 2026
  2. vladimirrott commented on Sep 10, 2026

    @vladimirrott
    MemberAuthor

    @QinXi-ai four pull requests in one night, and the reviews are on each of them. Three are approved; #413 has one blocking item and a baseline bump.

    Below are three issues I am holding for you. This is a batch, not a queue: take them in any order, take one, or take none. Declining any of them costs nothing and I will not ask twice. I have assigned them so they show on your dashboard rather than only on these threads, and one word hands any of them back.

    Every finding was re-measured today at f5dffd8 before I offered it, because an issue that no longer reproduces wastes an evening in the worst way.

    #417 — drop the sh and runuser -c sudo grants. Hard, and the largest security item on the tracker. packaging/sysknife-sudoers:111 grants NOPASSWD: /usr/bin/sh, and the file's own SECURITY NOTE says what that means: sudoers is not the boundary, the daemon is. There is a TODO(post-launch hardening) naming ConfigureFirewall and the ssh key ops as the call sites holding it up, and that TODO has never been filed, so nothing tracks it. You are the obvious person: in #413 you wrote the pattern it needs, a helper with a fixed argv and two argument-restricted grants instead of a wildcard on the binary. The AddAuthorizedKey path has a property that must survive the refactor, and #145 is why.

    #416 — six Ufw* actions still say "Ubuntu only". Easy. #384 moved ufw out of the Ubuntu fence and #412 makes Debian eligible, so all eight are in the Debian catalogue. You dropped the phrase from the two you touched in #415; the other six kept it, and that text reaches the model. The part worth doing is the third scope bullet, a test that derives the claim from the production lists so the next re-partitioning cannot leave prose behind.

    #346 — ci-local.sh misses seven of the scripts CI runs. Medium. I re-ran the comparison today and the number in the title is still exact:

    CI runs 29 script(s); ci-local mentions 25
    missing: cassette-replay-parity, codex-plugin-manifest, grub-kargs-edit,
             log-edit, mount-edit, no-secrets, rmswap
    

    Somebody running ci-local.sh before pushing gets a green board and a red PR. It was offered to @xianjianlf2 on 2 September and released this morning with nothing owed.

    On your four: the only pairwise conflict among them is CHANGELOG.md, one hunk each, and I will resolve those at merge rather than making you rebase four times. Only #413 moves the test count, so merging it last means it is the only baseline regeneration anyone has to do.

  3. added
    claimedSomeone has said in the thread that they are working on this
    on Sep 10, 2026
  4. vladimirrott commented on Sep 10, 2026

    @vladimirrott
    MemberAuthor

    Correction to the comment above, before anyone acts on it.

    I said I had assigned these so they would show on your dashboard. The assignment did not take. GitHub returns success for POST /assignees and then silently drops anyone who is not a repository collaborator and has not already posted on that specific issue, which is the case here. My tooling read the assignee list back afterwards, found it empty, and refused to report success, which is the only reason I know.

    So the reservation is the claimed label plus this thread, and that is visible to everybody reading the tracker but not on your own dashboard. The moment you reply on one of these threads, the assignment becomes possible and I will apply it.

    Nothing else in that comment changes. The issues are held for you, and declining any of them still costs nothing.

  5. QinXi-ai commented on Sep 14, 2026

    @QinXi-ai
    Contributor

    I will take this. Before implementation, I enumerated daemon command construction at main 61b3a87 rather than relying on the TODO. The list is longer:

    • sudo sh -c: ConfigureFirewall, AddUserToGroup, RemoveUserFromGroup, and SnapInstall when auto_update=false.
    • sudo runuser -u ... -- sh -c: AddAuthorizedKey and RemoveAuthorizedKey.
    • sudo runuser -l ... -c: six Podman actions and three Toolbox actions.
    • The unrestricted runuser grant also serves the shell-free Flatpak paths; those must move to a bounded helper too before removing the grant.
    • Separate unprivileged bash -c paths: AptListUpgradable, AptHistoryList, and CheckPendingReboot. Executor shell invocations below cfg(test) are process-control fixtures, not production action constructors.

    I will replace the production shell paths with fixed argv/operation-specific helpers, retain the group-existence guards and snap install-then-hold failure sequencing, and drop supplementary groups/GID/UID to the resolved target account before any SSH key file access. User-scoped container/Flatpak execution will use an explicit target-user environment and fixed tool/subcommand allowlists, with no arbitrary command parameter. Both whole-binary grants will be removed and guarded against reintroduction. Codex is assisting; Linux/runtime evidence will be reported separately from Windows-local checks.

  6. added a commit that references this issue on Sep 15, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

claimedSomeone has said in the thread that they are working on thisenhancementNew feature or requesthardDifficulty: crosses a trust boundary or needs hardwarehelp wantedExtra attention is needed

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions