Runbook — settings-drift-checker GitHub App¶
The settings-posture-drift-check workflow (and any other drift-check that needs admin-scoped read access to repo settings) authenticates as a dedicated read-only GitHub App. This isolates the admin-read blast radius from both secrets.GITHUB_TOKEN (which never receives admin scope) and from the linpeas-flake-bumper App used for bump commits.
Why a separate App¶
secrets.GITHUB_TOKEN— never granted admin scope by GitHub regardless ofpermissions:block declarations. ReturnsHTTP 403 Resource not accessible by integrationon/repos/X/actions/permissions,security_and_analysisfields of/repos/X, and environment endpoints.linpeas-flake-bumper— the bump-bot App. Adding admin scope to it widens the blast radius of its write-capable installation token. Keep it scoped to its bump duties.settings-drift-checker— this App. Read-only. Administration:Read + Metadata:Read. Cannot mutate any state. If its private key leaks, the worst an attacker gains is a snapshot of already-public repo metadata plus admin-only fields they could read by viewing repo Settings in a browser as a viewer.
One-time setup¶
-
Create the App. https://github.com/settings/apps/new (or the user-account Apps page if registering personally).
-
GitHub App name:
settings-drift-checker(or any unique name). -
Homepage URL:
https://github.com/rvenutolo/linPEAS-flake -
Webhook: deactivate (no events needed).
-
Repository permissions:
- Administration: Read-only
- Metadata: Read-only
-
Subscribe to events: none.
-
Where can this GitHub App be installed?: Only on this account.
-
-
Note the Client ID (shown on the App's "About" page after creation).
-
Generate a private key. App page → "Private keys" → "Generate a private key". A
.pemfile downloads. -
Install the App on this repo. App page → "Install App" → choose your account → "Only select repositories" →
rvenutolo/linPEAS-flake. -
Add the Client ID as a repository variable:
-
Add the private key as a repository secret:
gh secret set SETTINGS_DRIFT_APP_PRIVATE_KEY --body "$(cat path/to/downloaded-key.pem)" --repo rvenutolo/linPEAS-flakeThen delete the local
.pemfile — the only copy now lives in GitHub Secrets. -
Manually dispatch the workflow once to confirm setup:
gh workflow run settings-posture-drift-check.yml --repo rvenutolo/linPEAS-flake gh run watch --repo rvenutolo/linPEAS-flakeExpect the
drift-checkjob to pass withsettings-posture: live repo configuration matches docs/security/settings-posture.mdin the log.
Failure modes¶
Error: Could not create installation access token— App not installed on this repo, orSETTINGS_DRIFT_APP_CLIENT_ID/SETTINGS_DRIFT_APP_PRIVATE_KEYmismatched or rotated without updating both halves.HTTP 403 Resource not accessible by integrationon a settings endpoint — App permissions narrower than required. Open the App's "Permissions & events" page and confirm Administration:Read + Metadata:Read are present. After widening, GitHub requires installation owners to accept the new permission scope before it takes effect.- Workflow fires but
drift-checkreports drift on settings nobody touched — most often: GitHub added a new field to one of the endpoints with a default that differs from the doc's expectation. Treat as a security review: either the new default is acceptable (update the doc) or it's a posture regression (revert via API or UI).
Rotation¶
Rotate the private key every time:
- the previous key may have been exposed (e.g., committed accidentally, shared in a screenshot, copied to a non-secret store)
- a maintainer with key access leaves
- the App's permissions are widened
To rotate:
-
App page → "Private keys" → "Generate a private key" (creates a new key, does not invalidate the old one).
-
Update the repo secret:
-
App page → delete the old key.
-
Re-dispatch the workflow to confirm the new key works.
Reuse for related drift checks¶
This App is intentionally read-only and broadly useful. Any future workflow that needs Administration:Read + Metadata:Read against this repo should reuse vars.SETTINGS_DRIFT_APP_CLIENT_ID + secrets.SETTINGS_DRIFT_APP_PRIVATE_KEY rather than creating a parallel App. Subsequent drift checks (e.g., allowed-actions-api-drift-check) follow the same auth pattern.
If a future drift check requires permissions the App does not yet have, widen the App rather than splitting into another App — every App's private key is itself a secret with rotation cost. The trade-off: one App means one private-key blast radius.