Every GitHub Actions job receives an execution environment; many also receive a repository-scoped token. The question is not simply whether a workflow uses secrets. It is what each job can read or change if a script, dependency, pull-request value, or third-party action behaves unexpectedly. This audit builds a permission map, removes unnecessary writes, and reruns the workflow to prove that the reduced configuration still performs its intended task. It is a repository change with a testable result, not a generic instruction to “use least privilege.”

The workflow under review

Create a branch and record the commit. List every file under the repository’s workflow directory, including reusable workflows called from another repository. For each workflow, record its triggers, whether it handles forked pull requests, the runner type, and any environment protection. A scheduled documentation build and a release job that publishes packages should not share the same review row.

Search for top-level and job-level permission declarations. GitHub allows permissions to be set broadly and narrowed per job, so the effective value can be missed when only one block is read. Also list every use of the token, whether passed explicitly or consumed by an action through the runtime context.

A job-by-job permission map

Use one row per job. Start with the documented permission categories and mark only the operations observed in the workflow.

JobTriggerRequired operationCandidate permissionExternal code
testpull_requestread source and checkscontents: readcheckout, setup action
publishreleaseupload packagepackages: writeregistry action
commentissue eventcreate one commentissues: writescript or API call

Do not infer that a write permission is required because a workflow has always had it. Find the exact API operation, command, or action documentation. Split jobs when a broad token exists only because one late step publishes an artifact. Isolation makes the permission boundary visible and limits what earlier steps inherit.

Third-party actions are executable dependencies

An action referenced by tag can change when the tag moves. GitHub’s secure-use guidance recommends pinning third-party actions to a full-length commit SHA. Record the repository, current reference, resolved commit, publisher, release notes, and why the action is needed. A marketplace badge is discovery information, not a source review.

Read the action metadata and the scripts it invokes. Look for shell construction from event fields, package installation at runtime, network calls, artifact uploads, and writes to the workspace or GitHub environment files. Untrusted pull-request text must not become executable code through string interpolation. If the review cost exceeds the benefit for a small step, replace the action with a short, readable command or an official action whose behavior you can bound.

Why start from deny-by-default?

Set workflow or job permissions to an empty or read-only baseline according to the documented syntax. Add the narrow permission only where the failing operation demonstrates it is needed. Keep publishing credentials behind protected environments and avoid exposing secrets to code from forks.

Test three paths: the normal success case, an untrusted pull request when applicable, and a failure case before publication. Confirm that logs do not print tokens or sensitive request data. Confirm that the reduced token cannot perform a write outside the intended job. The test evidence should include the workflow run URL, commit, effective permissions, and expected failed operation from a deliberately underprivileged branch.

Trigger context changes the threat model

The same job can have a different risk profile under push, pull_request, pull_request_target, workflow_run, or a manually dispatched event. Record which revision supplies the workflow definition and which revision supplies the code being executed. Pay special attention when a privileged event consumes artifacts or metadata produced by an untrusted run. Treat branch names, issue text, commit messages, matrix values, and changed file paths as untrusted strings. A narrow token limits damage, but it does not make unsafe shell construction or a confused cross-workflow handoff correct.

Runner risk is a separate boundary

A narrow token does not make a self-hosted runner safe. Persistent machines can retain files, credentials, processes, or network reach between jobs. Record whether the runner is ephemeral, what network it can reach, and what cleanup occurs. Similarly, cloud credentials obtained through identity federation need their own scope and audience review; reducing GITHUB_TOKEN does not reduce an unrelated cloud role.

When does the audit expire?

Require a permission review when a workflow trigger changes, a new external action is added, a job gains deployment access, or a runner changes. A small repository rule can flag unpinned actions and broad write permissions, but the human record still needs to explain why a capability exists.

The finished audit is successful when the workflow passes with fewer privileges and the remaining writes are tied to named operations. That is stronger evidence than a policy sentence because a future reader can see the exact job, permission, pinned dependency, and test run that justify the access.