Skip to content

feat: scheduled grants: approve now, use later, with an end time and a number of uses #108

Closed

Description

@groupsky

Why

A job that runs later (a timer, a job runner) cannot tap a button. Today there are two ways:

  • a "never ask" rule in broker.toml (DESIGN.md decision 3 B): it needs a config edit, and it stays open until the owner removes it;
  • a tap at run time, often at night, when the owner sleeps. The job waits or fails.

The owner often knows hours ahead that a job will need a secret: at the time the job is planned. A tap at that time, while the owner is awake, should still be valid when the job runs, and end by itself.

#103 step 9 already plans a one-use pre-approval for a login that lasts about 60 s. This issue is the same idea for any secret or SSH key, with a start time in the future, an absolute end time and a number of uses.

Idea (decide the design when the work starts)

  1. Request. A new client command, for example:

    nodkey preapprove --secret backup-token \
      --from 2026-10-28T03:00 --until 2026-10-28T04:00 --uses 1 \
      --reason "job nightly-backup"

    It hands out nothing. It only asks for a tap. Times need a UTC offset, or are read as broker local time.

  2. Message. For example: "approve 1 use of backup-token from laptop, between 03:00 and 04:00 on 2026-10-28. Reason (claimed): job nightly-backup". Buttons: Approve and Deny. After Approve, the message keeps a Revoke button until the grant is used up or ends.

  3. Scheduled grant. The broker keeps (client, secret, not before, not after, uses left). For an SSH key: (client, key, host key, remote user), as nodkey ssh-grant does. Times are absolute, so summer time does not change them. The message shows broker local time.

  4. Use. A request from the same client, for that secret, inside the time, takes one use without a tap. Each use sends the silent notice, as a "never ask" use does (section 12). When no uses are left, or the time ends, the grant is gone.

  5. Not used. A grant that ends with uses left sends a notice ("not used"). The job did not run, or ran late.

  6. Limits (config, with safe defaults): how far ahead a grant may start (for example 7 days), how long it may be open (for example 8 hours), the most uses (for example 10), and the most pending grants per client.

  7. Rules. A secret allows it only when its rule says so (preapprove = true; off by default). As for any grant, it starts only after its decision entry is on disk (sections 8 and 9). If the notice of a use cannot be sent, the use is denied (section 12), and these notices count toward the limits of 6 per client and 20 in all, in any 60 seconds.

  8. Audit log. New events: the request, the decision, each use, the end with uses left, and revoke.

  9. Job identity (later, feat: job identity: tokens signed by a job runner, so rules and grants can name one job #109). With signed job tokens, a scheduled grant can also name a job (name and hash of its definition), or one run. Then only a request with a matching token uses it.

The honest limit

With the client as the only identity, anything on that client can use the grant inside its time. This is decision 3 B again, but smaller: it is made by a tap and not by a config edit, it ends by itself, and its uses are counted. If something else uses it up, the real job fails, and that is a visible sign.

Two more limits:

  • The agent can ask for a pre-approval at any time, with any reason, such as "job nightly-backup". The reason is claimed. The approver cannot check that such a job exists. A tap must be read as "this client may use this secret in this time", nothing more.
  • After a use, the secret stays on the agent machine (THREAT-MODEL.md section 5, item 1).

Open questions

  1. Broker restart. A restart ends all grants today. Keep scheduled grants across a restart (saved state, checked against the audit log) or drop them, so the job asks as usual? Proposed: drop them, and send one notice that lists the dropped grants.
  2. Several secrets in one run (nodkey run --secret A_VAR=A --secret B_VAR=B): one pre-approval for all of them, as one window tap makes one grant for each secret?
  3. Cancel by the client without a tap, when the job is removed. Proposed: yes, because a cancel can only take rights away.
  4. The broker clock set back inside the time of a grant: the uses already made still count, as for max_per_day.
  5. A later Deny. Section 8 says a Deny also stops a grant from an earlier tap that has not started yet. A scheduled grant is such a grant. Does a later Deny for the same client and secret cancel it? Proposed: yes, and the message says so.
  6. The window of a rule. Section 8 says a grant is never longer than the rule's window. Is a scheduled grant's open time also capped by window, or only by the new limits in step 6?

Done when

  • DESIGN.md has scheduled grants: the request, the message, what the broker keeps, use, end, revoke and the audit events. THREAT-MODEL.md has the new threat: a planned grant used by something else on the same client.
  • A scheduled grant is used only by the same client, for the named secret or key, inside its time, and at most N times. A test for each, and a test for the exact end time.
  • Tests for the limits: how far ahead, how long open, uses, pending grants per client. Tests for what is decided about a later Deny and a broker restart.
  • Revoke on the phone and cancel by the client work. Tests.
  • A grant that ends with uses left sends a notice. A test with the fake Telegram server.
  • docs/setup.md shows a job that asks for its pre-approval when it is planned.
  • It gets the security review, as every change does.

Related: #103 (one-use pre-approval for a login, step 9), #29 (reload the config without a restart).

Activity

  1. added this to the later milestone on Oct 27, 2026
  2. groupsky commented on Oct 27, 2026

    @groupsky
    ContributorAuthor

    One grant core with #103 step 9 and standing grants (#110): see the comment on #103. Open question 1 (broker restart) must match #110, where standing grants survive a restart.

  3. groupsky commented on Oct 27, 2026

    @groupsky
    ContributorAuthor

    Grants that survive a broker restart: #111 (for later). It answers the restart open question here.

  4. groupsky commented on Oct 28, 2026

    @groupsky
    ContributorAuthor

    Done in two pull requests: #112 (part 1, 102d0bf) and #114 (part 2, 89d07e9). CI was green on both.

    Done when, box by box:

    • DESIGN.md and THREAT-MODEL.md.
    • Same client, named secret or key, inside its time, at most N times.
      • internal/approval/preapprove_test.go:
        • TestScheduledGrantScope: same client and named secrets only.
        • TestScheduledGrantKey: named hosts and the one remote user; each login takes one use.
        • TestScheduledGrantTime: the first moment is inside, and the exact end time is outside.
        • TestScheduledGrantUses: at most N uses.
        • More: TestScheduledGrantSeveralSecrets, TestScheduledGrantDoesNotMix, TestScheduledUseConcurrent, TestScheduledUseClockSetBack, TestScheduledUseWallClockForward.
    • Limits, later Deny, restart.
      • Limits:
        • TestPreapproveLimits: max_ahead, max_open, max_uses, at the border and one over.
        • TestPreapproveMaxGrants: grants per client.
        • TestPreapproveLimitsFromConfig, TestLoadBrokerPreapproveLimits, TestPreapproveOffByDefault, TestPreapproveRules.
      • Later Deny: TestScheduledGrantDeny, TestScheduledGrantDenyBeforeStart, TestGrantEndChangesMessage, TestCoreGrantEndMessages.
      • Restart (dropped, one notice): TestScheduledGrantStops, TestStopNoticeListsGrants, TestStopNoticeNoGrants, TestStopNoticeTelegramSilent, TestStopDeniesBeforeTheNoticeGoesOut.
    • Revoke on the phone and cancel by the client.
      • Revoke: TestRevokeEndsGrant, TestRevokeRefused, TestRevokeBeforeStart, TestPreapproveRevoke.
      • Revoke with the fake Telegram server: TestCoreRevoke, TestCoreRevokeRefused, TestCoreRevokeSkipsEditedCheck, TestCoreRevokeOnWaitingMessage.
      • Cancel: TestScheduledGrantCancel, TestPreapproveCancel, TestPreapproveCancelCommand, TestPreapproveCancelLeftover.
    • "Not used" notice, tested with the fake Telegram server. TestCoreNotUsedNotice (internal/telegram/revoke_test.go), plus TestNotUsedNotice and TestNotUsedNoticeLimits.
    • docs/setup.md. Section 11 shows a nightly job that asks for its pre-approval when it is planned (nodkey preapprove --secret backup-token ...). It also says that each SSH connection takes one use, so a tool with many connections needs ControlPersist or more uses.
    • Security review. The security-reviewer agent ran on each pull request and on each round of fixes. There were no critical or high findings. Every finding was fixed; the lists are in the descriptions of feat: scheduled grants with nodkey preapprove (part 1 of #108) #112 and feat: revoke button and notices for scheduled grants (part 2 of #108) #114.

    Two notes, not boxes:

    Later work: #110 (standing grants), #111 (grants that survive a restart).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions