Chapter 02Lesson 05~150 minutes

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.

CheckpointCredential matrixLeast privilegeCleanup proof

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.
Availability: Required: personal GitHub.com account, Git, OpenSSH, and GitHub CLI. A private disposable repository is supported by the free path and is used only so a fine-grained PAT permission denial is observable. Enterprise SSO is a design-only optional extension.

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.

Secrets never enter the report. Capture names, fingerprints, status codes, permission names, repository coordinates, timestamps, and verification results. Do not capture tokens, private keys, recovery codes, cookies, or screenshots that expose them.

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.

Destructive cleanup is mandatory. Repository deletion happens only in section 13 after you preserve the non-secret evidence you need.

5. Web authentication proof without factor mutation

  1. Open Settings → Password and authentication. Confirm 2FA is enabled if your account requires/uses it and identify configured method categories.
  2. Confirm you have recovery material/multiple methods available. Do not display or regenerate recovery codes.
  3. Record: “interactive account protection reviewed at TIMESTAMP; recovery material exists; no factor changed.”
  4. 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.

Independent verification: SSH and HTTPS can authenticate with different credentials yet read the same repository/ref. Matching the advertised 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:

  1. Create a token named github-auth-checkpoint-deny with a short expiration.
  2. Choose your personal account as Resource owner.
  3. Choose Only select repositories and select only the private checkpoint repository.
  4. Grant Issues: read-only but do not grant Contents. Metadata read access is included as required by GitHub.
  5. 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.

Security-sensitive: this token is real, even though short-lived. Keep the value only in the local terminal process/secure prompt and revoke it in section 11.

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

  1. Delete github-auth-checkpoint-temp from Settings → SSH and GPG keys.
  2. Repeat the exact-key ssh -i PATH_TO_KEY -o IdentitiesOnly=yes -T git@github.com test. The lab key should no longer authenticate.
  3. Delete only github_auth_checkpoint_ed25519 and its .pub file locally. Do not delete unrelated keys.
  4. If you loaded the lab key into an agent, remove that specific identity.
Cleanup proof: the server-side key registration is gone, the exact key no longer resolves to your account, and the local private/public lab files are removed.

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.

Destructive action: repository deletion is irreversible after any applicable restoration window/policy and should only target the disposable checkpoint repository. Verify the exact 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

Authentication matrix and authorization gates
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?

The PAT denial changes to 200 after adding Contents: read. What did that prove?

Why compare SSH and HTTPS ls-remote object IDs?

After deleting a lab SSH key on GitHub, why test it before deleting the private key file?

An enterprise learner sees an SSO denial during the same lab. Should they broaden PAT permissions?

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.

Next chapter

Create repositories with secure ownership and defaults

Chapter 03 moves from credentials to repository architecture: creation, templates, settings, visibility, topics, default branches, and the governance implications of each initial choice.

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.

Ethereum / ERC-20
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0 Send only Ethereum/ERC-20 compatible assets to this address.