On this page
ADR-0035: Approval decisions are human-only; service accounts never hold them
Status: Accepted (WP-70a, WP-70b; security review finding HIGH-2). Implemented in crates/commonlayer-core/src/authz/ (Permission::HUMAN_ONLY, Actor::require_human, Actor::require_human_session) and the deciding services.
Context
Several rules put a second person in the loop: a reward above the single-approval limit
needs two approvals, an exception is decided by someone other than its requester, a
payout run is approved by a second administrator. The independent security review of
2026-11-10 showed that one project admin could satisfy all of them alone: project
admins can create project service accounts with the project_admin or maintainer role
and give them full-scoped tokens, and the separation-of-duty checks compared only
principal ids, so two service accounts counted as two other people.
Decision
- A fixed set of approval-type permissions is human-only:
platform.maintainers.approve,platform.eligibility.override,platform.payouts.approve,platform.ledger.adjust,project.security.exceptions.decide,project.contributions.review,project.rewards.approve.Actor::cannever grants them to a service-account principal, whatever role it holds. Workload (CI) actors only ever holdproject.artifacts.submit. - The deciding service methods additionally require a person:
Actor::require_human(a person, with a session or an API token) for exception decisions,not_affectedconfirmations, contribution reviews, task waivers, reward approvals and abuse-report resolution;Actor::require_human_session(a person in an interactive session, never an API token) for payout-run approval and payout resolution, ledger adjustments, maintainer assignment approval and rejection, appeal resolution, eligibility overrides, and creating organization or project service accounts.
- Refusals answer
403 authz.human_requiredor403 auth.interactive_session_required.
Consequences
- What an API token can never do is now two lists: the never-by-token permissions
(
platform.ledger.adjust,platform.payouts.approve,platform.users.manage,platform.settings.manage, credential changes) and the decisions above that need a session. A service account cannot approve anything at all. - Automation can still propose, request, submit and read; every approval is a person's audited act.
- The rule is enforced in services, so REST and gRPC behave the same. Regression tests:
authz_matrix::service_accounts_never_hold_human_only_permissions,confirmations::a_service_account_cannot_decide_an_exception,human_decisions::*,tokens::project_service_accounts,hardening::reward_and_payout_approvals_need_a_person. - Not done: treating a service account as its creator in separation-of-duty checks (the review's optional suggestion). It is unnecessary while service accounts cannot approve.