Skip to content

Check affect of changing default workflow permissions to match GitHub's security recommendations #8178

Description

@t-will-gillis

Overview

We need to change the permissions for the default GITHUB_TOKEN from read/write to read only per GitHub's recommendation for security best practice.

Details

Before proceeding, read the explainer below.

We need to audit each of our workflows to identify exactly what permissions are needed at each level of the workflow, i.e. overall, job-level, and step-level.
This issue has three objectives:

  • First, we will reduce the permissions of the default GITHUB_TOKEN.
    • At the top level of each workflow's YAML explicitly state the default token's permissions:
      # Set defaults for GITHUB_TOKEN 
      permissions:  
        contents: read  
        issues: read
    • Next, we will analyze and test each workflow to check whether additional permissions are required at the step-level to ensure each workflow functions as expected, and note whether the permission requires a PAT.
  • This information will be itemized on a spreadsheet for each step of each workflow.
  • see additional comments and "Permissions audit" following

TODO

  • after completion of all, reduce default permissions at bottom of https://github.com/hackforla/website/settings/actions
  • three action items from Security audit
    • admin:org_hook on HACKFORLA_BOT_PA_TOKEN and HACKFORLA_ADMIN_TOKEN — No workflow creates, updates, or deletes webhooks. This scope appears unused on both tokens and can likely be removed after testing.
    • repo vs public_repo on HACKFORLA_GRAPHQL_TOKEN — repo grants full access including private repos. If all targeted repos are public, this could be narrowed to public_repo. Requires testing to confirm no operation depends on the broader scope.
    • pr-verification.yml — pull_request_target without a repository guard — This is the only pull_request_target workflow and has no if: github.repository == 'hackforla/website' guard. Practical risk is low (HACKFORLA_ADMIN_TOKEN is not available in fork secret stores), but the pattern is inconsistent with other workflows.

Action Items

Resources/Instructions

