GitHub
Start here
Section titled “Start here”If you only do a few things, do these:
- Set up branch protection on the default branch.
- Use
GITHUB_TOKENor GitHub App tokens, not Personal Access Tokens (PATs) with broad access. - Remove privileged workflow triggers (
pull_request_target,workflow_run). - Pass untrusted values to shell commands through environment variables, not inline expressions.
- Pin third-party actions to a full commit SHA.
- Set minimum
permissionsin every workflow. - Run zizmor against
.github/workflows. - Enable Dependabot, with a cooldown, and make sure the dependency graph is correct.
Repository settings
Section titled “Repository settings”Ownership
Section titled “Ownership”A GitHub team in the tryhuset organization should be admin of each repository, not individual people. When someone leaves or changes team, access follows the team and nothing needs to be cleaned up by hand. This is also the team that appears as owner in the product’s Backstage entry.
Branch protection
Section titled “Branch protection”Protect the default branch so nobody can delete code or push past review and checks.
Go to Settings → Rules → Rulesets → New ruleset → New branch ruleset, add the default branch as a target, and enable:
- Restrict deletions
- Block force pushes
- Require a pull request before merging
- Require status checks to pass
GitHub Actions
Section titled “GitHub Actions”Workflows run with access to secrets and write permissions on the repository, so a mistake in a workflow can have a big impact. These are the most important measures.
Avoid privileged triggers
Section titled “Avoid privileged triggers”Avoid pull_request_target and workflow_run. Both run with access to secrets and elevated permissions, but can be triggered by anyone who opens a pull request from a fork. If the workflow checks out and runs code from the PR (tests, builds, installs), the attacker’s code runs with the repository’s permissions.
Instead:
- Use
pull_requestfor anything that runs code from others. It gets no secrets and only read access. - If you need both to run code and to do something privileged (e.g. post test results as a PR comment), split it up. Run the tests under
pull_requestand upload the results as an artifact. Then use a separateworkflow_runjob that only reads the artifact data, and never executes it, to do the privileged step. - Never check out the PR’s head code under
pull_request_target.
Use intermediate environment variables
Section titled “Use intermediate environment variables”Never put ${{ ... }} expressions with user-controlled values directly in a run: step. Values like a PR title, branch name or issue body can contain shell commands, which makes the step open to command injection. Set the value in env: and reference it as a shell variable:
- name: Check PR title env: TITLE: ${{ github.event.pull_request.title }} run: | echo "PR title is $TITLE"See GitHub’s security hardening guide for more.
Pin third-party actions
Section titled “Pin third-party actions”Tags are mutable: v1 can point at different code tomorrow than it does today. Pin third-party actions to a full commit SHA, with the version in a comment so Dependabot can keep it updated:
- uses: some-org/some-action@aed4d69de423d70d995a9fac4bb00dedb7b00f91 # v1.2.3Tools like ratchet or the gh-act CLI extension (gh act pin) can pin existing workflows for you. GitHub’s own actions (actions/*) are immutable and can be referenced by tag.
Use minimum permissions
Section titled “Use minimum permissions”If a token leaks, the damage is limited if it can only read source code.
- Set
permissionsexplicitly in every workflow, with read-only defaults. - Grant each job only what it needs. A build-and-test job doesn’t need
id-token: write, for example.
permissions: contents: readRun zizmor
Section titled “Run zizmor”zizmor finds many common security problems in GitHub Actions configuration, including the ones above. Run it when setting up or hardening a repository:
zizmor .github/workflowsSecret scanning and dependency graph
Section titled “Secret scanning and dependency graph”Secret scanning
Section titled “Secret scanning”On public repositories, enable secret scanning and push protection. Push protection blocks pushes that contain secrets, and the scanner also finds secrets that are already in the repository.
On our plan, secret scanning isn’t available for private repositories, so use gitleaks instead. See GitHub Advanced Security. Neither option scans what ends up in built artifacts such as Docker images. See Secrets Management for more.
Dependency graph
Section titled “Dependency graph”GitHub uses the dependency graph to raise Dependabot alerts when a dependency has a known vulnerability. Alerts are only as good as the graph, so make sure it actually reflects what the project uses (for example, by committing lockfiles). See Dependency Management.
Tokens
Section titled “Tokens”Use GitHub’s built-in tokens instead of creating your own PATs. In order of preference:
GITHUB_TOKENfor access to the same repository.- GitHub App installation token when you need access across repositories.
- PAT only if you have a concrete reason and no better alternative.
GitHub App tokens have several advantages over PATs. They are short-lived (one hour), scoped to only the permissions and repositories the job needs, and they keep working when the person who set them up leaves or has their account deactivated.
Creating a GitHub App
Section titled “Creating a GitHub App”- Register a new app at https://github.com/settings/apps/new.
- Give it a name and a URL.
- Disable the webhook unless you need it.
- Under Permissions & events, grant only the permissions the app needs.
- Under Where can this GitHub App be installed?, choose Only on this account.
- Click Create GitHub App, and copy the Client ID (not sensitive).
- Under General → Private keys, generate a new key. Store it somewhere safe and delete it from your disk once it has been added where it is used.
- Under Advanced → Transfer ownership, transfer the app to the
tryhusetorganization, and ask a GitHub organization admin to approve the transfer. - Install the app in the
tryhusetorganization and select only the repositories it needs.
Using the app in a workflow
Section titled “Using the app in a workflow”In each repository that uses the app, add the Client ID as a repository variable named CLIENT_ID and the private key as a repository secret named PRIVATE_KEY (Settings → Secrets and variables → Actions).
Token for the current repository:
- uses: actions/create-github-app-token@v3 id: app-token with: client-id: ${{ vars.CLIENT_ID }} private-key: ${{ secrets.PRIVATE_KEY }}Token for specific repositories:
- uses: actions/create-github-app-token@v3 id: app-token with: client-id: ${{ vars.CLIENT_ID }} private-key: ${{ secrets.PRIVATE_KEY }} owner: ${{ github.repository_owner }} repositories: | repo1 repo2Leave out repositories to get a token for all repositories the app is installed on. Use the token with ${{ steps.app-token.outputs.token }}.
Other tools
Section titled “Other tools”- CodeQL: scans both application code and GitHub Actions workflows. Only available on public repositories for us, see GitHub Advanced Security.
- Trivy: scans Docker images, including for secrets baked into the image.
- Dependabot: keeps dependencies and actions up to date. Regular patching keeps updates small and applications safer.
Adapted from Nav’s security playbook.