|
| 1 | +--- |
| 2 | +title: 'Implementing a GitHub Actions Allow List as Code' |
| 3 | +author: Josh Johanning |
| 4 | +date: 2025-08-21 09:00:00 -0500 |
| 5 | +description: How to manage GitHub Actions allow lists using configuration as code for an improved management and user experience |
| 6 | +categories: [GitHub, Actions] |
| 7 | +tags: [GitHub, GitHub Actions, Policy Enforcement, Configuration as Code] |
| 8 | +media_subpath: /assets/screenshots/2025-08-21-github-actions-allow-list-as-code |
| 9 | +image: |
| 10 | + path: actions-allow-list-as-code-light.png |
| 11 | + width: 100% |
| 12 | + height: 100% |
| 13 | + alt: GitHub Actions Allow List as Code workflow |
| 14 | +mermaid: true |
| 15 | +--- |
| 16 | + |
| 17 | +## Overview |
| 18 | + |
| 19 | +As organizations scale and mature their use of GitHub Actions, maintaining oversight of action usage becomes increasingly important. While GitHub provides native [Actions permissions settings](https://docs.github.com/en/organizations/managing-organization-settings/disabling-or-limiting-github-actions-for-your-organization#allowing-select-actions-and-reusable-workflows-to-run), managing these through the UI can become cumbersome, error-prone, and developers have no idea which actions they're actually allowed to use. |
| 20 | + |
| 21 | +This post explores how to implement a GitHub Actions allow list using configuration as code which is version controlled, provides self-service visibility, and has a built-in request and approval process natively through pull requests. |
| 22 | + |
| 23 | +I'm going to be using my [actions-allow-list-as-code](https://github.com/joshjohanning-org/actions-allow-list-as-code) repository as the example implementation. This is the same repository I use to manage my own GitHub Actions allow list, so you can see it in action! 🚀 |
| 24 | + |
| 25 | +> If you're already sold on this idea and just want to get started, skip to the [TLDR / Getting Started](#tldr--getting-started) section below for the quick and dirty implementation details! |
| 26 | +{: .prompt-tip } |
| 27 | + |
| 28 | +## Why Configuration as Code? |
| 29 | + |
| 30 | +Before we talk about the benefits of this method, let's quickly review the GitHub Actions permission options (these can be defined at the enterprise, organization, or repository level): |
| 31 | + |
| 32 | +> - Allow all actions and reusable workflows |
| 33 | +> - *Increasingly less common for security-conscious organizations* |
| 34 | +> - Allow enterprise actions and reusable workflows |
| 35 | +> - *Not as common; don't allow any marketplace actions including GitHub's own actions* |
| 36 | +> - ⭐️ **Allow enterprise, and select non-enterprise, actions and reusable workflows** |
| 37 | +> - *This is becoming increasingly more common since organizations want to maintain better control over their third-party actions usage* |
| 38 | +
|
| 39 | +> As of [August 2025](https://github.blog/changelog/2025-08-15-github-actions-policy-now-supports-blocking-and-sha-pinning-actions/), there's a new setting below these to "Require actions to be pinned to a full-length commit SHA" which is the best practice to prevent against supply chain attacks. |
| 40 | +{: .prompt-tip } |
| 41 | + |
| 42 | +We're going to be focusing on the last option, which allows you to specify a list of approved actions and reusable workflows. This is where this setting shines when combined with configuration as code. |
| 43 | + |
| 44 | +{: .light } |
| 45 | +{: .dark } |
| 46 | +_GitHub Actions permission settings - with the "Allow enterprise, and select non-enterprise, actions and reusable workflows" setting checked_ |
| 47 | + |
| 48 | +The challenge with managing allow lists through the GitHub UI is that it has several drawbacks: |
| 49 | + |
| 50 | +1. **Lack of self-service visibility**: Developers can't easily see which actions are approved - so you most likely have to duplicate this list into documentation or a wiki page anyways |
| 51 | +2. **No approval process**: Changes are made directly without review by organization owners or [CI/CD admins](https://docs.github.com/en/organizations/managing-peoples-access-to-your-organization-with-roles/roles-in-an-organization#about-pre-defined-organization-roles) - and have to build out a separate process for developers to request new actions |
| 52 | +3. **No audit trail**: Limited visibility into when and why actions were added/removed (without digging up the audit log) |
| 53 | +4. **Scalability issues**: Managing hundreds of approved actions becomes unwieldy (better not miss a comma!) |
| 54 | +5. **No context**: No way to document why certain actions were approved or rejected |
| 55 | + |
| 56 | +## Implementation Walkthrough |
| 57 | + |
| 58 | +Now, let's walk through implementing the configuration as code solution using the [actions-allow-list-as-code](https://github.com/joshjohanning-org/actions-allow-list-as-code) repository. We'll take the different components step-by-step to see how they work together to create a robust allow list management system. |
| 59 | + |
| 60 | +### Allow List File Structure |
| 61 | + |
| 62 | +The heart of this approach is a simple YAML configuration file that lists all approved actions: |
| 63 | + |
| 64 | +```yml |
| 65 | +actions: |
| 66 | + - actionsdesk/github-actions-allow-list-as-code-action@* # have to allow this action to manage the allow list! |
| 67 | + - joshjohanning/* |
| 68 | + - azure/login@v2* |
| 69 | + - issue-ops/parser* |
| 70 | + - docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3.5.0 |
| 71 | +``` |
| 72 | +{: file='github-actions-allow-list.yml'} |
| 73 | +
|
| 74 | +This simple format allows for: |
| 75 | +
|
| 76 | +- Wildcards (`*`) for allowing all versions of an action (`issue-ops/parser*`) |
| 77 | +- Specific version pinning (`@v1`, `@main`) |
| 78 | +- Organization-wide approvals (`joshjohanning/*`) |
| 79 | +- Specific commit SHA pinning (`docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1`) |
| 80 | +- Documentation comments to provide context (add comments after the action with `#`) |
| 81 | + |
| 82 | +### Actions Workflow |
| 83 | + |
| 84 | +The automation workflow handles validation and deployment of the allow list: |
| 85 | + |
| 86 | +{% raw %} |
| 87 | +```yaml |
| 88 | +name: 🚀🔐 Deploy Actions allow list |
| 89 | +
|
| 90 | +on: |
| 91 | + push: |
| 92 | + branches: [main] |
| 93 | + paths: |
| 94 | + - github-actions-allow-list.yml |
| 95 | + - .github/workflows/actions-allow-list.yml |
| 96 | + pull_request: |
| 97 | + branches: [ main ] |
| 98 | + workflow_dispatch: |
| 99 | +
|
| 100 | +jobs: |
| 101 | + run: |
| 102 | + runs-on: ubuntu-latest |
| 103 | +
|
| 104 | + permissions: read-all |
| 105 | +
|
| 106 | + steps: |
| 107 | + - name: Checkout |
| 108 | + uses: actions/checkout@v5 |
| 109 | +
|
| 110 | + - name: validate yml |
| 111 | + run: | |
| 112 | + if yq eval github-actions-allow-list.yml; then |
| 113 | + echo "Actions YML is valid" |
| 114 | + else |
| 115 | + echo "Actions YML validation failed" |
| 116 | + exit 1 |
| 117 | + fi |
| 118 | +
|
| 119 | + - uses: actions/create-github-app-token@v2 |
| 120 | + if: github.event_name != 'pull_request' |
| 121 | + id: app-token |
| 122 | + with: |
| 123 | + app-id: ${{ vars.APP_ID }} |
| 124 | + private-key: ${{ secrets.PRIVATE_KEY }} |
| 125 | + owner: ${{ github.repository_owner }} |
| 126 | +
|
| 127 | + # if using Enterprise, use the `/enterprises/<enterprise-slug>` endpoint |
| 128 | + # and PAT - can't use GitHub app at Enterprise at Enterprise level |
| 129 | + # setting this in case someone changed the settings in the UI manually |
| 130 | + - name: Enable Actions Policy in Org |
| 131 | + if: github.event_name != 'pull_request' |
| 132 | + env: |
| 133 | + GH_TOKEN: ${{ steps.app-token.outputs.token }} # replace this if using PAT |
| 134 | + run: | |
| 135 | + gh api -X PUT /orgs/${{ github.repository_owner }}/actions/permissions \ |
| 136 | + -f enabled_repositories='all' \ |
| 137 | + -f allowed_actions='selected' |
| 138 | +
|
| 139 | + gh api -X PUT /orgs/${{ github.repository_owner }}/actions/permissions/selected-actions \ |
| 140 | + -F github_owned_allowed=true \ |
| 141 | + -F verified_allowed=true |
| 142 | +
|
| 143 | + - name: Deploy GitHub Actions allow list |
| 144 | + if: github.event_name != 'pull_request' |
| 145 | + uses: ActionsDesk/github-actions-allow-list-as-code-action@013d3b0b014f3a7656c5b0a28c00fe8c7e41b5e3 # v3.1.0 |
| 146 | + with: |
| 147 | + token: ${{ steps.app-token.outputs.token }} # replace this if using PAT |
| 148 | + organization: ${{ github.repository_owner }} |
| 149 | + allow_list_path: github-actions-allow-list.yml |
| 150 | +``` |
| 151 | +{: file='.github/workflows/actions-allow-list.yml'} |
| 152 | +{% endraw %} |
| 153 | +
|
| 154 | +Key features of this workflow: |
| 155 | +
|
| 156 | +1. **Pull Request Support & YML Validation**: Validates the YML without deploying the changes |
| 157 | +2. **GitHub App Authentication**: Uses a GitHub App for secure, auditable access (you could also use a Personal Access Token - and if you're trying to apply this at the Enterprise level, you would need to use a PAT since GitHub Apps can't be used at the Enterprise level yet) |
| 158 | +3. **Automated Deployment**: Only runs on pushes to main branch, and only triggered by changes to the allow list file or workflow file |
| 159 | +4. **Sets/Resets the Actions Policy**: Ensures the Actions policy is set to "selected actions" in case someone changed it manually in the UI |
| 160 | +
|
| 161 | +> I'm using a GitHub App here for authentication (with Organization > Administration permissions set to Read & Write) rather than a personal access token. This provides better security, auditing, and doesn't consume a license. Check out my [other post on GitHub Apps](/posts/github-apps/) if you're interested in learning more! 🚀 |
| 162 | +{: .prompt-info } |
| 163 | +
|
| 164 | +### Pull Request Template |
| 165 | +
|
| 166 | +The real power here is the built-in approval process through something that all GitHub users are probably already familiar with: pull requests. When someone wants to add a new action to the allow list, they create a pull request with the proposed changes. |
| 167 | +
|
| 168 | +To streamline the approval process, you can create a [pull request template](https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/creating-a-pull-request-template-for-your-repository) that guides contributors through providing the necessary information. Here's an example: |
| 169 | +
|
| 170 | +```md |
| 171 | +## 🚀 Action Addition Request |
| 172 | + |
| 173 | +### 📝 Action Details |
| 174 | +- **Action Name**: |
| 175 | +- **Version(s)**: |
| 176 | +- **Publisher**: (GitHub user/org or Marketplace publisher) |
| 177 | +- **Action Repository**: (link to source code) |
| 178 | + |
| 179 | +### 💡 Justification |
| 180 | +- **Use Case**: Describe how this action will be used |
| 181 | +- **Business Justification**: Why is this action needed? |
| 182 | +- **Security Review**: |
| 183 | + - [ ] Action is from a verified publisher |
| 184 | + - [ ] Action source code has been reviewed |
| 185 | + - [ ] No security concerns identified |
| 186 | +- **Alternatives Considered**: List any alternative actions evaluated |
| 187 | + |
| 188 | +### ✅ Approval Checklist |
| 189 | +- [ ] Action serves a legitimate business purpose |
| 190 | +- [ ] Publisher is trusted (GitHub user/org or verified Marketplace publisher) |
| 191 | +- [ ] Version pinning strategy is appropriate |
| 192 | +- [ ] Documentation/README is clear and professional |
| 193 | +- [ ] Action follows security best practices |
| 194 | + |
| 195 | +### 🧪 Testing |
| 196 | +- [ ] Action has been tested in a sandbox environment |
| 197 | +- [ ] Action works as expected |
| 198 | +- [ ] No unexpected side effects observed |
| 199 | + |
| 200 | +### 👥 Reviewers |
| 201 | +Please tag the appropriate teams for review: |
| 202 | +- [ ] @your-org/security-team (required for all Marketplace actions) 🔒 |
| 203 | +- [ ] @your-org/platform-team (required for infrastructure-related actions) ⚙️ |
| 204 | + |
| 205 | +### Additional Notes |
| 206 | +Add any additional context, concerns, or considerations here. |
| 207 | +``` |
| 208 | +{: file='.github/pull_request_template.md'} |
| 209 | + |
| 210 | +### CODEOWNERS |
| 211 | + |
| 212 | +While you're not required to use a CODEOWNERS file, it can be helpful to automatically request reviews from the right teams when someone creates a pull request to add a new action. Here's an example: |
| 213 | + |
| 214 | +```md |
| 215 | +# CODEOWNERS file for GitHub Actions allow list |
| 216 | +CODEOWNERS @joshjohanning-org/actions-approver-team |
| 217 | +github-actions-allow-list.yml @joshjohanning-org/actions-approver-team |
| 218 | +.github/workflows/actions-allow-list.yml @joshjohanning-org/actions-approver-team |
| 219 | +.github/dependabot.yml @joshjohanning-org/actions-approver-team |
| 220 | +README.md @joshjohanning-org/actions-approver-team |
| 221 | +``` |
| 222 | +{: file='.github/CODEOWNERS'} |
| 223 | + |
| 224 | +## TLDR / Getting Started |
| 225 | + |
| 226 | +To implement this solution in your organization: |
| 227 | + |
| 228 | +1. **Set up the YML file with approved actions**: Add in your approved actions ([example](https://github.com/joshjohanning-org/actions-allow-list-as-code/blob/main/github-actions-allow-list.yml)) |
| 229 | +2. **Set Up GitHub App (recommended if using this for org) or Personal Access Token (for Enterprise)**: Set up a token and grant the necessary permissions |
| 230 | + - If GitHub App: Organization > Administration permissions set to Read & Write |
| 231 | + - If using a Personal Access Token, `admin:enterprise` or `admin:org` depending on your use case |
| 232 | +3. **Configure Workflow**: See my [example](https://github.com/joshjohanning-org/actions-allow-list-as-code/blob/main/.github/workflows/actions-allow-list.yml), noting to modify the `app-id` and `private-key` secrets to match your GitHub App |
| 233 | + - If using a Personal Access Token, remove the `actions/create-github-app-token` step and update the subsequent steps to use the secret directly from the secret store |
| 234 | +4. **Create PR Template** (optional): Add a pull request template for new action requests ([example](https://github.com/joshjohanning-org/actions-allow-list-as-code/blob/main/.github/pull_request_template.md)) |
| 235 | +5. **Use CODEOWNERS** (optional): Automatically request reviews from the right teams when someone creates a pull request to add a new action ([example](https://github.com/joshjohanning-org/actions-allow-list-as-code/blob/main/CODEOWNERS)) |
| 236 | + |
| 237 | +## Summary |
| 238 | + |
| 239 | +If your organization is looking to manage GitHub Actions allow lists more effectively, implementing this configuration as code approach provides significant benefits over manual UI-based management. Whether it's this approach, [Terraform](https://registry.terraform.io/providers/integrations/github/latest/docs/resources/actions_organization_permissions), or rolling your own, the benefits include: |
| 240 | + |
| 241 | +- **Increased self-service visibility** through version-controlled configuration |
| 242 | +- **Built-in approval process** via pull requests |
| 243 | +- **Automation & validation** that eliminates manual work |
| 244 | +- **Auditability** with complete change history in Git |
| 245 | + |
| 246 | +To help illustrate the flow, I created a mermaid diagram of high-level process process from end-to-end. Happy automating! 🚀 |
| 247 | + |
| 248 | +```mermaid |
| 249 | +flowchart TD |
| 250 | + A[Developer wants new action] --> B[Creates PR with action request] |
| 251 | + B --> C[Security/Platform team reviews] |
| 252 | + C --> D{Approved?} |
| 253 | + D -->|Yes| E[PR merged to main] |
| 254 | + D -->|No| F[PR closed with feedback] |
| 255 | + E --> G[Workflow validates YAML] |
| 256 | + G --> H[GitHub App deploys allow list] |
| 257 | + H --> I[Action available organization-wide] |
| 258 | + F --> A |
| 259 | +``` |
0 commit comments