Activity

  1. added
    DraftIssue is still in the process of being created
    on Jun 10, 2025
  2. HackforLABot commented on Jun 10, 2025

    @HackforLABot
    Contributor

    Hi @t-will-gillis, thank you for taking up this issue! Hfla appreciates you :)

    Do let fellow developers know about your:-
    i. Availability: (When are you available to work on the issue/answer questions other programmers might have about your issue?)
    ii. ETA: (When do you expect this issue to be completed?)

    You're awesome!

    P.S. - You may not take up another issue until this issue gets merged (or closed). Thanks again :)

  3. t-will-gillis commented on Sep 20, 2025

    @t-will-gillis
    MemberAuthor

    Tokens, Secrets, Scopes, & Permissions

    Explanation of terms

    • Tokens
      • Tokens are credentials used to authenticate the actor (here 'actor' refers to the user, workflow, or bot) when that actor interacts with GitHub's REST API or GraphQL.
      • In addition to authenticating the actor, tokens are granted specific, defined 'scopes' - more about this in "Scopes" following.
      • Examples include the GITHUB_TOKEN and Personal Access Tokens (PATs). PATs themselves are either 'classic' and have broad scopes, or are 'fine-grained' and have targeted scopes. HfLA currently uses the classic PATs, though GitHub recommends that we transition to using fine-grained PATs for better security through "Least Privilege".
      • ⚠Important note: the GITHUB_TOKEN always exists by default in a workflow unless it is preempted by permissions: (see "Permissions" following) or by a PAT.
    • Secrets
      • Secrets are encrypted values stored in GitHub at the repo level. (They can also be stored at the org or environment levels)
      • Secrets securely hold the value of the Tokens (or other credentials) so we don't expose sensitive info in the workflow by hardcoding the token itself.
    • Scopes
      • Scopes represent the capabilities granted to the Token that limit what the token is permitted to do.
      • 'Classic' tokens use broad scopes while 'fine-grained' tokens use narrow, targeted scopes.
      • For example, a token might have a 'classic' scope of read:org, which means that a workflow using this token (via a secret) is given a specific permission. In the case of the read:org scope, the workflow is permitted to "Read org and team membership, read org projects".
    • Permissions
      • Permissions are temporary restrictions applied specifically to the GITHUB_TOKEN.
      • They can be set at the overall level or at the job level by adding permissions: to the workflow's YAML.

    To summarize then:

    • A "Token" is the credential (e.g., GITHUB_TOKEN or a PAT).
    • A "Secret" is how you store and inject a token into your workflow securely.
    • "Scopes" define the potential reach of the token (what it could ever do).
    • "Permissions" are the actual allowed actions that the GITHUB_TOKEN is permitted at different stages of the workflow.

    Additional notes

    • Each step of a workflow has at a minimum the permissions given by the default GITHUB_TOKEN. These permissions apply by default unless overridden or disabled in the workflow itself.
    • These default permissions are defined in the "Workflow permissions" section on the /settings/actions page in the master account, and include either:
      • a.) read and write permissions in the repo for all scopes supported by the GITHUB_TOKEN, or
      • b.) read permissions in the repo for contents and packages.
      • ⚠GitHub recommends using read permissions as the default, and then granting explicit permissions as needed in the workflows.
    • Certain workflow activities (such as pulling teams information, querying project info, and doing GraphQL mutations) require additional permissions/ scopes that cannot be granted to the default GITHUB_TOKEN using the permissions: entry in the workflow's YML. In these cases, each workflow step doing one of these activities must explicitly declare a PAT that is scoped with 'extended' permissions.
    • The fine-grained PATs are preferable over the broadly-defined 'classic' PATs; however, there may be some API calls that are not covered by the fine-grained tokens.
    • The default "Workflow permissions" defined for the GITHUB_TOKEN apply if no other permissions are declared. Otherwise, permissions: defined at the workflow level override the default repo/org defaults, permissions: defined at the workflow level preempt those at the workflow level (or the default repo/org), and PATs referenced at the step level preempt all others.
    • These permissions are not additive. Whatever is declared in the governing permissions: (i.e. default, workflow level, job level, step level) are the exact permissions that apply. (Example: if workflow says permissions: contents: read, then during that workflow run the GITHUB_TOKEN will only have contents:read permission, even if the default was broader.
    • ⚠It is not required to specific add github-token: ${{ secrets.github.token }} to a step- but this can be done for clarity and/or to avoid ambiguity if a PAT is not used.
    • It is not required to add workflow- or job-level permissions: if these permissions only repeat the default "Workflow permissions"- but again explicitly stating these permissions can be done for clarity and/or to avoid ambiguity. (And arguably should be done for self-documentation)
    • In the near future we will change the repo-wide GITHUB_TOKEN permissions to read only.
      • ⚠ Until then, we will not change the default "Workflow permissions" setting. Instead for every workflow we only need to state at the workflow-level: permissions: contents: read. This statement disables the write permission by omitting it.
  4. 5 remaining items

  5. t-will-gillis commented on Mar 28, 2026

    @t-will-gillis
    MemberAuthor

    Workflow permissions: audit

    Already has a permissions: block

    activity-trigger.yml — no change needed

    permissions:
      contents: read
      issues: write
      pull-requests: write

    codeql-scan-job.yml — no change needed (set at job level)

    permissions:
      actions: read
      contents: read
      security-events: write

    Needs a permissions: block added

    add-update-label-weekly.yml

    permissions:
      contents: read

    All meaningful operations use a GitHub App token. Default token is only exposed at checkout.


    check-closed-issue-for-linked-pr.yml

    permissions:
      contents: read
      issues: read
      pull-requests: read

    Default token runs check-issue-labels-and-linked-prs.js, which calls the GraphQL API via closedByPullRequestsReferences on an Issue — reads issue and linked PR data only. Reopen, label, and comment operations use HACKFORLA_GRAPHQL_TOKEN.


    codeql-create-issues.yml

    permissions:
      contents: read
      security-events: read
      issues: read

    GITHUB_TOKEN is used for GET /repos/{owner}/{repo}/code-scanning/alerts (security-events: read) and GET /search/issues (issues: read). Issue creation uses HACKFORLA_ADMIN_TOKEN.


    codeql.yml

    permissions:
      actions: read
      contents: read
      security-events: write

    Delegates entirely to codeql-scan-job.yml via uses:. The caller's token is what the reusable workflow receives — it must grant at least what the reusable workflow needs.


    flag-issues-unlabeled-after-deletion.yml

    permissions:
      contents: read

    All operations use HACKFORLA_GRAPHQL_TOKEN and HACKFORLA_BOT_PA_TOKEN. Default token is only exposed at checkout.


    issue-trigger.yml

    permissions:
      contents: read
      issues: write

    Default token is used by:

    • check-labels.js — issues.setLabels
    • post-labels-comment.js — issues.createComment
    • add-feature-branch-comment.js — issues.createComment
    • hide-feature-branch-comment.js — GraphQL minimizeComment mutation

    All require issues: write. check-label-preliminary-update.js makes no API calls (reads only from the event context payload). Team membership check and preliminary comment use HACKFORLA_GRAPHQL_TOKEN.


    lint-scss.yml

    permissions:
      contents: read
      statuses: write

    GITHUB_TOKEN is passed to super-linter v4, which posts commit status checks. If PR-level annotations fail in future, checks: write may also be required.


    move-closed-issues.yaml

    permissions:
      contents: read

    Default token runs sort-closed-issues.js, which reads only from the event context payload — no API calls. Project card operations use HACKFORLA_GRAPHQL_TOKEN.


    pr-instructions.yml

    permissions:
      contents: read
      pull-requests: read

    Default token runs create-instruction.js, which calls pulls.listFiles to determine which files were modified. No writes — the instruction is saved to an artifact.


    pr-verification.yml

    permissions:
      contents: read

    All verification logic uses HACKFORLA_ADMIN_TOKEN. Default token is only exposed at checkout.


    pull-request-trigger.yml

    permissions:
      contents: read
      issues: write

    Default token runs check-linked-issue.js, which calls issues.get to verify the linked issue exists and conditionally calls issues.createComment (via post-issue-comment.js) to notify the PR author. Both use the Issues API; issues: write covers both.


    schedule-daily-1100.yml

    permissions:
      contents: read

    GITHUB_TOKEN is passed as env.token to get-project-data.js, which reads public data only: repo search, repo languages, commit contributors, issue comment contributors. Push operations use HACKFORLA_BOT_PA_TOKEN via checkout.


    schedule-monthly.yml

    permissions:
      contents: read

    All operations (checkout, contributor queries, team membership changes, auto-commit) use HACKFORLA_ADMIN_TOKEN. Default token has no meaningful exposure.


    set-pr-labels.yaml

    permissions:
      contents: read
      issues: read

    Default token runs listIssuesFromPRBody (no API calls — parses context.payload.pull_request.body) and listLabelsFromIssues (issues.listLabelsOnIssue). Labels are written to an artifact, not to the repo.


    update-label-directory.yml

    permissions:
      contents: read

    All operations (label directory update, Google Sheets POST, auto-commit) use HACKFORLA_BOT_PA_TOKEN. Default token has no meaningful exposure.


    vrms-data.yml

    permissions:
      contents: read

    Data is fetched from vrms.io via curl — no GitHub API calls use the default token. Push uses HACKFORLA_BOT_PA_TOKEN via checkout.


    wr-pr-instructions.yml

    permissions:
      contents: read
      actions: read
      issues: write

    Default token calls actions.listWorkflowRunArtifacts and actions.downloadArtifact to retrieve the artifact from the triggering workflow run (actions: read), then calls issues.createComment (via post-issue-comment.js) to post the PR instructions (issues: write).


    wr-pull-request-trigger.yml

    permissions:
      contents: read

    Only echoes a string. No GitHub API calls.


    wr-schedule-monthly.yml

    permissions:
      contents: read
      issues: read

    Default token calls issues.listForRepo to retrieve the latest issue number (issues: read). Issue creation and closing both use HACKFORLA_BOT_PA_TOKEN.


    wr-set-pr-labels.yaml

    permissions:
      contents: read
      actions: read
      issues: write

    Default token calls actions.listWorkflowRunArtifacts and actions.downloadArtifact (actions: read), then calls issues.setLabels on the PR number (issues: write — GitHub's Labels API uses the Issues endpoint for both issues and PRs).


    Secondary flags (PAT scopes)

    admin:org_hook on HACKFORLA_BOT_PA_TOKEN and HACKFORLA_ADMIN_TOKEN — No workflow creates, updates, or deletes webhooks. This scope appears unused on both tokens and can likely be removed after testing.

    repo vs public_repo on HACKFORLA_GRAPHQL_TOKEN — repo grants full access including private repos. If all targeted repos are public, this could be narrowed to public_repo. Requires testing to confirm no operation depends on the broader scope.

    pr-verification.yml — pull_request_target without a repository guard — This is the only pull_request_target workflow and has no if: github.repository == 'hackforla/website' guard. Practical risk is low (HACKFORLA_ADMIN_TOKEN is not available in fork secret stores), but the pattern is inconsistent with other workflows.

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

Metadata

Metadata

Assignees

Type

No type

Projects

Relationships

None yet

Development

No branches or pull requests

Issue actions