Skip to content

Add minimum token permissions for all github workflow files #5

Open
@opentelemetrybot

Description

@opentelemetrybot

Add minimum token permissions for all GitHub workflow files to improve security posture according to OpenSSF Scorecard recommendations.

Context

This addresses the Token-Permissions check from https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#token-permissions.

Step 1: Add Root-Level Permissions

Every workflow file MUST have a root-level permissions: YAML block.

Rules for Root-Level Permission YAML Block:

  1. 🔍 READ THE FILE FIRST: Always examine the existing formatting style before making any changes
  2. Root-Level permissions MUST be limited to either read-all or contents: read
  3. If the existing root-level permission is read-all then leave it unchanged
  4. If existing root-level permissions have more than read permissions, then move the root-level permission down to the job level where it's needed
    (follow Step 2 for rules about adding job level permissions)
  5. Standard format: New root-level permissions should be:
    permissions:
      contents: read
  6. Placement: Insert immediately after the root-level on: YAML block (no other root-level YAML blocks in between)
  7. Don't reorder existing root-level YAML blocks - only add the permissions block
  8. Preserve formatting: Match the existing blank line style (see detailed rules below)
  9. Do not add any comments to the root-level permissions YAML block

🔍 CRITICAL: Blank Line Formatting Rules

STEP 1: Look at the original file. Find the on: root-level YAML block and see what comes immediately after it.

STEP 2: Apply the matching rule:

Rule A - If there is NO blank line between the on: root-level YAML block and the next root-level YAML block:

Insert permissions immediately after the `on:` root-level YAML block with NO blank lines above or below

Rule B - If there IS a blank line between on: root-level YAML block and the next root-level YAML block:

Insert permissions with blank lines both above and below

🧪 MANDATORY: Run This Verification Script

BEFORE making any changes, copy and run this Python script to understand the blank line pattern:

import sys

