Caller Validation
Pin Reusable Workflows to SHA
Branch or tag references for reusable workflows create supply chain attack vectors. Attackers who compromise upstream repositories can modify workflows and steal secrets from all callers. Always pin to full SHA commits.
Reusable Workflow Pattern¶
type: choice
options:
- dev
- staging
- production
description: 'Target deployment environment'
version:
required: true
type: string
description: 'Deployment version (semantic version format)'
secrets:
wif_provider:
required: true
description: 'GCP Workload Identity Federation provider'
wif_service_account:
required: true
description: 'GCP service account for deployment'
slack_webhook:
required: false
description: 'Slack webhook for deployment notifications'
permissions:
contents: read
id-token: write
jobs:
validate:
runs-on: ubuntu-latest
steps:
- name: Validate caller repository
run: |
ALLOWED_REPOS=(
"org/service-frontend"
"org/service-backend"
"org/service-api"
)
CALLER_REPO="${{ github.repository }}"
for repo in "${ALLOWED_REPOS[@]}"; do
if [[ "$CALLER_REPO" == "$repo" ]]; then
echo "Authorized caller: $CALLER_REPO"
exit 0
fi
done
echo "::error::Unauthorized caller: $CALLER_REPO"
echo "::error::Allowed repositories: ${ALLOWED_REPOS[*]}"
exit 1
- name: Validate version format
run: |
VERSION="${{ inputs.version }}"
if [[ ! "$VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9\.]+)?$ ]]; then
echo "::error::Invalid version format: $VERSION"
echo "::error::Expected format: vX.Y.Z or vX.Y.Z-prerelease"
exit 1
fi
echo "Valid version: $VERSION"
deploy:
runs-on: ubuntu-latest
needs: validate
environment: ${{ inputs.environment }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1
- uses: google-github-actions/auth@55bd3a7c6e2ae7cf1877fd1ccb9d54c0503c457c # v2.1.2
with:
workload_identity_provider: ${{ secrets.wif_provider }}
service_account: ${{ secrets.wif_service_account }}
- name: Deploy to environment
env:
ENVIRONMENT: ${{ inputs.environment }}
VERSION: ${{ inputs.version }}
run: |
echo "Deploying $VERSION to $ENVIRONMENT"
./scripts/deploy.sh "$ENVIRONMENT" "$VERSION"
- name: Notify deployment
if: always() && secrets.slack_webhook != ''
env:
SLACK_WEBHOOK: ${{ secrets.slack_webhook }}
ENVIRONMENT: ${{ inputs.environment }}
VERSION: ${{ inputs.version }}
STATUS: ${{ job.status }}
run: |
curl -X POST "$SLACK_WEBHOOK" \
-H 'Content-Type: application/json' \
-d "{\"text\":\"Deployment $STATUS: $VERSION to $ENVIRONMENT\"}"
Caller Workflow:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
tags: ['v*']
permissions:
contents: read
id-token: write
jobs:
deploy:
uses: org/workflows/.github/workflows/reusable-deploy-secure.yml@a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 # v2.1.0
with:
environment: production
version: ${{ github.ref_name }}
secrets:
wif_provider: ${{ secrets.WIF_PROVIDER }}
wif_service_account: ${{ secrets.WIF_SERVICE_ACCOUNT }}
slack_webhook: ${{ secrets.SLACK_WEBHOOK }}
Security Features:
- Choice input for environment with fixed values
- String input with regex validation for version
- Explicit secret passing (no
secrets: inherit) - Caller repository allowlist validation
- SHA-pinned workflow reference in caller
- Environment protection gates
- OIDC authentication (no stored cloud credentials)
- Minimal GITHUB_TOKEN permissions
Security Best Practices¶
- Always pin to SHA: Never use branch or tag references for reusable workflows in production
- Validate all inputs: Use
choicetype or runtime validation for string inputs - Explicit secrets only: Avoid
secrets: inherit, declare required secrets explicitly - Restrict callers: Validate
github.repositoryto allowlist authorized callers - Minimal permissions: Declare minimal
permissionsblock in reusable workflow - Environment protection: Use environment gates for deployment workflows
- Prefer OIDC: Use Workload Identity Federation instead of stored secrets
- Document requirements: Clear descriptions for inputs and secrets
- Audit usage: Monitor which repositories call shared workflows
- Version workflows: Tag reusable workflows with semantic versions
Common Mistakes¶
Mistake 1: Unpinned Workflow Reference¶
Problem: Branch reference allows supply chain attacks
Fix: Pin to SHA
jobs:
deploy:
uses: org/workflows/.github/workflows/deploy.yml@a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 # v1.2.3
Mistake 2: Unvalidated String Input¶
Problem: Command injection via unvalidated input
# DANGEROUS
on:
workflow_call:
inputs:
command:
type: string
jobs:
run:
steps:
- run: ${{ inputs.command }}
Fix: Validate input or use choice type
on:
workflow_call:
inputs:
task:
type: choice
options: [build, test, deploy]
jobs:
run:
steps:
- name: Validate task
run: |
case "${{ inputs.task }}" in
build|test|deploy) ;;
*) exit 1 ;;
esac
- run: ./scripts/${{ inputs.task }}.sh
Mistake 3: Using secrets: inherit¶
Problem: Excessive secret exposure
Fix: Explicit secret passing
jobs:
deploy:
uses: ./.github/workflows/reusable-deploy.yml@a1b2c3d4e5f6
secrets:
deploy_key: ${{ secrets.DEPLOY_KEY }}
Mistake 4: No Caller Validation¶
Problem: Any repository can call workflow
Fix: Add caller allowlist
jobs:
validate:
steps:
- run: |
if [[ "${{ github.repository }}" != "org/allowed-repo" ]]; then
exit 1
fi
Troubleshooting¶
| Issue | Cause | Solution |
|---|---|---|
| Workflow not found | Incorrect path or SHA | Verify workflow exists at .github/workflows/ in referenced commit |
| Input validation failed | Invalid input format | Check input against validation rules in reusable workflow |
| Secret not available | Secret not passed or wrong name | Verify secret name matches between caller and reusable workflow |
| Caller validation failed | Repository not in allowlist | Add repository to allowed repositories list |
| Permission denied | Insufficient GITHUB_TOKEN permissions | Check permissions block in both caller and reusable workflow |
| Environment protection blocks | Missing approval or branch policy | Configure environment protection rules or approve deployment |
Quick Reference¶
Input Type Selection¶
| Input Data | Type | Validation |
|---|---|---|
| Fixed set of values | choice |
Automatic |
| Environment name | environment |
GitHub validates |
| Free text | string |
Runtime validation required |
| Version number | string |
Regex validation |
| Feature flag | boolean |
Type validated |
| Count/index | number |
Range validation |
Secret Passing Patterns¶
| Pattern | Risk | Use Case |
|---|---|---|
| No secrets | Minimal | OIDC-only workflows |
| Explicit secrets | Low | Production workflows |
secrets: inherit |
High | Trusted internal workflows only |
Reusable Workflow Security Checklist¶
- [ ] Workflow pinned to SHA in caller (not branch/tag)
- [ ] All string inputs validated with allowlist or regex
- [ ] Secrets passed explicitly (no
secrets: inherit) - [ ] Caller repository validated against allowlist
- [ ] Minimal
permissionsblock declared - [ ] Environment protection for deployments
- [ ] OIDC preferred over stored secrets
- [ ] Input and secret requirements documented
- [ ] Version tag added for tracking
- [ ] Dependabot configured for updates
Related Pages¶
- Workflow Trigger Security -
workflow_callvs other triggers, event security - Environment Protection Patterns - Deployment gates for reusable workflows
- Token Permissions - Permission inheritance in reusable workflows
- Secret Management - Secret scoping and OIDC patterns
- Action Pinning - SHA pinning strategy for dependencies