Timeout Minutes Rule Overview #
This rule enforces the timeout-minutes attribute for all jobs in GitHub Actions workflows. Without explicit timeouts, jobs can run indefinitely, consuming CI/CD resources and potentially being exploited for malicious purposes.
Security Impact #
Severity: Low (3/10)
Missing timeouts are primarily a best practice and resource management concern:
- Resource Exhaustion: Long-running jobs consume CI/CD compute quotas
- Cost Overruns: Billable minutes accumulate when jobs hang indefinitely
- C2 Potential: Compromised workflows could use runners for extended operations
- Pipeline Blocking: Stuck jobs delay releases and block other workflows
While not a direct security vulnerability, missing timeouts can enable or exacerbate security issues. This rule is classified as Low severity because it primarily addresses operational best practices rather than exploitable vulnerabilities.
Invalid Example:
name: CI
on: [push, pull_request]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
# Missing timeout-minutes
steps:
- uses: actions/checkout@v4
- run: npm run lint
docker:
name: Build Docker
runs-on: ubuntu-latest
# Missing timeout-minutes
steps:
- uses: actions/checkout@v4
- run: docker build .
Detection Output:
CI.yaml:5:3: timeout-minutes is not set for job lint; see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes for more details. [missing-timeout-minutes]
5 👈| lint:
CI.yaml:13:3: timeout-minutes is not set for job docker; see https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idtimeout-minutes for more details. [missing-timeout-minutes]
13 👈| docker:
Rule Background #
Why Timeout Configuration Matters #
Jobs without explicit timeouts pose several risks:
- Resource Exhaustion: Long-running jobs consume compute minutes and can exhaust CI/CD quotas
- Denial of Service: Malicious PRs could intentionally create infinite loops
- C2 Attack Vector: Compromised workflows could be used as command-and-control infrastructure
- Cost Overruns: Billable minutes accumulate when jobs hang indefinitely
- Developer Friction: Stuck jobs block CI/CD pipelines and delay releases
Default Behavior #
GitHub Actions has a default timeout of 360 minutes (6 hours) per job. This is often excessive for most workflows and should be explicitly reduced.
Security Implications #
Without timeouts, attackers can potentially:
- Mine Cryptocurrency: Use runner compute for resource-intensive operations
- Exfiltrate Data Slowly: Transfer data in small chunks over extended periods
- Establish Persistence: Maintain long-running processes for command-and-control
- Consume Resources: Create denial-of-service conditions through resource exhaustion
Technical Detection Mechanism #
The rule checks each job for the presence of timeout-minutes:
func (rule *TimeoutMinutesRule) VisitJobPre(node *ast.Job) error {
if node.TimeoutMinutes == nil {
rule.Errorf(node.Pos,
"timeout-minutes is not set for job %s; see %s for more details.",
node.ID.Value, timeoutDocsURL)
// Add auto-fixer
rule.AddAutoFixer(NewJobFixer(node, rule))
}
return nil
}
Detection Logic Explanation #
What the Rule Checks #
- Job-Level Timeouts: Validates that each job has
timeout-minutesdefined - Explicit Configuration: Ensures timeouts are explicitly set, not relying on defaults
- All Jobs: Applies to every job in the workflow
Why Not Step-Level Timeouts? #
While GitHub Actions supports step-level timeout-minutes, the rule focuses on job-level timeouts because:
- Job-level timeouts provide overall protection for the entire job
- Step-level timeouts are optional refinements
- A single stuck step should not run indefinitely
Valid Patterns #
Pattern 1: Simple Timeout #
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- uses: actions/checkout@v4
- run: npm run build
Pattern 2: Different Timeouts per Job #
jobs:
lint:
runs-on: ubuntu-latest
timeout-minutes: 5 # Quick job
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
timeout-minutes: 30 # Longer job
steps:
- uses: actions/checkout@v4
- run: npm test
deploy:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- run: ./deploy.sh
Pattern 3: Using Variables #
env:
DEFAULT_TIMEOUT: 10
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: ${{ env.DEFAULT_TIMEOUT }}
steps:
- uses: actions/checkout@v4
- run: npm run build
Auto-Fix Support #
The timeout-minutes rule supports auto-fixing by adding a default timeout:
# Preview changes without applying
sisakulint -fix dry-run
# Apply fixes
sisakulint -fix on
Before (Missing Timeout):
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
After Auto-Fix:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 5 # Added by sisakulint
steps:
- uses: actions/checkout@v4
- run: npm run build
Note: The auto-fix adds a default timeout of 5 minutes. Review and adjust this value based on your job’s actual requirements.
Best Practices #
1. Set Realistic Timeouts #
Choose timeouts based on typical job duration plus buffer:
# Typical duration: 3 minutes → Set timeout: 5-10 minutes
timeout-minutes: 10
2. Different Timeouts for Different Jobs #
Match timeout to job complexity:
jobs:
lint:
timeout-minutes: 5 # Fast checks
unit-test:
timeout-minutes: 15 # Moderate
integration-test:
timeout-minutes: 30 # Complex tests
deploy:
timeout-minutes: 20 # Deployment operations
3. Consider Matrix Jobs #
Matrix jobs may need longer timeouts:
jobs:
test:
strategy:
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
node: [16, 18, 20]
timeout-minutes: 20 # Account for slower runners
runs-on: ${{ matrix.os }}
steps:
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
4. Self-Hosted Runners #
Self-hosted runners may need adjusted timeouts:
jobs:
build:
runs-on: self-hosted
timeout-minutes: 60 # Self-hosted may have different performance
steps:
- run: ./long-build-process.sh
Recommended Timeout Values #
| Job Type | Recommended Timeout |
|---|---|
| Linting | 5-10 minutes |
| Unit Tests | 10-20 minutes |
| Integration Tests | 20-45 minutes |
| Build (Simple) | 10-15 minutes |
| Build (Complex) | 20-30 minutes |
| Docker Build | 15-30 minutes |
| Deployment | 10-20 minutes |
Step-Level Timeouts (Optional) #
For additional protection, you can also set step-level timeouts:
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 30 # Job-level timeout
steps:
- uses: actions/checkout@v4
timeout-minutes: 2 # Step-level timeout
- name: Install dependencies
timeout-minutes: 5
run: npm ci
- name: Run tests
timeout-minutes: 15
run: npm test
False Positives #
This rule has no false positives because:
- Explicit timeouts are always a best practice
- The default 6-hour timeout is rarely appropriate
- Setting timeouts has no negative side effects (when properly configured)
Related Rules #
- permissions: Limits job permissions
- commit-sha: Pins actions for security
References #
GitHub Documentation #
Testing #
To test this rule:
# Detect missing timeouts
sisakulint .github/workflows/*.yml
# Apply auto-fix
sisakulint -fix on .github/workflows/*.yml
Configuration #
This rule is enabled by default. To disable it:
sisakulint -ignore missing-timeout-minutes
However, disabling this rule is not recommended as explicit timeouts are an important security and resource management practice.