def analyze_yaml_formatting(file_path):
    """
    Analyzes the blank line pattern after the 'on:' block in a YAML workflow file.
    Use this to determine which formatting rule (A or B) to apply.
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
    except Exception as e:
        print(f"Error reading file: {e}")
        return
    
    # Find the 'on:' root-level block
    on_block_end = -1
    in_on_block = False
    next_block_line = -1
    
    for i, line in enumerate(lines):
        stripped = line.strip()
        
        # Found root-level 'on:' block
        if stripped == 'on:' and not line.startswith(' '):
            in_on_block = True
            print(f"Found 'on:' block at line {i+1}")
            continue
            
        # We're in the on block, look for the end
        if in_on_block:
            # If this line has content and is indented, it's part of the on block
            if stripped and line.startswith(' '):
                on_block_end = i
            # If line starts with non-space and isn't empty, we've found the next root-level block
            elif stripped and not line.startswith(' '):
                next_block_line = i
                break
    
    if on_block_end == -1:
        print("Could not find complete 'on:' block structure")
        return
    
    # Check if there's a blank line between on block and next block
    has_blank_line = (on_block_end + 1 < len(lines) and 
                     lines[on_block_end + 1].strip() == '')
    
    print(f"On block ends at line {on_block_end + 1}")
    print(f"Next root-level block starts at line {next_block_line + 1}: '{lines[next_block_line].strip()}'")
    print(f"Blank line between them: {has_blank_line}")
    print()
    
    if has_blank_line:
        print("🟢 RULE B APPLIES: Insert permissions with blank lines above and below")
        print("Format should be:")
        print("on:")
        print("  # on block content")
        print("")
        print("permissions:")
        print("  contents: read")
        print("")
        print("next-block:")
    else:
        print("🟢 RULE A APPLIES: Insert permissions with NO blank lines")
        print("Format should be:")
        print("on:")
        print("  # on block content")
        print("permissions:")
        print("  contents: read")
        print("next-block:")

# Usage: analyze_yaml_formatting('/path/to/workflow.yml')

How to use this script:

  1. Save the script as check_formatting.py
  2. Run: python check_formatting.py (then call analyze_yaml_formatting('/path/to/workflow.yml'))
  3. Follow the output to determine which rule applies
  4. Make your changes accordingly

✅ FINAL VERIFICATION: Check Root-Level Permissions

AFTER making changes, run this verification script to ensure the root-level permissions block is correct:

def verify_root_permissions(file_path):
    """
    Verifies that the root-level permissions block is correctly formatted.
    Must be either 'permissions: read-all' or 'permissions:\\n  contents: read'
    """
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            lines = f.readlines()
    except Exception as e:
        print(f"Error reading file: {e}")
        return False
    
    # Find the root-level permissions block
    permissions_line = -1
    for i, line in enumerate(lines):
        stripped = line.strip()
        # Found root-level 'permissions:' block (not indented)
        if stripped.startswith('permissions:') and not line.startswith(' '):
            permissions_line = i
            break
    
    if permissions_line == -1:
        print("❌ ERROR: No root-level 'permissions:' block found")
        return False
    
    perm_line = lines[permissions_line].strip()
    
    # Check for valid formats
    if perm_line == 'permissions: read-all':
        print("✅ VALID: Root-level permissions is 'permissions: read-all'")
        return True
    elif perm_line == 'permissions:':
        # Check if next line is '  contents: read' and there are no additional permissions
        if permissions_line + 1 >= len(lines):
            print(f"❌ ERROR: Found 'permissions:' but no content following it")
            return False
        
        contents_line = lines[permissions_line + 1]
        if (contents_line.strip() == 'contents: read' and 
            contents_line.startswith('  ')):
            
            # Check that there are no additional permissions after contents: read
            next_line_idx = permissions_line + 2
            if (next_line_idx < len(lines) and 
                lines[next_line_idx].strip() != '' and 
                lines[next_line_idx].startswith('  ')):
                print(f"❌ ERROR: Found additional permissions after 'contents: read'")
                print(f"Additional line: '{lines[next_line_idx].strip()}'")
                print("Root-level permissions must contain ONLY 'contents: read'")
                return False
            
            print("✅ VALID: Root-level permissions is 'permissions:\\n  contents: read'")
            return True
        else:
            print(f"❌ ERROR: Found 'permissions:' but next line is not '  contents: read'")
            print(f"Next line: '{contents_line.strip()}'")
            return False
    else:
        print(f"❌ ERROR: Invalid root-level permissions format: '{perm_line}'")
        print("Must be either 'permissions: read-all' or 'permissions:' followed by '  contents: read'")
        return False

# Usage: verify_root_permissions('/path/to/workflow.yml')

How to use this verification:

  1. Add the function to your check_formatting.py file
  2. After making changes, run: verify_root_permissions('/path/to/workflow.yml')
  3. Ensure it returns ✅ VALID before moving to the next file

Step 2: For Regular Workflow Jobs, apply appropriate Job-Level Permissions

Regular Workflow Jobs are defined here as workflow jobs that DO NOT have a uses: node directly under the job node and DO have a steps: node.

Check these and add job-specific permissions for any that need more than read permission.

When to Add Job Permissions for Regular Workflow Jobs

  • Steps explicitly using secrets.GITHUB_TOKEN or github.token
    • If the step calls a script, analyze that script to see what permissions are needed
  • Steps that use actions/github-script implicitly use github.token
    • Analyze the script it is executing to see what permissions are needed
  • Steps that call a script: Analyze the script to see what permissions are needed

Job Permission Rules for Regular Workflow Jobs

  1. If only read permissions are needed then do not insert a new permissions block.

  2. Placement: Insert the permissions: YAML block at the very top of the job YAML block, or directly under a needs: block if one exists

    Example A - Job without needs: block:

    jobs:
      my-job:
        permissions:
          contents: write  # required for pushing changes
        name: My Job
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v3

    Example B - Job with needs: block:

    jobs:
      my-job:
        needs: [setup, build]
        permissions:
          contents: write  # required for pushing changes
          pull-requests: write  # required for commenting on PRs
        name: My Job
        runs-on: ubuntu-latest
        steps:
          - name: Checkout
            uses: actions/checkout@v3
  3. Don't reorder existing YAML blocks

  4. Only add write permissions

  5. Preserve existing read permissions if they are already present

  6. Add a trailing comment for each write permission that you add, explaining briefly why it's needed, e.g. # required for assigning reviewers to PRs. Trailing line comments should only have a single space before the #.

  7. DO NOT add any comment for write permissions which were already present

  8. DO NOT add any additional comment for write permissions moved down from the root-level permissions block

Common Permission Patterns:

  • JamesIves/github-pages-deploy-action → needs contents: write
  • Writing to repository → needs contents: write
  • Creating releases → needs contents: write
  • Posting comments → needs issues: write or pull-requests: write

⚠️ Important Exceptions:

  • Steps which use other tokens such as OPENTELEMETRYBOT_GITHUB_TOKEN: Custom tokens don't need workflow permissions
  • Steps which use actions/cache/save: Doesn't require special permissions
  • Don't add unnecessary permissions: Only add what's actually needed

Step 3: For Workflow Jobs that call a local reusable workflow, apply appropriate Job-Level Permissions

After applying Step 2 to all workflows, check each workflow job that calls a local reusable workflow.
These are workflow jobs that have a uses: node directly under the job node and have NO steps: node.

Read the local reusable workflow file and gather all of the permissions that it requires
(from its root-level permission block workflow AND all job-specific permission blocks).
This may include one or more read permissions in addition to write permissions.

This is the set of permissions required by the job that calls the local reusable workflow.

If the local reusable workflow only requires "contents: read" permissions, then do not add a job-specific permission block. Otherwise apply these rules when adding the job-specific permission block:

Notes that only apply to Jobs that call a local reusable workflow

  1. Placement: If a job-level permissions: block already exists, do not reorder it.
    If one does not already exist and you are going to add one, it should be the first line under the job name node. With one exception, if the job has a needs: block then add the permissions: block directly after that node.
  2. If you add a new permissions block, "contents: read" should be the first permission listed under it.
  3. Don't reorder existing YAML blocks
  4. Add a trailing comment only to the line with text permissions:: # required by the reusable workflow. Trailing line comments should only have a single space before the #. Don't add any comments to any other lines in this case.
  5. DO NOT add any comments to any of the read or write permissions in this case. Only add the above comment above as a trailing comment specifically on the permissions: line

📝 Implementation Guidelines:

  • Read each file completely before making changes
  • Only modify what's necessary for security compliance
  • Maintain existing code style and formatting
  • Don't add comments to the workflow files other than trailing line comments explaining the write permissions
  • No need to test locally (workflows don't run in local builds)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions