Skip to main content

harden-github-repo-security

Apply GitHub repository security protections tier by tier — rulesets, read-only Actions token, secret scanning + push protection, Dependabot, and (gated) required status checks / required PR with a GitHub App bypass for a CI auto-commit bot. Mutating and confirmation-gated: always assess first, apply the no-regret baseline, then decide required checks separately. Use when hardening a public user-owned repo after an audit, when a repo has no branch protection, when adding required checks without breaking a bot that pushes to the default branch, or when provisioning a GitHub App bypass actor for trusted automation.

소스 정보

저장소
pjt222/agent-almanac
최근 소스 활동
2026년 9월 4일 12:42
감지된 SKILL.md 언어
영어
스타
34
포크
4

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
harden-github-repo-security
description
Apply GitHub repository security protections tier by tier — rulesets, read-only Actions token, secret scanning + push protection, Dependabot, and (gated) required status checks / required PR with a GitHub App bypass for a CI auto-commit bot. Mutating and confirmation-gated: always assess first, apply the no-regret baseline, then decide required checks separately. Use when hardening a public user-owned repo after an audit, when a repo has no branch protection, when adding required checks without breaking a bot that pushes to the default branch, or when provisioning a GitHub App bypass actor for trusted automation.
license
MIT
allowed-tools
Read Write Edit Bash
metadata
{"author":"Philipp Thoss","version":"1.0","domain":"git","complexity":"advanced","language":"multi","tags":"github, security, rulesets, branch-protection, github-actions, hardening","locale":"es","source_locale":"en","source_commit":"5addd67a4","fence_basis_commit":"5addd67a4","translator":"(untranslated stub)","translation_date":"2026-07-16"}
# Harden GitHub Repository Security Apply GitHub protections in order of blast radius: assess, then a no-regret baseline that breaks no CI, then a gated decision on required checks / PR. No-regret means every item is either a control you will not want to undo or a decision you will not want to have skipped — not that the tier is decision-free. Every mutating step is confirmation-gated. Running example: a **public, user-owned** repo whose CI auto-commits to the default branch via `stefanzweifel/git-auto-commit-action` with the default `GITHUB_TOKEN`. Throughout, `R` = `OWNER/REPO` (e.g. `pjt222/agent-almanac`). Set it once: `R=OWNER/REPO`. All `gh api` writes require repo admin. ## When to Use - Hardening a public user-owned repo after `assess-github-repo-security` - A repo has no ruleset / branch protection and you want a safe baseline - Adding required status checks or required PR **without** breaking a bot that pushes to the default branch - Provisioning a GitHub App (or deploy key) bypass actor for trusted automation ## Inputs - **Required**: `OWNER/REPO` and admin access (`gh auth status` shows admin) - **Required**: whether a CI bot pushes to the default branch (and which action/identity) - **Optional**: assessment report from `assess-github-repo-security` - **Optional**: exact CI check `context` name(s) to require (must run on `push`) - **Optional**: GitHub App id + private key if going to required checks/PR ## Procedure ### Step 1: Assess First and Confirm Scope Never mutate blind. Run the read-only assessment and confirm the plan with the user. ```bash # 1. Run the companion read-only skill first (or its core probes): gh api /repos/$R --jq '{visibility,default_branch,is_org:.organization!=null}' gh api /repos/$R/rulesets --jq '.[] | {id,name,enforcement}' gh api /repos/$R/actions/permissions/workflow # 2. Confirm the two decisive facts before any write: # - Is the repo PUBLIC and USER-OWNED? (rulesets are free here) # - Does a bot push to the default branch? (gates Step 3 entirely) ``` Confirm with the user: baseline (Step 2) breaks no CI, but it is not decision-free — its fork-PR approval item is a deliberate choice, not a default to apply blind. Step 3 (required checks / PR) is a separate opt-in that **will** block a default-branch bot unless a bypass is provisioned first. **Expected:** You know visibility, owner type, current rulesets, current token default, and whether a bot pushes to the default branch. The user has approved applying at least the baseline. **On failure:** If the repo is **private and user-owned**, rulesets need GitHub Pro — stop and surface that. If not admin, stop (writes will 403). If a bot pushes to the default branch, flag that Step 3 is blocked until Step 3a runs. ### Step 2: Apply the No-Regret Baseline (breaks no CI auto-commit) This tier hardens the repo without breaking a direct-push bot. Apply after confirmation. Force-push/deletion protection, a read-only token default, the free security features, and policy files. ```bash # 2a. Default-branch ruleset: force-push + deletion protection ONLY. # These do NOT block the bot's direct push. gh api --method POST /repos/$R/rulesets --input - <<'JSON' { "name": "protect-default", "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" } ] } JSON # 2b. Actions token: read-only default + no token PR approval. # A DEFAULT (not a cap) for same-repo push events, so the bot still works # once its job declares contents: write. gh api --method PUT /repos/$R/actions/permissions/workflow \ -f default_workflow_permissions=read \ -F can_approve_pull_request_reviews=false # 2c. Free security features (all free on public repos): gh api -X PUT repos/$R/vulnerability-alerts # alerts + dependency graph gh api -X PUT repos/$R/automated-security-fixes # Dependabot fix PRs (separate toggle!) gh api -X PATCH repos/$R --input - <<'JSON' {"security_and_analysis":{"secret_scanning":{"status":"enabled"},"secret_scanning_push_protection":{"status":"enabled"}}} JSON gh api -X PUT repos/$R/private-vulnerability-reporting # 2d. Fork-PR approval — READ it here; the value is a decision, not a default to # apply blind. See the paragraph below before writing. gh api repos/$R/actions/permissions/fork-pr-contributor-approval # Write, once decided (loosest to strictest): # first_time_contributors_new_to_github | first_time_contributors | all_external_contributors # gh api -X PUT repos/$R/actions/permissions/fork-pr-contributor-approval \ # -f approval_policy=first_time_contributors_new_to_github ``` **Decide fork-PR approval deliberately.** It is the one item in this tier that is a decision rather than a control, which is why the tier is *no-regret* and not *zero-downside* — the term this skill used until #768, retired in #707 because a tier containing a decision is not downside-free. The public-repo default is "Require approval for first-time contributors", and fork `pull_request` runs already receive a read-only `GITHUB_TOKEN` with no access to secrets — so the default is safe. What it also does is run **nothing at all** on a first-time contributor's PR until a maintainer clicks approve, and if nobody notices, the contributor sees a PR with no checks and no signal. Measured on this repository's first external PR: 0 workflow runs, 0 check-runs, 0 check-suites. The setting is readable **and writable** over the API — it is not a UI-only toggle, and believing otherwise sends people designing around a constraint that does not exist. **The predicate is fork-REACHABILITY, not "does this repo have secrets."** That test is wrong in both directions: a repo whose deploy runs only on push-to-main is not endangered by loosening this, because a fork PR cannot trigger it and the platform withholds secrets from fork runs regardless; a repo whose `pull_request` validators run on **self-hosted runners** should stay strict with no secret anywhere, because arbitrary code execution on your own hardware, resource abuse and cache-poisoning are what the gate is actually for — and none of them is "touches secrets". Measure reachability with the four commands in `guides/protecting-github-repositories.md` under "Decide fork-PR approval deliberately", and read the ruler warnings beside them rather than reaching for the obvious grep — an unanchored `pull_request` scan matches `pull_request_target`, the one trigger that runs base-repo code with secrets, and misses `on: [push, pull_request]` entirely. Both errors read as safe. Then add two tracked files (commit them): `.github/dependabot.yml` — include a `github-actions` block so action SHAs stay fresh: ```yaml version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ``` `.github/SECURITY.md` — a short disclosure policy pointing reporters at the "Report a vulnerability" button (private vulnerability reporting, enabled above). Flipping the token default to `read` silently strips write from **every** workflow that assumed a write default, not just the auto-commit job. Audit **all** files in `.github/workflows/` and declare the minimal `permissions:` each job actually needs — common scopes are `contents`, `packages`, `pull-requests`, `pages`, and `id-token`. For the running example: top-level `permissions: {}`, `contents: write` only on the auto-commit job, and every `uses:` pinned to a full 40-char commit SHA with the version in a trailing comment. **Expected:** `gh api /repos/$R/rulesets` shows `protect-default` active; `.../actions/permissions/workflow` shows `default_workflow_permissions: read` and `can_approve_pull_request_reviews: false`; secret scanning + push protection + Dependabot + private reporting are on; `.github/dependabot.yml` and `.github/SECURITY.md` are committed. The bot's next run still pushes. `.../actions/permissions/fork-pr-contributor-approval` has been **read**, the guide's four reachability commands have been **run**, and their results are quoted in the same turn as the decision — one line suffices, e.g. `1: 12 pull_request workflows · 2: none · 3: 2 secret-bearing · 4: none`. The policy is then left or changed, and **both branches require that quote**: tightening on instinct is the same defect as leaving an unexamined default, and the quote is what distinguishes either from a decision. Quote the output rather than asserting the measurement — every other criterion in this step is checkable against live API state, and this one is checkable only against what you paste. **On failure:** If 2b makes an existing bot push 403 — or any other workflow fails after losing a write scope it silently relied on — the job is missing the explicit `permissions:` it needs (e.g. `contents: write`, `packages: write`, `pull-requests: write`); add the minimal scope back (the read-only default is not a hard cap for same-repo pushes). If `automated-security-fixes` seems to do nothing, verify `vulnerability-alerts` (2c line 1) ran first — alerts are a separate prerequisite toggle from fixes. If push protection later fails a bot run, treat the flagged secret as a **real finding** — the bot cannot click the interactive web bypass. ### Step 3: Decision Gate — Required Status Checks / Required PR STOP. Required checks and required PR **block direct pushes to the ref**, not just PR merges. If a bot pushes to the default branch, turning either on **will break it** unless you first provision a bypass. The default `GITHUB_TOKEN` / `github-actions[bot]` **cannot** be a bypass actor (GitHub design). Do not enable this tier on a bot-pushing branch without a bypass in place. Also, on a **solo** repo, `required_approving_review_count >= 1` is a self-lockout (you cannot approve your own PR) — keep it `0`. And a required check that only runs on `pull_request` never reports on a direct push, so it **permanently** blocks the bot. Ensure the check runs on `push`. **Step 3a — provision the bot bypass FIRST (if a bot pushes to this branch).** Recommended: a GitHub App installation token. (Deploy key is a narrower single-repo alternative: add an SSH deploy key with write access and use `actor_type: DeployKey` in 3b.) Create a GitHub App (personal account, Settings > Developer settings > GitHub Apps), permission Repository > Contents: Read and write; install it on THIS repo. Store the App id as a repo variable and the private key as an Actions secret. In the workflow, mint a token and pass it to checkout + the commit action: ```yaml - uses: actions/create-github-app-token@<sha> # v2 id: app-token with: { app-id: ${{ vars.APP_ID }}, private-key: ${{ secrets.APP_KEY }} } - uses: actions/checkout@<sha> with: { token: ${{ steps.app-token.outputs.token }} } - uses: stefanzweifel/git-auto-commit-action@<sha> with: { ... } # inherits the app token via the checkout credentials ``` The numeric App id used in Step 3b is shown on the App's own page: Settings > Developer settings > GitHub Apps > `<your app>` > About ("App ID"). It is unrelated to the repository id and cannot be derived from /repos/$R. **Deploy-key alternative (no App to maintain).** Register a write deploy key and push over SSH; the checkout's `ssh-key` makes `git-auto-commit-action` push as that identity: ```bash # ssh-keygen -t ed25519 -N '' -f deploy_key # then register the public half: gh api repos/$R/keys -f title="ci-bot" -f key="$(cat deploy_key.pub)" -F read_only=false gh secret set DEPLOY_KEY < deploy_key # store the private half, then delete the local copy # - uses: actions/checkout@<sha> # with: { ssh-key: ${{ secrets.DEPLOY_KEY }} } # remote becomes SSH via the key # - uses: stefanzweifel/git-auto-commit-action@<sha> # inherits the SSH remote # In 3b use actor_type "DeployKey"; GitHub stores its actor_id as null (it matches # ANY write deploy key on the repo), so keep the deploy-key list minimal. ``` **Higher-assurance alternative (no standing bypass actor).** If a manual merge click is acceptable, convert the job to open a PR with the App token (e.g. `peter-evans/create-pull-request`) and let a human merge. No bypass actor is needed and every rule stays enforced even against the automation's identity. The App token is still required: a PR opened with the plain `GITHUB_TOKEN` does not trigger `pull_request`/`push` workflows, so required checks never run and it sits `expected` forever (see Common Pitfalls). **Step 3b — apply the hardened ruleset with the App as an `Integration` bypass actor.** Replace `RULESET_ID` with the id from Step 2a, `<APP_ID>` with your App id, and `build` with your real check context. `bypass_mode: always` because the action pushes directly (use `pull_request` only if the bot opens PRs). ```bash gh api --method PUT /repos/$R/rulesets/RULESET_ID --input - <<'JSON' { "name": "protect-default", "target": "branch", "enforcement": "active", "conditions": { "ref_name": { "include": ["~DEFAULT_BRANCH"], "exclude": [] } }, "bypass_actors": [ { "actor_id": <APP_ID>, "actor_type": "Integration", "bypass_mode": "always" } ], "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" }, { "type": "pull_request", "parameters": { "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": true, "require_code_owner_review": false, "require_last_push_approval": false, "required_review_thread_resolution": false, "allowed_merge_methods": ["merge"] } }, { "type": "required_status_checks", "parameters": { "strict_required_status_checks_policy": true, "do_not_enforce_on_create": true, "required_status_checks": [ { "context": "build" } ] } } ] } JSON # Verify the bypass actor is present: gh api /repos/$R/rulesets/RULESET_ID --jq '.bypass_actors' ``` `do_not_enforce_on_create: true` avoids the new-branch catch-22 (CI can't have run on a branch that does not exist yet). If there is **no** bot on this branch, you may skip 3a and omit `bypass_actors`. **Bypass is whole-ruleset, not per-rule.** The single ruleset above lets the bypass actor skip `deletion`/`non_fast_forward` too — a bypass actor could force-push or delete the branch. To keep ref protection **universal** while exempting only the check, split into two stacked rulesets (they aggregate, most-restrictive-wins): ruleset A = `deletion` + `non_fast_forward` with `bypass_actors: []` (applies to everyone, including you), ruleset B = `required_status_checks` with the bot — and, if you want to keep your own direct-push, your own `{ "actor_type": "User", "actor_id": <your-user-id>, "bypass_mode": "always" }` — in `bypass_actors`. A successful bypassed push prints `remote: Bypassed rule violations … Required status check "<name>" is expected`. Trade-off: `strict_required_status_checks_policy: true` ("branch must be up to date") forces every open human PR to be re-updated each time the bot auto-commits to the default branch, and `dismiss_stale_reviews_on_push: true` compounds the churn — a chatty bot can make PRs perpetually unmergeable. On a frequently-auto-committed branch prefer `strict_required_status_checks_policy: false` (loose) unless up-to-date-before-merge is genuinely required. **Expected:** The ruleset now enforces the required check and required-PR rule for humans and forks, `.bypass_actors` lists the App (`Integration`), and the bot's next run still lands on the default branch (via the App token). Humans must open a PR; the check must be green. **On failure:** If the bot 403s after 3b, the App identity is not actually the bypass actor — confirm the workflow passes the App token to **checkout** (not just to the commit action) and that `<APP_ID>` in `bypass_actors` matches. If the bot's push sits `expected`/`pending` forever, the required check does not run on `push` — make it trigger on `push` or drop it from the required list. If the
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기