Checkpoint Lab — Account Security, Two-Factor Authentication, SSH Keys, Tokens, and Credential Hygiene
Prove a complete GitHub authentication matrix with disposable credentials, an intentionally under-privileged API request, least-privilege correction, independent verification, and full credential cleanup.
Learning objectives
- Build and document an authentication matrix for browser, Git over SSH, Git over HTTPS, and gh/API access.
- Use a disposable private repository only where private-resource authorization is necessary to demonstrate least privilege; keep the entire lab GitHub Free-compatible.
- Predict hosted/local/credential changes before creating a repository, SSH key, or temporary PAT and verify each prediction independently.
- Trigger one intentionally under-privileged fine-grained PAT request against a private resource, diagnose the denial, and add only the required read permission in a replacement token.
- Prove that temporary SSH/PAT credentials have been revoked and remove the disposable repository after preserving non-secret evidence.
- Produce a concise operating runbook that separates authentication, authorization, policy, and recovery.
1. Checkpoint goal: prove four authentication channels without leaking one secret
This checkpoint is evidence-driven. You will prove: (1) the human account has a viable 2FA/recovery posture, (2) a disposable SSH key resolves to the intended GitHub account, (3) HTTPS Git can access the private lab repository through an approved credential helper, and (4) gh/API access resolves to the expected account. Then you will create one short-lived, repository-specific fine-grained PAT solely to demonstrate an authorization denial and least-privilege correction.
2. Preflight and assumptions
git --version
ssh -V
gh --version
gh auth status --active --hostname github.com
git config --show-origin --get-all credential.helper
- You control the personal GitHub account used for the lab and may create/delete a disposable private repository.
- You are not using an employer-managed account where personal test credentials are prohibited.
- 2FA changes are not part of the lab; you inspect current protection/recovery state only.
- Any existing SSH keys, credential helpers, or gh accounts remain untouched unless a step explicitly targets the lab credential.
-
The REST examples use
X-GitHub-Api-Version: 2026-03-10. - If your organization/account policy blocks PATs, skip the live PAT section and use the provided synthetic denial fixture; the learning objective is authorization reasoning, not policy bypass.
3. Create the authentication matrix before touching state
| Channel | Principal | Credential/control | Expected proof | Cleanup |
|---|---|---|---|---|
| Web | Your GitHub user | Existing 2FA/passkey/recovery configuration | Password & authentication settings show protected state; no codes revealed. | None—do not disable factors. |
| Git over SSH | Your GitHub user | Temporary lab SSH key |
GitHub SSH greeting names expected user; private repo
ls-remote works.
|
Delete key registration, verify failure, delete local files. |
| Git over HTTPS | Your GitHub user | GCM/gh-managed credential |
Private repo git ls-remote succeeds without
token in URL.
|
No lab secret if using existing managed auth; delete repo later. |
| gh/API | Your GitHub user | gh browser/device auth | gh api /user returns expected login. |
Keep normal gh login unless created solely for lab. |
| Fine-grained API exercise | Your GitHub user | Temporary repo-specific PAT | First private contents request denied; corrected read permission succeeds. | Delete temporary PAT(s) and verify they no longer authenticate. |
4. Create a private disposable repository for authorization testing
The repository is private so a token without the required repository permission cannot fall back to unauthenticated public access. Use a unique name and initialize a README.
Git Bash, Bash, or zsh
OWNER=$(gh api /user --jq .login)
REPO="github-auth-checkpoint-$(date +%Y%m%d%H%M%S)"
gh repo create "$OWNER/$REPO" --private --add-readme
gh repo view "$OWNER/$REPO" --json nameWithOwner,visibility,defaultBranchRef --jq '{nameWithOwner,visibility,defaultBranch: .defaultBranchRef.name}'
PowerShell
$OWNER = gh api /user --jq .login
$REPO = "github-auth-checkpoint-$(Get-Date -Format yyyyMMddHHmmss)"
gh repo create "$OWNER/$REPO" --private --add-readme
gh repo view "$OWNER/$REPO" --json nameWithOwner,visibility,defaultBranchRef --jq '{nameWithOwner,visibility,defaultBranch: .defaultBranchRef.name}'
Prediction 1: creating the repository creates
hosted repository/ref state, but it does not create an SSH key,
change your 2FA setup, or change your local Git repositories. Verify
with the structured gh repo view output before
continuing.
5. Web authentication proof without factor mutation
- Open Settings → Password and authentication. Confirm 2FA is enabled if your account requires/uses it and identify configured method categories.
- Confirm you have recovery material/multiple methods available. Do not display or regenerate recovery codes.
- Record: “interactive account protection reviewed at TIMESTAMP; recovery material exists; no factor changed.”
- Open the private repository in the browser while signed in. This proves the browser session is authorized for the repository, but it does not prove SSH/PAT access.
6. Prediction 2 and SSH proof: add one exact disposable key
Prediction 2: after registering the lab public key, SSH authentication with the matching private key will resolve to your GitHub username; repository refs will remain unchanged.
Create and fingerprint
mkdir -p "$HOME/.ssh"
ssh-keygen -t ed25519 -C "github-auth-checkpoint" -f "$HOME/.ssh/github_auth_checkpoint_ed25519"
ssh-keygen -lf "$HOME/.ssh/github_auth_checkpoint_ed25519.pub"
cat "$HOME/.ssh/github_auth_checkpoint_ed25519.pub"
In Settings → SSH and GPG keys, add the public key
as an authentication key named
github-auth-checkpoint-temp. Match GitHub’s displayed
fingerprint to ssh-keygen -lf.
Verify identity and private repository transport
ssh -i "$HOME/.ssh/github_auth_checkpoint_ed25519" -o IdentitiesOnly=yes -T git@github.com
GIT_SSH_COMMAND='ssh -i "$HOME/.ssh/github_auth_checkpoint_ed25519" -o IdentitiesOnly=yes' git ls-remote "git@github.com:$OWNER/$REPO.git" HEAD
The first command should greet the intended username and then exit 1
by design. The second should return the private repository’s
HEAD object ID and should exit successfully. For
additional evidence, compare that object ID with the repository
default branch OID obtained through a separate read-only
gh repo view / gh api inspection; no push
is needed.
7. HTTPS proof: use an approved credential helper, never a tokenized URL
First inspect the current helper:
git config --show-origin --get-all credential.helper
If GitHub CLI/GCM is already configured for HTTPS, run the private-repository read-only test:
git ls-remote "https://github.com/$OWNER/$REPO.git" HEAD
If Git prompts for your GitHub account password, stop. Do not type
the account password and do not paste a PAT into the URL. Configure
GitHub CLI/GCM according to the official credential-caching
guidance, then rerun the test. The expected output is the same
HEAD object ID observed through SSH.
HEAD OID proves
you are observing the same Git state through two transports.
8. gh/API proof: verify caller and repository without exposing the gh token
gh auth status --active --hostname github.com
gh api /user --jq '{login: .login, id: .id}'
gh api "/repos/$OWNER/$REPO" --jq '{full_name, private, default_branch}'
Do not use gh auth status --show-token or
gh auth token. The API identity query proves the
principal; the repository query proves that principal is currently
authorized to see the private lab resource.
For raw REST clients, the equivalent request would carry an
authorization bearer token plus
Accept: application/vnd.github+json and
X-GitHub-Api-Version: 2026-03-10. The lab intentionally
delegates bearer-token storage to gh until the
fine-grained-token exercise.
9. Create an intentionally under-privileged fine-grained PAT
This is the only point where the lab requires a temporary bearer token. In GitHub Settings → Developer settings → Personal access tokens → Fine-grained tokens:
-
Create a token named
github-auth-checkpoint-denywith a short expiration. - Choose your personal account as Resource owner.
- Choose Only select repositories and select only the private checkpoint repository.
- Grant Issues: read-only but do not grant Contents. Metadata read access is included as required by GitHub.
- Generate the token. Do not screenshot or paste the value into notes/source/history.
The target request is
GET /repos/{owner}/{repo}/contents/README.md. GitHub
documents that private repository content requires
Contents: read for a fine-grained token. Therefore
the deliberately under-privileged token should receive an
authorization denial such as 403/404 rather than the README content.
10. Inject the token without putting it in shell history
Git Bash, Bash, or zsh — hidden prompt
read -rsp "Temporary fine-grained token: " GH_LAB_TOKEN
echo
export GH_LAB_TOKEN
status=$(curl -sS -o response.json -D response.headers -w '%{http_code}' -H "Accept: application/vnd.github+json" -H "Authorization: Bearer $GH_LAB_TOKEN" -H "X-GitHub-Api-Version: 2026-03-10" "https://api.github.com/repos/$OWNER/$REPO/contents/README.md")
printf 'HTTP %s
' "$status"
grep -i '^x-accepted-github-permissions:' response.headers || true
Do not print $GH_LAB_TOKEN. The saved response contains
only the denial body/headers, not the request authorization header.
Expect an authorization denial and, where provided, an
X-Accepted-GitHub-Permissions header indicating the
endpoint’s required permission.
PowerShell — avoid a token literal in command history
Use a secure prompt or your approved password/secret manager to
place the temporary value into $env:GH_LAB_TOKEN for
the current process, then call Invoke-WebRequest/Invoke-RestMethod
with an Authorization header. Do not type the token as
part of the command line, and remove the environment variable after
the request. If your terminal tooling cannot do this safely, use Git
Bash for this one exercise or use the synthetic fixture from Lesson
04.
Interpretation: a 403/404 here is the intended result. The token can represent the correct user and repository selection yet still lack the endpoint permission. This is authorization working as designed.
11. Correct only the missing permission, verify success, then revoke
To avoid relying on undocumented token-edit behavior, delete the
under-privileged token and create a replacement named
github-auth-checkpoint-read with the same short
expiration, same resource owner, and same single repository—but
grant exactly Contents: read-only (no write/admin
permission).
Load the replacement token through the same hidden/secure mechanism
and repeat the identical GET request. Expected result:
200 OK and README metadata/content. The request changed
only one authorization dimension: Contents read permission.
Now immediately delete the replacement PAT in GitHub settings and clear the process variable:
unset GH_LAB_TOKEN
rm -f response.json response.headers
Verify revocation by attempting the same request only if you can do so without retaining the token value beyond the local process. Otherwise GitHub’s token list showing the token absent plus your deletion timestamp is acceptable cleanup evidence. Never preserve the token “just to prove it fails later.”
12. Remove the SSH lab credential and verify retirement
-
Delete
github-auth-checkpoint-tempfrom Settings → SSH and GPG keys. -
Repeat the exact-key
ssh -i PATH_TO_KEY -o IdentitiesOnly=yes -T git@github.comtest. The lab key should no longer authenticate. -
Delete only
github_auth_checkpoint_ed25519and its.pubfile locally. Do not delete unrelated keys. - If you loaded the lab key into an agent, remove that specific identity.
13. Preserve evidence, then delete the private disposable repository
Before deletion, preserve only non-secret evidence: repository name, visibility, default branch, observed HEAD OID, SSH/HTTPS success, gh/API principal, under-privileged status/required permission, corrected 200 result, PAT deletion timestamps, and SSH-key fingerprint/removal result.
Then delete the repository through Settings → General → Danger Zone → Delete this repository, or with an explicitly targeted CLI command if you understand the consequence. Browser deletion is preferred for this beginner checkpoint because GitHub presents the destructive confirmation context.
OWNER/REPO before confirming.
Finally run gh repo view "$OWNER/$REPO" and expect a
not-found/access failure. That verifies hosted cleanup; it does not
imply your local shell variables were cleared, so unset/remove any
temporary variables as well.
14. Final platform-boundary diagram
flowchart TD
H[Human account] --> W[Browser: 2FA / passkey]
H --> S[SSH key on device]
H --> C[gh OAuth credential]
H --> P[Temporary fine-grained PAT]
W --> R[(Private lab repository)]
S --> A1{repo permission}
C --> A2{repo + API permission}
P --> A3{repo selection + Contents read}
A1 --> R
A2 --> R
A3 --> R
X[Org / SSO / enterprise policy when applicable] -. constrains .-> A1
X -. constrains .-> A2
X -. constrains .-> A3
Browser authentication, SSH, gh, and PATs are distinct credential paths. They converge only at authorization: the target repository, user role, credential permission/resource selection, and applicable organization/enterprise policy. That is the operating model you should carry into pull requests, rulesets, Actions, packages, and security controls.
15. What this adds to a production GitHub operating model
Chapter 01 established ownership and platform boundaries. Chapter 02 adds an identity-control plane: strong recoverable human authentication, explicit transport credentials, managed CLI/HTTPS storage, least-privilege API credentials, machine identities for automation, and incident-ready revocation.
A production team can now ask concrete questions: Which principal performed this action? Which credential represented it? What resource/permission allowed it? Which policy narrowed it? How quickly can we revoke it? Can we prove cleanup without preserving the secret? Those questions turn credential hygiene from personal preference into an auditable DevOps control.
16. Final verification checklist
- 2FA/recovery posture was reviewed without disabling factors or exposing recovery codes.
- The disposable private repository was created only for authorization testing and was deleted after evidence capture.
- The SSH lab key used a unique filename, matching public fingerprints, exact-key identity test, and verified server-side removal.
- HTTPS Git accessed the private repository without a token embedded in the URL.
- gh/API identity resolved to the expected GitHub account without printing the gh token.
- The first fine-grained PAT selected only the lab repository and intentionally lacked Contents permission.
- The denial was diagnosed from resource/permission evidence; the replacement added only Contents: read.
- Both temporary PATs were deleted; the token value was never stored in source, notes, URLs, screenshots, or command history.
- No enterprise/paid control was required; SSO remained optional/design-only.
- Your final report contains evidence and lifecycle metadata—not secret values.
Knowledge check
Why did the checkpoint use a private repository for the under-privileged PAT test?
Public repository content may be readable without the token’s private-resource permission, which can mask the authorization boundary. A private disposable repo makes the required Contents permission observable while remaining free-compatible.
The PAT denial changes to 200 after adding Contents: read. What did that prove?
The authenticated user/repository selection were already viable; the missing endpoint permission was the causal authorization gate. No write/admin scope was needed.
Why compare SSH and HTTPS ls-remote object
IDs?
They use different transport credentials but should observe the same hosted Git ref. Matching OIDs independently verifies both channels reached the same repository state.
After deleting a lab SSH key on GitHub, why test it before deleting the private key file?
The failed exact-key test proves server-side revocation. Deleting the local file alone would not prove GitHub stopped accepting the credential.
An enterprise learner sees an SSO denial during the same lab. Should they broaden PAT permissions?
No. SSO is a separate organization authorization/policy gate. Establish/authorize the required SSO context according to enterprise policy; token scope inflation does not solve that root cause.
17. Chapter 02 completion summary
You have operated the full authentication chain safely: interactive account protection, SSH identity, HTTPS credential management, GitHub CLI/API identity, least-privilege PAT authorization, denial diagnosis, correction, revocation, and cleanup. The chapter’s central rule is now observable rather than theoretical: authentication proves a principal; authorization and policy decide what that principal may do.
The next chapter applies these controls to repository creation. Ownership, visibility, initialization, default branches, templates, and repository settings are not only convenience choices—they decide where these identities and permissions will later operate.
Authoritative references
Configuring two-factor authentication
Configuring 2FA recovery methods
Recovering an account after losing 2FA credentials
Generating a new SSH key and adding it to the ssh-agent
Adding a new SSH key to your GitHub account
Testing your SSH connection
Caching GitHub credentials in Git
gh auth login
gh auth status
Managing personal access tokens
Permissions required for fine-grained PATs
Authenticating to the REST API
REST API versions
About authentication with single sign-on
Keep the academy open
Support free, practical DevOps education.
Every lesson is designed to remain readable in a browser, downloadable from GitHub, and usable without a paid learning platform. Contributions help expand and maintain the curriculum.
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0
Send only Ethereum/ERC-20 compatible assets to this
address.