Add Minimum Token Permissions For GitHub Workflows A Comprehensive Guide
Ensuring the security of GitHub workflows is crucial for maintaining the integrity of your projects. One significant step in enhancing this security is setting minimum token permissions for all workflow files. This article will guide you through the process of adding these permissions, adhering to OpenSSF Scorecard recommendations, and provide comprehensive scripts for verification.
Understanding the Need for Minimum Token Permissions
Minimum token permissions are essential for reducing the risk of unauthorized access and potential security breaches in your GitHub workflows. By limiting the permissions granted to workflow tokens, you minimize the damage that can occur if a token is compromised. The OpenSSF Scorecard highlights the importance of this practice, emphasizing the need to implement the least privilege principle.
Addressing the Token-Permissions Check
The token-permissions check from the OpenSSF Scorecard, available at https://github.com/ossf/scorecard/blob/ab2f6e92482462fe66246d9e32f642855a691dc1/docs/checks.md#token-permissions, serves as a critical guideline for enhancing workflow security. This check ensures that your workflow tokens have only the necessary permissions to perform their tasks, thereby mitigating the risk of privilege escalation and potential security vulnerabilities. By following the recommendations outlined in this article, you can effectively address the token-permissions check and bolster the overall security posture of your GitHub repositories. Implementing minimum token permissions is not just a best practice; it's a fundamental step towards securing your workflows and protecting your projects from potential threats.
Benefits of Minimum Token Permissions
Implementing minimum token permissions offers several key benefits, significantly enhancing the security and maintainability of your GitHub workflows. Here's a detailed look at these advantages:
- Reduced Attack Surface: By granting tokens only the permissions they need, you minimize the potential damage from compromised tokens. This limits the scope of what an attacker can do, reducing the attack surface.
- Improved Compliance: Adhering to security best practices, like minimum permissions, helps meet compliance requirements and industry standards, ensuring your projects are secure and well-governed.
- Enhanced Trust: Demonstrating a commitment to security through proper permissions management builds trust with users and stakeholders. This proactive approach assures them that you are serious about protecting their data and contributions.
- Easier Auditing: With clearly defined permissions, auditing workflows becomes simpler. You can easily track and verify which tokens have access to specific resources, facilitating quicker security reviews and compliance checks.
- Preventing Privilege Escalation: Setting minimum permissions prevents privilege escalation, where an attacker gains higher access levels than intended. This defense is crucial in safeguarding sensitive data and critical operations.
By adopting minimum token permissions, you create a more robust and secure environment for your GitHub workflows, protecting your projects from potential threats and vulnerabilities. This approach not only enhances security but also improves overall operational efficiency and trustworthiness.
Step 1: Adding Root-Level Permissions
Every workflow file must include a root-level permissions:
YAML block to define the default permissions for the workflow. This block sets the baseline permissions that apply to all jobs within the workflow unless overridden at the job level. Adding this block is crucial for enforcing the principle of least privilege, ensuring that workflows only have the necessary permissions to execute their tasks.
Rules for Root-Level Permission YAML Block
Adhering to specific rules when adding the root-level permissions:
YAML block ensures consistency and security across your workflows. These guidelines cover formatting, placement, and the types of permissions allowed, helping you maintain a secure and organized workflow configuration.
-
๐ READ THE FILE FIRST: Before making any changes, carefully examine the existing formatting style of the workflow file. This ensures that your additions maintain the consistency and readability of the file.
-
Root-Level permissions MUST be limited to either
read-all
orcontents: read
. These permissions provide the necessary access for most workflows while minimizing the risk of unauthorized actions. -
If the existing root-level permission is
read-all
, leave it unchanged. This permission level is already aligned with the recommended least privilege approach for many workflows. -
If existing root-level permissions have more than read permissions, move the root-level permission down to the job level where it's needed. This ensures that jobs only have the permissions they require, further limiting potential security risks. Follow Step 2 for rules about adding job-level permissions.
-
Standard format: New root-level permissions should be:
permissions: contents: read
This format is the recommended standard for specifying read-only access to the repository contents.
-
Placement: Insert the
permissions:
block immediately after the root-levelon:
YAML block, ensuring no other root-level YAML blocks are in between. This placement helps maintain a clear and organized structure in your workflow file. -
Don't reorder existing root-level YAML blocks: Only add the permissions block without altering the order of other blocks. This prevents unintentional changes to the workflow's logic or configuration.
-
Preserve formatting: Match the existing blank line style to maintain consistency within the file. Follow the detailed rules below to ensure proper spacing and readability.
-
Do not add any comments to the root-level permissions YAML block. Keeping the block free of comments helps maintain a clean and easily understandable configuration.
By following these rules, you can effectively add root-level permissions to your GitHub workflow files, ensuring a secure and well-organized workflow configuration. These permissions serve as a foundation for the overall security of your automation processes, helping you adhere to best practices and minimize potential risks.
๐ CRITICAL: Blank Line Formatting Rules
Maintaining consistent formatting, especially blank lines, is crucial for the readability and maintainability of your workflow files. The presence or absence of blank lines can significantly impact how easily the file can be understood and modified. To ensure proper formatting, follow these rules based on the existing style in the file.
STEP 1: Look at the original file. Find the on:
root-level YAML block and see what comes immediately after it. The on:
block defines the triggers for the workflow, making it a key reference point for determining formatting.
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
This rule ensures that the new permissions block is seamlessly integrated into the existing structure, maintaining a compact and tidy layout. The absence of blank lines indicates that the file follows a tightly formatted style, which should be preserved for consistency.
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
This rule maintains the visual separation between blocks, enhancing readability. The presence of blank lines suggests that the file uses a more spaced-out formatting style, which should be followed to ensure the file remains easy to parse visually.
Adhering to these blank line formatting rules helps maintain a consistent and readable style across your workflow files. This consistency is not just about aesthetics; it's about making your workflows easier to understand and modify, which is crucial for long-term maintainability and security. Correct formatting reduces the chances of errors and misunderstandings, contributing to the overall security and reliability of your automated processes.
๐งช MANDATORY: Run This Verification Script
Before making any changes, it is essential to understand the existing blank line pattern in your workflow file. This ensures that you maintain the current formatting style when adding the permissions block. The following Python script helps analyze the blank line pattern after the on:
block, allowing you to apply the correct formatting rule (A or B).
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
- Save the script as
check_formatting.py
in your local directory. - Run the script by executing
python check_formatting.py
in your terminal. After the script starts, call the functionanalyze_yaml_formatting('/path/to/workflow.yml')
, replacing/path/to/workflow.yml
with the actual path to your workflow file. - Carefully follow the output provided by the script to determine which formatting rule (A or B) applies to your workflow file.
- Make the necessary changes to your workflow file according to the identified formatting rule, ensuring consistency and readability.
By using this verification script, you can accurately determine the blank line pattern and apply the appropriate formatting rule, maintaining the integrity and consistency of your workflow files. This attention to detail is crucial for the long-term maintainability and security of your automated processes. Proper formatting reduces the risk of errors and ensures that your workflows are easily understandable and modifiable.
โ FINAL VERIFICATION: Check Root-Level Permissions
After adding or modifying the root-level permissions block, it's crucial to verify that it is correctly formatted. This ensures that your workflows adhere to the required security standards and that the permissions are correctly applied. Use the following Python script to verify the root-level permissions block.
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
- Add the
verify_root_permissions
function to your existingcheck_formatting.py
file, which you created in the previous step. - After making changes to the root-level permissions, run the verification by calling
verify_root_permissions('/path/to/workflow.yml')
in your Python script. Replace/path/to/workflow.yml
with the path to your workflow file. - Ensure that the script returns
โ VALID
before proceeding to the next file. This confirms that the root-level permissions are correctly formatted and comply with the required standards.
This verification step is essential for maintaining the security and integrity of your GitHub workflows. By ensuring that the root-level permissions block is correctly formatted, you establish a strong foundation for permissions management across your workflows. This proactive approach helps minimize potential security risks and ensures that your automated processes adhere to best practices.
Step 2: For Regular Workflow Jobs, Apply Appropriate Job-Level Permissions
Regular Workflow Jobs are defined as workflow jobs that DO NOT have a uses:
node directly under the job node and DO have a steps:
node. These jobs typically perform a series of actions within the workflow, such as building, testing, or deploying code.
Identifying Regular Workflow Jobs
To accurately apply job-level permissions, it's crucial to correctly identify regular workflow jobs. These jobs are characterized by the presence of a steps:
node, which outlines the sequence of actions to be performed. Additionally, they do not have a uses:
node directly under the job node, distinguishing them from jobs that call reusable workflows. By understanding these characteristics, you can ensure that the appropriate permissions are applied to the correct jobs, enhancing the overall security of your workflows.
When to Add Job Permissions for Regular Workflow Jobs
Adding job permissions for regular workflow jobs is essential when the default permissions (set at the root level) are insufficient for the tasks performed by the job. Specifically, you need to consider adding job permissions in the following scenarios:
- Steps explicitly using
secrets.GITHUB_TOKEN
orgithub.token
: When a step within the job directly uses the GitHub token to access repository resources, you must ensure that the job has the necessary permissions. This includes actions such as pushing changes, creating releases, or commenting on pull requests.- If the step calls a script, analyze that script to determine the permissions needed. Scripts often perform actions that require specific permissions, so a thorough analysis is necessary to ensure that the job has the appropriate access.
- Steps that use
actions/github-script
implicitly usegithub.token
: Theactions/github-script
action allows you to run custom JavaScript code within your workflow. Since this action implicitly uses the GitHub token, you must analyze the script to determine the necessary permissions.- Analyze the script it is executing to determine the permissions needed. This involves understanding the actions performed by the script and ensuring that the job has the required permissions to execute those actions.
- Steps that call a script: Analyze the script to see what permissions are needed. Scripts can perform a variety of actions that require specific permissions, so it's essential to understand the script's functionality and ensure that the job has the necessary access.
By carefully evaluating these scenarios, you can determine when to add job permissions for regular workflow jobs, ensuring that your workflows have the necessary access while adhering to the principle of least privilege. This proactive approach enhances the security of your workflows and minimizes the risk of unauthorized actions.
Job Permission Rules for Regular Workflow Jobs
When adding job permissions for regular workflow jobs, it's crucial to follow specific rules to ensure consistency and maintain security. These rules cover various aspects, including when to add permissions, where to place the permissions block, and how to document the permissions being granted.
-
If only read permissions are needed, do not insert a new permissions block. The root-level permissions often provide sufficient read access, so adding a job-level block is unnecessary if no additional permissions are required.
-
Placement: Insert the
permissions:
YAML block at the very top of the job YAML block, or directly under aneeds:
block if one exists. This ensures that the permissions are clearly defined and easily identifiable at the beginning of the job configuration.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
In this example, the permissions block is placed at the top of the
my-job
configuration, specifying write access to the repository contents.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
Here, the permissions block is placed directly after the
needs:
block, ensuring that the job has the necessary permissions to push changes and comment on pull requests. -
Don't reorder existing YAML blocks: Only add the permissions block without altering the order of other blocks. This prevents unintentional changes to the job's logic or configuration.
-
Only add write permissions: Job-level permissions should primarily focus on granting write access when necessary. Read permissions are typically covered by the root-level configuration.
-
Preserve existing read permissions if they are already present. If a job already has specific read permissions defined, ensure that these are retained when adding write permissions.
-
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#
. This documentation helps maintain clarity and understandability of the permissions being granted. -
DO NOT add any comment for write permissions which were already present. If a permission was previously defined, there's no need to add a redundant comment.
-
DO NOT add any additional comment for write permissions moved down from the root-level permissions block. These permissions are already documented at the root level, so no additional comments are needed at the job level.
By adhering to these job permission rules, you can ensure that your regular workflow jobs have the necessary access while maintaining a clear, consistent, and secure configuration. This approach not only enhances the security of your workflows but also improves their maintainability and understandability.
Common Permission Patterns
Understanding common permission patterns can streamline the process of configuring job-level permissions for your GitHub workflows. Recognizing these patterns allows you to quickly identify the necessary permissions based on the actions performed by the job. Here are some typical patterns:
JamesIves/github-pages-deploy-action
โ needscontents: write
: This action is commonly used to deploy a GitHub Pages site, requiring write access to the repository's contents to update the site.- Writing to repository โ needs
contents: write
: Any job that modifies the repository's contents, such as pushing code changes or creating new files, requires write access to the contents. - Creating releases โ needs
contents: write
: Jobs that automate the creation of releases need write access to the repository's contents to create and publish the release artifacts. - Posting comments โ needs
issues: write
orpull-requests: write
: If a job needs to post comments on issues or pull requests, it requires write access to either the issues or pull requests scope, depending on where the comments are being posted.
By recognizing these common permission patterns, you can efficiently configure job-level permissions for your GitHub workflows, ensuring that they have the necessary access to perform their tasks securely. This approach not only saves time but also helps maintain a clear and consistent permissions strategy across your workflows.
โ ๏ธ Important Exceptions
While configuring job-level permissions, it's important to be aware of certain exceptions where special permissions are not required. Understanding these exceptions helps prevent the unnecessary granting of permissions, adhering to the principle of least privilege. Here are some key exceptions to keep in mind:
- Steps which use other tokens such as
OPENTELEMETRYBOT_GITHUB_TOKEN
: Custom tokens don't need workflow permissions. When a step uses a token other than the defaultGITHUB_TOKEN
, such as a custom bot token, the workflow permissions do not apply. The permissions are governed by the custom token's configuration. - Steps which use
actions/cache/save
: Doesn't require special permissions. Theactions/cache/save
action, used for caching dependencies and build outputs, does not require any special permissions. This action operates within the workflow context and does not need additional access to the repository. - Don't add unnecessary permissions: Only add what's actually needed. It's crucial to avoid granting permissions that are not explicitly required by the job. Unnecessary permissions increase the risk of unauthorized actions and potential security breaches.
By being mindful of these exceptions, you can ensure that your job-level permissions are configured accurately and securely. This approach helps maintain a minimal permission set, reducing the attack surface and enhancing the overall security of your GitHub workflows. Only granting necessary permissions is a key principle in effective security management.
Step 3: For Workflow Jobs That Call a Local Reusable Workflow, Apply Appropriate Job-Level Permissions
After applying Step 2 to all workflows, the next critical step is to check each workflow job that calls a local reusable workflow. These jobs are distinct because they utilize a uses:
node directly under the job node and do not have a steps:
node. Local reusable workflows are modular components that encapsulate a set of actions, making it easier to reuse and maintain workflow logic. However, ensuring these jobs have the correct permissions is vital for security.
Identifying Jobs Calling Local Reusable Workflows
To correctly apply job-level permissions, you must first identify the workflow jobs that call local reusable workflows. These jobs are characterized by the presence of a uses:
node directly under the job node, indicating that they are invoking a reusable workflow. Unlike regular workflow jobs, they do not have a steps:
node, as the steps are defined within the reusable workflow itself. Recognizing these jobs is the first step in ensuring that they have the necessary permissions to execute securely.
Gathering Required Permissions from Reusable Workflows
When a job calls a local reusable workflow, it's crucial to gather all the permissions required by that workflow. This involves reading the local reusable workflow file and identifying all the permissions defined within it. You need to consider permissions from two primary sources:
- Its root-level permission block: The root-level permission block sets the default permissions for the entire reusable workflow. This block typically includes permissions that apply to all jobs within the workflow, such as
contents: read
. - All job-specific permission blocks: Reusable workflows may also have job-specific permission blocks, which define permissions that are required for particular jobs within the workflow. These permissions override the root-level permissions for the specific job.
By gathering permissions from both the root-level block and job-specific blocks, you can create a comprehensive list of permissions required by the reusable workflow. This list will then be used to configure the calling job with the necessary permissions, ensuring it can execute the reusable workflow securely. This thorough approach to permission gathering is essential for maintaining the security and integrity of your GitHub workflows.
Applying Job-Level Permissions for Jobs Calling Reusable Workflows
Once you have gathered the permissions required by the local reusable workflow, the next step is to apply these permissions to the job that calls the workflow. This ensures that the calling job has the necessary access to execute the reusable workflow securely. Here are the rules to follow when applying job-level permissions:
- If the local reusable workflow only requires "contents: read" permissions, then do not add a job-specific permission block. The default read access provided at the root level is sufficient in this case.
- Otherwise, apply these rules when adding the job-specific permission block:
Notes That Only Apply to Jobs That Call a Local Reusable Workflow
- 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 aneeds:
block then add thepermissions:
block directly after that node. - If you add a new permissions block, "contents: read" should be the first permission listed under it.
- Don't reorder existing YAML blocks: Only add the permissions block without altering the order of other blocks. This prevents unintentional changes to the job's configuration.
- 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. This comment provides a clear indication of why the permissions block is present. - 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.
By following these rules, you can accurately apply job-level permissions for jobs that call local reusable workflows, ensuring they have the necessary access while maintaining a clear and consistent permissions strategy. This approach helps enhance the security and maintainability of your GitHub workflows, adhering to the principle of least privilege.
๐ Comprehensive Workflow Permissions Verification Script
To ensure that all your workflows have the proper root-level permissions defined, it's essential to use a comprehensive verification script. This script should accurately parse YAML files and provide detailed analysis of each workflow file. The following Python script uses the PyYAML library for robust YAML parsing and offers a thorough verification process.
Installation Requirements
Before using the verification script, you need to install the PyYAML library. This library is essential for parsing YAML files and extracting the necessary information for verification. Use the following command to install PyYAML:
pip install PyYAML
Complete Verification Script
Save this script as verify_all_workflow_permissions.py
in your local directory. This script provides a comprehensive solution for verifying workflow permissions, ensuring that your GitHub workflows adhere to best practices.
#!/usr/bin/env python3
"""
Comprehensive GitHub Workflow Permissions Verification Script
This script verifies that all GitHub workflow files have proper root-level permissions
defined according to OpenSSF Scorecard recommendations.
Requirements:
- PyYAML library: pip install PyYAML
Usage:
python verify_all_workflow_permissions.py [directory]
If no directory is provided, it will scan the current directory recursively.
"""
import os
import sys
import yaml
import glob
from pathlib import Path
from typing import Dict, List, Tuple, Optional, Any
class WorkflowPermissionsVerifier:
"""Verifies GitHub workflow permissions compliance."""
def __init__(self):
self.valid_root_permissions = {
'read-all',
'write-all', # Not recommended but valid
'contents: read'
}
# Statistics
self.total_files = 0
self.files_with_errors = 0
self.files_with_warnings = 0
self.files_passed = 0
def find_workflow_files(self, directory: str = '.') -> List[str]:
"""Find all GitHub workflow files in the given directory."""
workflow_patterns = [
'**/.github/workflows/*.yml',
'**/.github/workflows/*.yaml'
]
workflow_files = []
for pattern in workflow_patterns:
workflow_files.extend(glob.glob(os.path.join(directory, pattern), recursive=True))
return sorted(workflow_files)
def load_yaml_file(self, file_path: str) -> Tuple[Optional[Dict], Optional[str]]:
"""Load and parse a YAML file safely."""
try:
with open(file_path, 'r', encoding='utf-8') as f:
content = yaml.safe_load(f)
return content, None
except yaml.YAMLError as e:
return None, f"YAML parsing error: {e}"
except Exception as e:
return None, f"File reading error: {e}"
def check_root_permissions(self, workflow_data: Dict, file_path: str) -> Tuple[bool, List[str], List[str]]:
"""
Check if the workflow has proper root-level permissions.
Returns:
(is_valid, errors, warnings)
"""
errors = []
warnings = []
# Check if permissions key exists at root level
if 'permissions' not in workflow_data:
errors.append("Missing root-level 'permissions' block")
return False, errors, warnings
permissions = workflow_data['permissions']
# Handle different permission formats
if permissions is None:
errors.append("Root-level 'permissions' block is empty")
return False, errors, warnings
if isinstance(permissions, str):
# Single string permission like 'read-all'
if permissions == 'read-all':
return True, errors, warnings
elif permissions == 'write-all':
warnings.append("Root-level 'write-all' permission is overly permissive - consider using 'read-all' or 'contents: read'")
return True, errors, warnings
else:
errors.append(f"Invalid root-level permissions string: '{permissions}'")
return False, errors, warnings
elif isinstance(permissions, dict):
# Dictionary permissions like {'contents': 'read'}
if len(permissions) == 1 and permissions.get('contents') == 'read':
return True, errors, warnings
elif len(permissions) == 0:
errors.append("Root-level permissions block is empty")
return False, errors, warnings
else:
# Check if it has more than just contents: read
perm_items = list(permissions.items())
if len(perm_items) > 1 or (len(perm_items) == 1 and perm_items[0] != ('contents', 'read')):
warnings.append(f"Root-level permissions should be limited to 'contents: read' or 'read-all'. Found: {permissions}")
warnings.append("Consider moving specific permissions to job level if needed")
return True, errors, warnings
else:
return True, errors, warnings
else:
errors.append(f"Invalid root-level permissions format: {type(permissions)}")
return False, errors, warnings
def analyze_job_permissions(self, workflow_data: Dict) -> Dict[str, Any]:
"""Analyze job-level permissions for informational purposes."""
job_analysis = {}
if 'jobs' not in workflow_data:
return job_analysis
jobs = workflow_data['jobs']
if not isinstance(jobs, dict):
return job_analysis
for job_name, job_data in jobs.items():
if not isinstance(job_data, dict):
continue
analysis = {
'has_permissions': False,
'permissions': None,
'is_reusable_workflow': False,
'has_steps': False
}
# Check if job has permissions
if 'permissions' in job_data:
analysis['has_permissions'] = True
analysis['permissions'] = job_data['permissions']
# Check if it's a reusable workflow call
if 'uses' in job_data and 'steps' not in job_data:
analysis['is_reusable_workflow'] = True
# Check if it has steps
if 'steps' in job_data:
analysis['has_steps'] = True
job_analysis[job_name] = analysis
return job_analysis
def verify_workflow_file(self, file_path: str) -> Dict[str, Any]:
"""Verify a single workflow file."""
result = {
'file_path': file_path,
'is_valid': False,
'errors': [],
'warnings': [],
'job_analysis': {}
}
# Load the YAML file
workflow_data, load_error = self.load_yaml_file(file_path)
if load_error:
result['errors'].append(load_error)
return result
if not isinstance(workflow_data, dict):
result['errors'].append("Workflow file is not a valid YAML dictionary")
return result
# Check root-level permissions
is_valid, errors, warnings = self.check_root_permissions(workflow_data, file_path)
result['is_valid'] = is_valid
result['errors'].extend(errors)
result['warnings'].extend(warnings)
# Analyze job-level permissions
result['job_analysis'] = self.analyze_job_permissions(workflow_data)
return result
def print_file_result(self, result: Dict[str, Any]) -> None:
"""Print the verification result for a single file."""
file_path = result['file_path']
relative_path = os.path.relpath(file_path)
if result['is_valid']:
if result['warnings']:
print(f"โ ๏ธ {relative_path}")
self.files_with_warnings += 1
else:
print(f"โ
{relative_path}")
self.files_passed += 1
else:
print(f"โ {relative_path}")
self.files_with_errors += 1
# Print errors
for error in result['errors']:
print(f" ERROR: {error}")
# Print warnings
for warning in result['warnings']:
print(f" WARNING: {warning}")
# Print job analysis if there are jobs with permissions
job_analysis = result['job_analysis']
jobs_with_permissions = [name for name, analysis in job_analysis.items() if analysis['has_permissions']]
if jobs_with_permissions:
print(f" ๐ Jobs with permissions: {', '.join(jobs_with_permissions)}")
print() # Empty line for readability
def verify_all_workflows(self, directory: str = '.') -> Dict[str, Any]:
"""Verify all workflow files in the given directory."""
workflow_files = self.find_workflow_files(directory)
if not workflow_files:
print(f"No GitHub workflow files found in {directory}")
return {'summary': 'No workflows found'}
print(f"Found {len(workflow_files)} workflow file(s) to verify:\n")
results = []
for file_path in workflow_files:
self.total_files += 1
result = self.verify_workflow_file(file_path)
results.append(result)
self.print_file_result(result)
# Print summary
print("=" * 60)
print("VERIFICATION SUMMARY")
print("=" * 60)
print(f"Total files checked: {self.total_files}")
print(f"โ
Passed: {self.files_passed}")
print(f"โ ๏ธ Warnings: {self.files_with_warnings}")
print(f"โ Errors: {self.files_with_errors}")
if self.files_with_errors > 0:
print(f"\nโ {self.files_with_errors} file(s) have errors that need to be fixed")
sys.exit(1)
elif self.files_with_warnings > 0:
print(f"\nโ ๏ธ {self.files_with_warnings} file(s) have warnings - review recommended")
else:
print(f"\n๐ All workflow files have proper root-level permissions!")
return {
'total_files': self.total_files,
'files_passed': self.files_passed,
'files_with_warnings': self.files_with_warnings,
'files_with_errors': self.files_with_errors,
'results': results
}
def main():
"""Main function to run the verification."""
import argparse
parser = argparse.ArgumentParser(
description="Verify GitHub workflow permissions compliance",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
python verify_all_workflow_permissions.py
python verify_all_workflow_permissions.py /path/to/repo
python verify_all_workflow_permissions.py --help
"""
)
parser.add_argument(
'directory',
nargs='?',
default='.',
help='Directory to scan for workflow files (default: current directory)'
)
parser.add_argument(
'--quiet',
action='store_true',
help='Only show summary (suppress individual file results)'
)
args = parser.parse_args()
if not os.path.exists(args.directory):
print(f"Error: Directory '{args.directory}' does not exist")
sys.exit(1)
print("GitHub Workflow Permissions Verification")
print("=" * 60)
print(f"Scanning directory: {os.path.abspath(args.directory)}")
print()
verifier = WorkflowPermissionsVerifier()
# Temporarily suppress individual file output if quiet mode
if args.quiet:
original_print_file_result = verifier.print_file_result
verifier.print_file_result = lambda result: None
try:
summary = verifier.verify_all_workflows(args.directory)
if args.quiet:
# Restore original method and print summary
verifier.print_file_result = original_print_file_result
except KeyboardInterrupt:
print("\n\nVerification interrupted by user")
sys.exit(1)
except Exception as e:
print(f"Unexpected error: {e}")
sys.exit(1)
if __name__ == '__main__':
main()
Quick Usage Examples
To effectively use the verification script, you can run it with various command-line arguments to suit your needs. Here are some common usage examples:
-
Verify all workflows in the current directory:
Run the script without any arguments to verify all workflow files in the current directory:
python verify_all_workflow_permissions.py
-
Verify workflows in a specific directory:
Specify the directory path as an argument to verify workflows in a particular location:
python verify_all_workflow_permissions.py /path/to/your/repo
-
Quiet mode (summary only):
Use the
--quiet
flag to suppress individual file results and display only the summary:python verify_all_workflow_permissions.py --quiet
These examples provide a flexible way to use the verification script, allowing you to check workflow permissions in different scenarios. Whether you need to verify all workflows in a repository or just a specific directory, the script can be adapted to your needs.
What the Script Checks
The comprehensive workflow permissions verification script is designed to perform several key checks to ensure that your GitHub workflows adhere to best practices and security standards. Here's a detailed breakdown of what the script checks:
-
Root-level permissions presence: The script ensures that every workflow file has a
permissions:
block at the root level. This is a fundamental requirement for setting default permissions and adhering to the principle of least privilege. The absence of a root-level permissions block is flagged as an error. -
Valid permission formats: The script verifies that the permissions defined in the root-level block are in a valid format. It accepts the following formats:
permissions: read-all
(recommended)permissions: { contents: read }
(recommended)permissions: write-all
(valid but warns about over-permissive)
Any other format is considered invalid and will be flagged as an error.
-
Invalid configurations: The script detects various invalid configurations, including:
- Missing permissions blocks
- Empty permissions blocks
- Malformed permissions blocks
These issues can lead to incorrect permissions settings and potential security vulnerabilities. The script identifies these configurations to ensure that they are addressed.
-
Job-level analysis: In addition to verifying root-level permissions, the script provides informational output about job-level permissions. It lists jobs that have their own permissions blocks, allowing you to quickly identify which jobs have specific permissions requirements. This analysis helps in understanding the permissions landscape of your workflows.
By performing these checks, the script ensures that your GitHub workflows have the necessary permissions configurations in place, adhering to best practices and enhancing security. The detailed output from the script helps you identify and address any issues, ensuring that your workflows are secure and well-managed.
Script Output
The workflow permissions verification script provides detailed output to help you understand the status of your workflow files. The output is designed to be clear and informative, making it easy to identify any issues that need to be addressed. Here's a breakdown of the script's output:
- โ
Pass: This indicates that the workflow file has the proper root-level permissions defined and meets the required standards. The script confirms that the file has a
permissions:
block at the root level and that the permissions are in a valid format. - โ ๏ธ Warning: This indicates that the workflow file has valid permissions but may be over-permissive. For example, if the root-level permissions are set to
write-all
, the script will issue a warning, suggesting that you consider usingread-all
orcontents: read
instead. Warnings highlight areas where you can further enhance the security of your workflows. - โ Error: This indicates that the workflow file is missing or has invalid root-level permissions. Errors can include missing
permissions:
blocks, malformed permissions configurations, or invalid permission formats. Errors must be addressed to ensure that your workflows are secure and compliant. - ๐ Info: This provides additional information about the workflow file, such as a list of jobs that have their own permission blocks. This information helps you understand the permissions landscape of your workflows and identify jobs that may require specific attention.
In addition to these status indicators, the script also prints detailed error and warning messages, providing specific guidance on how to resolve any issues. The clear and informative output makes it easy to identify and address permissions problems, ensuring that your GitHub workflows are secure and well-managed.
Integration with CI/CD
Integrating the workflow permissions verification script into your Continuous Integration/Continuous Deployment (CI/CD) pipeline is a proactive way to ensure that all your workflows adhere to security best practices. By including this script in your CI/CD process, you can automatically verify permissions whenever changes are made to your workflow files, preventing security vulnerabilities from being introduced. Here's how you can integrate the script into your GitHub Actions CI/CD pipeline:
# Example GitHub Actions step
- name: Verify Workflow Permissions
run: |
pip install PyYAML
python verify_all_workflow_permissions.py
This example demonstrates a simple GitHub Actions step that installs the PyYAML library and runs the verify_all_workflow_permissions.py
script. By adding this step to your workflow, you can automatically check permissions whenever you push changes to your repository. If the script detects any errors, it will cause the CI/CD pipeline to fail, preventing the changes from being deployed.
Integrating the verification script into your CI/CD pipeline helps you maintain a consistent permissions strategy across your workflows, ensuring that they remain secure and compliant over time. This proactive approach to security management is essential for preventing vulnerabilities and protecting your projects from potential threats.
๐ Implementation Guidelines
To ensure a smooth and effective implementation of minimum token permissions for your GitHub workflows, it's crucial to follow a set of clear guidelines. These guidelines cover various aspects of the implementation process, from initial assessment to final verification, helping you maintain a consistent and secure approach. Here are the key implementation guidelines to follow:
- Read each file completely before making changes: This ensures that you have a thorough understanding of the workflow's structure and existing permissions settings. Understanding the context is essential for making informed decisions about permissions configurations.
- Only modify what's necessary for security compliance: Avoid making unnecessary changes to the workflow files. Focus solely on the modifications required to meet security standards and adhere to the principle of least privilege. This minimizes the risk of introducing unintended issues.
- Maintain existing code style and formatting: Preserve the existing code style and formatting conventions within the workflow files. Consistency in style and formatting enhances readability and maintainability, making it easier for others to understand and modify the workflows in the future.
- Don't add comments to the workflow files other than trailing line comments explaining the write permissions: Keep the workflow files clean and concise by limiting comments to only those that are essential for explaining write permissions. Over-commenting can clutter the files and make them harder to read.
- No need to test locally (workflows don't run in local builds): Workflow files are designed to run in the GitHub Actions environment, so there's no need to test them locally. Focus on verifying the permissions configurations using the provided scripts and rely on the CI/CD pipeline for testing the workflow's functionality.
By adhering to these implementation guidelines, you can ensure that the process of adding minimum token permissions to your GitHub workflows is efficient, effective, and consistent. This proactive approach helps maintain the security and integrity of your workflows, protecting your projects from potential vulnerabilities.
Conclusion
Adding minimum token permissions to your GitHub workflow files is a critical step in improving your project's security posture. By following the guidelines and utilizing the scripts provided in this article, you can ensure that your workflows adhere to best practices and minimize the risk of security breaches. Regular verification and adherence to these guidelines will help maintain a secure and efficient CI/CD pipeline.