Keeping projects secure
Protect accounts, dependencies, secrets and workflows with 2FA, Dependabot, secret scanning, code scanning, rulesets and hardened Actions.
- Secure accounts with two-factor authentication and passkeys
- Use Dependabot, secret scanning with push protection, and code scanning
- Harden branches with rulesets and workflows with pinned actions and least privilege
An open-source game might not seem like a target, but attackers love popular repositories: compromise one, and you can ship malware to everyone who installs it. GitHub’s Security tab gathers the defenses:
| Feature | Protects against |
|---|---|
| Two-factor authentication / passkeys | a stolen password taking over a maintainer’s account |
| Dependabot alerts | dependencies with known vulnerabilities (from the GitHub Advisory Database) |
| Dependabot updates | falling behind - it opens PRs that bump vulnerable or outdated dependencies |
| Secret scanning + push protection | committed tokens and keys; push protection blocks the push before the secret lands |
| Code scanning (CodeQL) | vulnerabilities in your own code, like SQL injection, flagged on PRs |
| Branch protection / rulesets | unreviewed or untested code reaching main, force pushes, deleted history |
| SECURITY.md + private vulnerability reporting | researchers announcing a vulnerability publicly before it’s fixed |
1version: 2
2updates:
3 - package-ecosystem: pip
4 directory: "/"
5 schedule:
6 interval: weekly
7 - package-ecosystem: github-actions # keep the actions in workflows up to date too
8 directory: "/"
9 schedule:
10 interval: weeklyHardening GitHub Actions
Workflows run code with access to your secrets and repository, so they’re a favorite target. The essentials:
- Pin third-party actions to a full commit SHA:
uses: some/action@8f4b7f84...instead of@v2. Tags can be moved to point at malicious code - this has happened. (Add the version as a comment; Dependabot can still update pinned SHAs.) - Least privilege: set
permissions:at the top of every workflow, starting fromcontents: read. - Beware
pull_request_target: it runs with secrets and write access in the context of the base repository, even for PRs from forks. Never check out and run the PR’s code in it. - Don’t interpolate untrusted input into scripts:
run: echo "${{ github.event.issue.title }}"lets anyone who opens an issue inject shell commands. Pass it through an environment variable instead.
Try it
Audit this workflow
A contributor proposed this workflow to welcome new issue authors. Click every line that’s a security problem, then check what you missed.
Click every part that looks suspicious. There are 4.
1name: Welcome
2on:
3 issues:
4 types: [opened]
5permissions:
6 issues: write
7jobs:
8 greet:
9 runs-on: ubuntu-latest
10 steps:
11 - uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
12 env:
13 TITLE: ${{ github.event.issue.title }}
14 with:
15 script: |
16 await github.rest.issues.createComment({
17 ...context.repo,
18 issue_number: context.issue.number,
19 body: `Thanks for reporting "${process.env.TITLE}"! A maintainer will take a look.`,
20 })Key takeaways
Turn on 2FA (or passkeys) - maintainer accounts are the keys to everyone’s installs.
Dependabot alerts and updates handle vulnerable dependencies; secret scanning with push protection stops leaked tokens.
Rulesets and branch protection keep unreviewed, untested code out of main.
Harden workflows: pin actions to SHAs, use least-privilege permissions, and never interpolate untrusted input into scripts.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: automate GitHub chores with Python
Real GitHub work involves lots of small automation: matching CODEOWNERS, expanding build matrices, bumping versions, reading the API. Write those helpers in Python and run them against sample inputs - locally in your browser, with no GitHub account needed.
Audit the actions
The input is a workflow file. Find every uses: reference and classify it, printing owner/repo@ref: CATEGORY in order:
local- starts with./docker- starts withdocker://pinned- the ref is a full 40-character hex commit SHAtrusted tag- an unpinned tag or branch on an action owned byactionsorgithubUNPINNED- any other third-party action on a tag or branch
Finally print N unpinned third-party actions (or all clear) - and permissions: missing if the file has no permissions: key at all.
- The welcome workflow
- Hardened
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Triage Dependabot alerts
Each input line is an alert as JSON: {"package": ..., "severity": "critical"|"high"|"medium"|"low", "vulnerable": "<2.3.4", "fixed_in": "2.3.4"|null, "state": "open"|"dismissed"|"fixed"}. For open alerts only, print them most severe first (ties by package name) as CRITICAL pillow: update to 10.3.0 or HIGH tinyparse: no fix yet, then a summary open: 1 critical, 2 high, 0 medium, 0 low.
- This week
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…