diff --git a/README.md b/README.md index 3f50fc0..960064a 100644 --- a/README.md +++ b/README.md @@ -250,6 +250,16 @@ Note: - E2E needs stable data: - use `npm run web:e2e` and `npm run test:e2e:full` +- **`ssh -T git@github.com` → `Permission denied (publickey)` on Windows:** + - see [`docs/SSH_TROUBLESHOOTING.md`](docs/SSH_TROUBLESHOOTING.md) for a step-by-step guide + - includes: diagnosing which key is offered (`ssh -vvv`), adding the public key to GitHub, + configuring `~/.ssh/config` with `IdentityFile`, handling multiple keys, + and HTTPS + PAT / Git Credential Manager fallback + - run the PowerShell helper script for an automated diagnosis: + ```powershell + .\scripts\check-ssh.ps1 + ``` + ## Notes For Contributors - Keep selectors and user-facing labels stable for E2E reliability. diff --git a/docs/SSH_TROUBLESHOOTING.md b/docs/SSH_TROUBLESHOOTING.md new file mode 100644 index 0000000..516c061 --- /dev/null +++ b/docs/SSH_TROUBLESHOOTING.md @@ -0,0 +1,339 @@ +# SSH Troubleshooting: `Permission denied (publickey)` on Windows + +This guide resolves the most common cause of: + +``` +$ ssh -T git@github.com +git@github.com: Permission denied (publickey). +``` + +on Windows when cloning or pushing to the `deilert00/Goald` repository. + +--- + +## Quick-start checklist + +1. [ ] Run `ssh -vvv git@github.com` and note **which key file** is being offered +2. [ ] Confirm the matching public key is uploaded to **your** GitHub account +3. [ ] If you have multiple keys, create or update `~/.ssh/config` with an explicit `IdentityFile` entry +4. [ ] If SSH cannot be made to work, use the **HTTPS + PAT fallback** described at the bottom + +--- + +## 1. Check which key is being offered + +Run the following in **Git Bash** or **PowerShell**: + +```powershell +ssh -vvv git@github.com 2>&1 | Select-String -Pattern "Offering|identity|Authentications" +``` + +Or in **Git Bash / WSL**: + +```bash +ssh -vvv git@github.com 2>&1 | grep -E "Offering|identity|Authentications" +``` + +Key lines to look for in the verbose output: + +``` +debug1: Trying private key: C:\Users\\.ssh\id_ed25519 +debug1: Offering public key: C:\Users\\.ssh\id_rsa +debug1: Authentications that can continue: publickey +``` + +- **`Offering public key`** — the key currently being tried +- **`Authentications that can continue: publickey`** after the offer means the server rejected it + +If no key is being offered at all, SSH cannot find any key — skip to [Step 3](#3-multiple-keys-and-ssh-config). + +--- + +## 2. Verify the public key is on the correct GitHub account + +### 2a. Find your public key fingerprint + +```powershell +# Git Bash / WSL +ssh-keygen -lf ~/.ssh/id_ed25519.pub + +# PowerShell (adjust filename if you use id_rsa or another key) +ssh-keygen -lf "$env:USERPROFILE\.ssh\id_ed25519.pub" +``` + +Expected output format: + +``` +256 SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx your@email.com (ED25519) +``` + +### 2b. Compare with keys on GitHub + +1. Go to +2. For each key listed, GitHub shows only the fingerprint (not the full public key) +3. Compare the `SHA256:…` portion with the fingerprint printed above + +If the fingerprint is **not** in the list, add the public key: + +```powershell +# Print the public key to paste into GitHub +Get-Content "$env:USERPROFILE\.ssh\id_ed25519.pub" + +# Or in Git Bash / WSL +cat ~/.ssh/id_ed25519.pub +``` + +Then: + +1. Go to → **New SSH key** +2. Set **Title** to something recognisable (e.g. `Work laptop – Windows`) +3. Paste the entire `ssh-ed25519 AAAA… your@email.com` string into **Key** +4. Click **Add SSH key** + +Re-test: + +```bash +ssh -T git@github.com +# Expected: Hi ! You've successfully authenticated… +``` + +--- + +## 3. Multiple keys and `~/.ssh/config` + +When you have more than one key pair (personal, work, client, etc.) SSH may offer the wrong one. +The fix is a `Host` block in `~/.ssh/config` that pins the identity file. + +### 3a. Open (or create) the config file + +```powershell +# PowerShell +notepad "$env:USERPROFILE\.ssh\config" +``` + +```bash +# Git Bash / WSL +nano ~/.ssh/config +``` + +If the `.ssh` directory does not exist yet, create it first: + +```powershell +New-Item -ItemType Directory -Force "$env:USERPROFILE\.ssh" +``` + +### 3b. Add a `Host github.com` block + +``` +Host github.com + HostName github.com + User git + IdentityFile ~/.ssh/id_ed25519 + IdentitiesOnly yes +``` + +> **`IdentitiesOnly yes`** is important — it prevents SSH from trying keys from an SSH agent +> that belong to a different account. + +Replace `id_ed25519` with the actual filename of your private key if it differs +(e.g. `id_rsa`, `id_ed25519_work`, etc.). + +### 3c. Multiple GitHub accounts (personal + work) + +If you need to switch between two GitHub accounts, use separate hostnames: + +``` +# Personal account +Host github.com + HostName github.com + User git + IdentityFile ~/.ssh/id_ed25519_personal + IdentitiesOnly yes + +# Work account — use as: git@github-work:org/repo.git +Host github-work + HostName github.com + User git + IdentityFile ~/.ssh/id_ed25519_work + IdentitiesOnly yes +``` + +Clone with the work alias: + +```bash +git clone git@github-work:deilert00/Goald.git +``` + +Update an existing remote: + +```bash +git remote set-url origin git@github-work:deilert00/Goald.git +``` + +--- + +## 4. Reading `ssh -vvv` output + +Below is a **minimal annotated example** of a successful and a failing handshake. + +### Successful authentication + +``` +debug1: Connecting to github.com [140.82.121.4] port 22. +debug1: Connection established. +debug1: Offering public key: /c/Users/you/.ssh/id_ed25519 ED25519 SHA256:xxx +debug1: Server accepts key: /c/Users/you/.ssh/id_ed25519 ED25519 SHA256:xxx ← key accepted +debug1: Authentication succeeded (publickey). +``` + +### Failing authentication (wrong key offered) + +``` +debug1: Offering public key: /c/Users/you/.ssh/id_rsa RSA SHA256:yyy +debug1: Authentications that can continue: publickey ← key rejected +debug1: No more authentication methods to try. +git@github.com: Permission denied (publickey). +``` + +**What to do:** +- Look at the `Offering public key:` filename — add that public key to GitHub, **or** +- Add an `IdentityFile` line in `~/.ssh/config` pointing at the correct key + +### Failing authentication (no key found) + +``` +debug1: No more authentication methods to try. +git@github.com: Permission denied (publickey). +``` + +Without any `Offering` line, SSH found no key to try. + +**What to do:** +- Generate a key: `ssh-keygen -t ed25519 -C "your@email.com"` +- Add it to GitHub (see [Step 2](#2-verify-the-public-key-is-on-the-correct-github-account)) + +### Agent not running (Windows) + +``` +debug1: No identities. +``` + +OpenSSH agent may not be running. Start it once, or start it automatically: + +```powershell +# Start agent for this session +Start-Service ssh-agent + +# Or start on boot (run as Administrator) +Set-Service -Name ssh-agent -StartupType Automatic +Start-Service ssh-agent + +# Add your key +ssh-add "$env:USERPROFILE\.ssh\id_ed25519" +``` + +--- + +## 5. HTTPS + Personal Access Token (PAT) fallback + +If SSH cannot be resolved, use HTTPS with a PAT instead. + +### 5a. Generate a PAT + +1. Go to +2. Set an **Expiration** (e.g. 90 days) +3. Select scope: **`repo`** (full control of private repositories) +4. Click **Generate token** and copy the value — you will not see it again + +### 5b. Configure the remote to use HTTPS + +```bash +# Check current remote URL +git remote -v + +# Switch from SSH to HTTPS +git remote set-url origin https://github.com/deilert00/Goald.git +``` + +### 5c. Store credentials so you are not prompted every push + +**Option A — Git Credential Manager (recommended, see Section 6)** + +**Option B — credential in URL (less secure, fine for CI):** + +```bash +git remote set-url origin https://:@github.com/deilert00/Goald.git +``` + +> ⚠️ Never commit a URL that contains a PAT to a public repository. + +**Option C — store in Git credential helper:** + +```bash +git config --global credential.helper store +# Then perform any authenticated operation; Git will prompt once and store the token +git fetch origin +``` + +Credentials are saved in `~/.git-credentials` in plain text. Use the credential manager for +better security. + +--- + +## 6. Git Credential Manager (GCM) + +Git for Windows ships with **Git Credential Manager** (GCM). It stores tokens in the Windows +Credential Store (encrypted) and supports OAuth browser flows. + +### Check if GCM is active + +```bash +git config --global credential.helper +# Expected output: manager (or manager-core on older installations) +``` + +### Configure GCM + +```bash +git config --global credential.helper manager +``` + +### Authenticate + +Simply run any `git` command that needs credentials (clone, fetch, push). GCM will open a browser +window and ask you to authorise the GitHub app. After that, credentials are cached automatically. + +### Clear cached credentials (force re-authentication) + +```powershell +# Remove stored GitHub credential from the Windows Credential Store +cmdkey /delete:LegacyGeneric:target=git:https://github.com +``` + +Or via **Credential Manager** in Control Panel → Windows Credentials → remove any `git:https://github.com` entry. + +--- + +## 7. Helper script: `scripts/check-ssh.ps1` + +A convenience PowerShell script is included at `scripts/check-ssh.ps1`. +Run it in PowerShell (no admin required): + +```powershell +.\scripts\check-ssh.ps1 +``` + +The script: +1. Lists all public keys in `~/.ssh/` +2. Prints the fingerprint of each key +3. Tests the connection with `ssh -T git@github.com` using any `Host github.com` entry in your config +4. Prints a summary and next-step hints + +--- + +## See also + +- [GitHub Docs – Generating a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/generating-a-new-ssh-key-and-adding-it-to-the-ssh-agent) +- [GitHub Docs – Adding a new SSH key](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/adding-a-new-ssh-key-to-your-github-account) +- [GitHub Docs – Testing your SSH connection](https://docs.github.com/en/authentication/connecting-to-github-with-ssh/testing-your-ssh-connection) +- [Git Credential Manager](https://github.com/git-ecosystem/git-credential-manager) diff --git a/scripts/check-ssh.ps1 b/scripts/check-ssh.ps1 new file mode 100644 index 0000000..efbcae1 --- /dev/null +++ b/scripts/check-ssh.ps1 @@ -0,0 +1,233 @@ +<# +.SYNOPSIS + Diagnose common SSH authentication issues with github.com on Windows. + +.DESCRIPTION + This script helps resolve "Permission denied (publickey)" errors when + running `ssh -T git@github.com`. It: + 1. Lists public keys found in ~/.ssh/ + 2. Prints the SHA-256 fingerprint of each key + 3. Checks whether an ssh-agent is running and which keys it holds + 4. Verifies that ~/.ssh/config contains a Host github.com block + 5. Runs `ssh -T git@github.com` and reports the result + 6. Prints next-step suggestions based on the findings + +.NOTES + Run from any PowerShell prompt — administrator rights are NOT required. + For full verbose SSH output run: ssh -vvv git@github.com +#> + +Set-StrictMode -Version Latest +$ErrorActionPreference = 'Continue' + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +function Write-Header([string]$text) { + Write-Host "" + Write-Host "=== $text ===" -ForegroundColor Cyan +} + +function Write-Ok([string]$text) { Write-Host " [OK] $text" -ForegroundColor Green } +function Write-Warn([string]$text) { Write-Host " [!!] $text" -ForegroundColor Yellow } +function Write-Info([string]$text) { Write-Host " [--] $text" -ForegroundColor Gray } +function Write-Fail([string]$text) { Write-Host " [XX] $text" -ForegroundColor Red } + +# --------------------------------------------------------------------------- +# 1. Locate public keys +# --------------------------------------------------------------------------- + +Write-Header "Public keys in ~/.ssh/" + +$sshDir = Join-Path $env:USERPROFILE ".ssh" + +if (-not (Test-Path $sshDir)) { + Write-Fail "Directory $sshDir does not exist." + Write-Warn "Run: ssh-keygen -t ed25519 -C `"your@email.com`" to generate a key." + exit 1 +} + +$pubKeys = Get-ChildItem -Path $sshDir -Filter "*.pub" -File -ErrorAction SilentlyContinue | + Where-Object { $_.Name -notlike "known_hosts*" } + +if ($pubKeys.Count -eq 0) { + Write-Fail "No public key files (*.pub) found in $sshDir" + Write-Warn "Generate one with: ssh-keygen -t ed25519 -C `"your@email.com`"" + exit 1 +} + +$fingerprintMap = @{} +foreach ($key in $pubKeys) { + Write-Info "Found: $($key.Name)" + try { + $fp = & ssh-keygen -lf $key.FullName 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Ok " Fingerprint: $fp" + $fingerprintMap[$key.FullName] = $fp + } + else { + Write-Warn " Could not read fingerprint for $($key.Name): $fp" + } + } + catch { + Write-Warn " ssh-keygen not available or failed: $_" + } +} + +Write-Info "" +Write-Info "Compare these fingerprints against https://github.com/settings/keys" +Write-Info "The fingerprint you see there must match one of the values above." + +# --------------------------------------------------------------------------- +# 2. SSH agent status +# --------------------------------------------------------------------------- + +Write-Header "SSH agent" + +$agentService = Get-Service -Name ssh-agent -ErrorAction SilentlyContinue +if ($null -eq $agentService) { + Write-Warn "ssh-agent service not found. OpenSSH may not be installed." + Write-Info "Install via: Settings > Apps > Optional features > OpenSSH Client" +} +elseif ($agentService.Status -ne 'Running') { + Write-Warn "ssh-agent service is not running (Status: $($agentService.Status))" + Write-Info "Start it with: Start-Service ssh-agent" + Write-Info "Auto-start: Set-Service -Name ssh-agent -StartupType Automatic" +} +else { + Write-Ok "ssh-agent is running." + + # List keys loaded in the agent + $agentKeys = & ssh-add -l 2>&1 + if ($LASTEXITCODE -eq 0) { + Write-Ok "Keys loaded in agent:" + $agentKeys | ForEach-Object { Write-Info " $_" } + } + elseif ($agentKeys -match "no identities") { + Write-Warn "No keys are loaded in ssh-agent." + Write-Info "Add your key with: ssh-add `"$sshDir\id_ed25519`"" + } + else { + Write-Warn "Could not list agent keys: $agentKeys" + } +} + +# --------------------------------------------------------------------------- +# 3. Check ~/.ssh/config for a github.com Host block +# --------------------------------------------------------------------------- + +Write-Header "~/.ssh/config" + +$configPath = Join-Path $sshDir "config" + +if (-not (Test-Path $configPath)) { + Write-Warn "No ~/.ssh/config file found." + Write-Info "Create one at: $configPath" + Write-Info "Recommended content:" + Write-Info "" + Write-Info " Host github.com" + Write-Info " HostName github.com" + Write-Info " User git" + Write-Info " IdentityFile ~/.ssh/id_ed25519" + Write-Info " IdentitiesOnly yes" +} +else { + $configContent = Get-Content $configPath -Raw + if ($configContent -match "(?mi)^\s*Host\s+github\.com") { + Write-Ok "Found a 'Host github.com' block in $configPath" + + # Extract and display the IdentityFile value if present + if ($configContent -match "(?mi)IdentityFile\s+(.+)") { + $identityFile = $Matches[1].Trim() + Write-Info "IdentityFile configured: $identityFile" + + # Expand ~ to $HOME + $expanded = $identityFile -replace "^~", $env:USERPROFILE + if (Test-Path $expanded) { + Write-Ok "Key file exists: $expanded" + } + else { + Write-Fail "Key file does NOT exist: $expanded" + Write-Warn "Generate it with: ssh-keygen -t ed25519 -f `"$expanded`" -C `"your@email.com`"" + } + } + else { + Write-Warn "No IdentityFile line found in the github.com Host block." + Write-Info "Add one to pin which key is used, e.g.:" + Write-Info " IdentityFile ~/.ssh/id_ed25519" + } + + if ($configContent -notmatch "(?mi)IdentitiesOnly\s+yes") { + Write-Warn "IdentitiesOnly yes is missing — SSH may try keys from the agent for other accounts." + Write-Info "Add 'IdentitiesOnly yes' under Host github.com to prevent this." + } + } + else { + Write-Warn "No 'Host github.com' block found in $configPath" + Write-Info "Add the following block to $configPath :" + Write-Info "" + Write-Info " Host github.com" + Write-Info " HostName github.com" + Write-Info " User git" + Write-Info " IdentityFile ~/.ssh/id_ed25519" + Write-Info " IdentitiesOnly yes" + } +} + +# --------------------------------------------------------------------------- +# 4. Test the SSH connection +# --------------------------------------------------------------------------- + +Write-Header "SSH connection test (ssh -T git@github.com)" + +Write-Info "Running ssh -T git@github.com (may take a few seconds) ..." + +$sshOutput = & ssh -T git@github.com 2>&1 +$sshExitCode = $LASTEXITCODE + +# GitHub returns exit code 1 even on a *successful* authentication, but it +# prints the "Hi !" message in that case. +$successPattern = "Hi .+! You've successfully authenticated" + +if ($sshOutput -match $successPattern) { + Write-Ok "Authentication SUCCEEDED." + Write-Ok "$sshOutput" +} +elseif ($sshOutput -match "Permission denied") { + Write-Fail "Authentication FAILED: $sshOutput" + Write-Info "" + Write-Info "Next steps:" + Write-Info " 1. Compare the fingerprints printed above with https://github.com/settings/keys" + Write-Info " 2. If the key is missing, add it: https://github.com/settings/keys > New SSH key" + Write-Info " 3. Add an IdentityFile entry to ~/.ssh/config (see above)" + Write-Info " 4. For detailed diagnostics run: ssh -vvv git@github.com" + Write-Info " 5. As a fallback, switch to HTTPS + PAT:" + Write-Info " git remote set-url origin https://github.com/deilert00/Goald.git" + Write-Info " Then configure Git Credential Manager:" + Write-Info " git config --global credential.helper manager" +} +else { + Write-Warn "Unexpected output (exit code $sshExitCode): $sshOutput" + Write-Info "Run 'ssh -vvv git@github.com' for detailed diagnostics." +} + +# --------------------------------------------------------------------------- +# 5. HTTPS / PAT fallback reminder +# --------------------------------------------------------------------------- + +Write-Header "HTTPS + PAT fallback" +Write-Info "If SSH cannot be resolved, switch to HTTPS:" +Write-Info " git remote set-url origin https://github.com/deilert00/Goald.git" +Write-Info "" +Write-Info "Generate a PAT at: https://github.com/settings/tokens/new" +Write-Info " - Expiration: 90 days" +Write-Info " - Scope: repo" +Write-Info "" +Write-Info "Use Git Credential Manager (ships with Git for Windows) to store the token:" +Write-Info " git config --global credential.helper manager" +Write-Info "" +Write-Info "Full troubleshooting guide: docs/SSH_TROUBLESHOOTING.md" +Write-Host "" + +exit 0