How I learned to stop worrying and love the Permission denied (publickey) error
If you've ever stared at Permission denied (publickey) and wondered if you're the only one who finds SSH confusing — you're not, and this is for you.
This isn't just another "here's a script" tutorial. This is the war story, the mental models, the troubleshooting guide, and the production-ready setup I built after fighting with SSH for [however long you fought with it]. Everything you're about to read came from real failures, real confusion, and real 2 a.m. debugging sessions.
The Ultimate Goal
What we're building:
A system where your VPS can clone private GitHub repos without ever having your personal credentials, using deploy keys that are scoped to exactly one repository.
Why it matters:
- Your personal SSH keys stay on your laptop (where they belong)
- Each project gets its own isolated deploy key
- If one server is compromised, only that one repo is exposed
- You can revoke access without touching anything else
This is how professionals do it. And now you will too.
The Three Foundations You Need to Understand
Before we get to the script, let's build the mental model. SSH confusion usually comes from not understanding these three things:
1. Root vs User: Who Is Actually Trying to Connect?
When you run git clone as root, SSH looks for keys in /root/.ssh/.
When you run it as deployuser, SSH looks in /home/deployuser/.ssh/.
The failure I had:
I created keys as root, then tried to use them as a regular user. SSH couldn't find them because it was looking in a completely different directory.
The fix:
Always create and use SSH keys as the user who will actually run git clone.
2. Deploy Keys vs Personal Keys: Different Tools for Different Jobs
| Type | Purpose | Scope | Where It Lives |
|---|---|---|---|
| Personal SSH Key | Your identity across all of GitHub | All repos you have access to | Your laptop |
| Deploy Key | Server's identity for ONE repo | Single repository only | The VPS |
The failure I had:
I tried to use my personal key on the server. This works, but it's dangerous — if the server is compromised, the attacker gets access to everything you can access on GitHub.
The fix:
Generate a fresh SSH key on the server, add it as a deploy key to only the repo you need, and never let it leave that server.
3. SSH Config: The Map That Tells SSH Which Key to Use
SSH doesn't magically know which key to use for which host. You have to tell it.
The ~/.ssh/config file is like a phonebook:
Host github.com-myproject
HostName github.com
User git
IdentityFile ~/.ssh/deploy_key_myproject
IdentitiesOnly yes
This says:
"When you see github.com-myproject in a git URL, use the key at ~/.ssh/deploy_key_myproject"
Then you clone like this:
git clone git@github.com-myproject:yourusername/your-repo.git
Notice the difference:
- Normal:
git@github.com:yourusername/repo.git - With config:
git@github.com-myproject:yourusername/repo.git
That -myproject part is the "Host" name from your config. It's how SSH knows which key to grab.
The failure I had:
I created the config but forgot to use the custom host name in the clone URL. SSH fell back to looking for id_rsa, which didn't exist, and I got Permission denied.
The fix:
Always match your clone URL to the Host name in your SSH config.
The Problems I Actually Hit (And How I Fixed Them)
Problem 1: Permission denied (publickey)
Why it happens:
SSH can't find a valid key, or the key you have isn't authorized on GitHub.
How to debug it:
ssh -vvv git@github.com-myproject
Look for lines like:
debug1: Trying private key: /home/user/.ssh/deploy_key_myprojectdebug1: Authentication succeeded (publickey)
Common causes:
- You're using the wrong user (root vs deployuser)
- The deploy key isn't added to GitHub
- The SSH config
Hostname doesn't match your clone URL - File permissions are wrong (keys must be
600,.sshmust be700)
Problem 2: Keys Work, But Still Can't Clone
Why it happens:
The deploy key is added to GitHub, but with read-only access, and you're trying to push.
The fix:
When adding the deploy key on GitHub, check the box for "Allow write access" if you need to push from the server.
Problem 3: Multiple Repos, Keys Colliding
Why it happens:
You have two repos, both on GitHub, and SSH doesn't know which key to use.
The fix:
Create separate SSH config blocks for each repo:
Host github.com-project1
HostName github.com
User git
IdentityFile ~/.ssh/deploy_key_project1
IdentitiesOnly yes
Host github.com-project2
HostName github.com
User git
IdentityFile ~/.ssh/deploy_key_project2
IdentitiesOnly yes
Then clone with:
git clone git@github.com-project1:user/repo1.git
git clone git@github.com-project2:user/repo2.git
Problem 4: It Works Once, Then Breaks Later
Why it happens:
You ran the setup as root but your deploy script runs as a regular user, or vice versa.
The fix:
Consistency. Decide who owns the deployment process (usually a dedicated user like deployuser) and do everything as that user.
The Production-Ready Setup Script
This is what I wish I had on day one. Save this, use it on every new server.
#!/bin/bash
# SSH Deploy Key Setup Script
# Run this as the user who will deploy code (NOT as root)
set -e # Exit on any error
echo "=== SSH Deploy Key Setup ==="
echo ""
# 1. Get repo details
read -p "Enter your GitHub username: " GITHUB_USER
read -p "Enter the repository name: " REPO_NAME
read -p "Enter a name for this deploy key (e.g., myproject): " KEY_NAME
# 2. Set paths
SSH_DIR="$HOME/.ssh"
KEY_PATH="$SSH_DIR/deploy_key_$KEY_NAME"
CONFIG_PATH="$SSH_DIR/config"
# 3. Create .ssh directory if it doesn't exist
mkdir -p "$SSH_DIR"
chmod 700 "$SSH_DIR"
# 4. Generate SSH key
echo ""
echo "Generating SSH key..."
ssh-keygen -t ed25519 -C "deploy-$KEY_NAME@$(hostname)" -f "$KEY_PATH" -N ""
# 5. Set correct permissions
chmod 600 "$KEY_PATH"
chmod 644 "$KEY_PATH.pub"
# 6. Add to SSH config
echo ""
echo "Adding to SSH config..."
cat >> "$CONFIG_PATH" << EOF
# Deploy key for $REPO_NAME
Host github.com-$KEY_NAME
HostName github.com
User git
IdentityFile $KEY_PATH
IdentitiesOnly yes
EOF
chmod 600 "$CONFIG_PATH"
# 7. Display the public key
echo ""
echo "=========================================="
echo "PUBLIC KEY (add this to GitHub):"
echo "=========================================="
cat "$KEY_PATH.pub"
echo "=========================================="
echo ""
echo "Next steps:"
echo "1. Copy the public key above"
echo "2. Go to: https://github.com/$GITHUB_USER/$REPO_NAME/settings/keys"
echo "3. Click 'Add deploy key'"
echo "4. Paste the key and give it a title"
echo "5. Check 'Allow write access' if you need to push"
echo ""
echo "Then test with:"
echo " ssh -T git@github.com-$KEY_NAME"
echo ""
echo "And clone with:"
echo " git clone git@github.com-$KEY_NAME:$GITHUB_USER/$REPO_NAME.git"
echo ""
How to use it:
# Download and save as setup-deploy-key.sh
chmod +x setup-deploy-key.sh
# Run as the deploy user (NOT root)
./setup-deploy-key.sh
The 2 A.M. Debugging Checklist
When something breaks and you can't think straight, run through this:
□ Am I running as the correct user?
→ Check with: whoami
→ Keys should be in /home/USERNAME/.ssh, not /root/.ssh
□ Does the SSH key exist?
→ ls -la ~/.ssh/deploy_key_*
□ Are permissions correct?
→ chmod 700 ~/.ssh
→ chmod 600 ~/.ssh/deploy_key_*
→ chmod 600 ~/.ssh/config
□ Is the public key added to GitHub?
→ Check: github.com/USERNAME/REPO/settings/keys
□ Does the Host name in SSH config match my clone URL?
→ Config says: Host github.com-myproject
→ Clone URL must use: git@github.com-myproject:user/repo.git
□ Can SSH actually connect?
→ Test: ssh -vvv git@github.com-myproject
→ Look for "Authentication succeeded"
□ If pushing fails, did I check "Allow write access"?
→ GitHub → Repo → Settings → Deploy Keys → Edit
□ Are there other keys interfering?
→ Check: ssh-add -l
→ If needed, clear: ssh-add -D
→ Config should have: IdentitiesOnly yes
The Mental Model: Three Pieces, One System
This whole setup is just three pieces working together:
SSH Key Pair (private + public)
→ Lives in~/.ssh/deploy_key_projectname
→ Your "muscle memory" — create one per repoSSH Config (the map)
→ Lives in~/.ssh/config
→ Your "compass" — tells SSH which key to useGitHub Deploy Key (the authorization)
→ Lives on GitHub's website
→ Your "permission slip" — lets that key access the repo
When all three are aligned:
SSH key exists → Config points to it → GitHub recognizes it → Clone works
When something breaks:
One of these three is missing or misaligned. The checklist above helps you find which one.
What's Next
Now that you have the foundation, you can extend this to:
- CI/CD pipelines — Same concept, but keys live in GitHub Actions secrets
- Multiple environments — One deploy key per environment (staging, production)
- Automated deployments — Combine this with systemd, cron, or webhook listeners
- Full server bootstrap — Script that creates users, sets up SSH, installs Git, and configures everything
But first, test this setup. Break it on purpose. Re-run the script. Get comfortable with the debugging checklist.
You didn't mess anything up by hitting these errors — you just ran into real infrastructure reality earlier than most people. This is how engineers are made.
Quick Reference
Create a deploy key:
ssh-keygen -t ed25519 -C "deploy@server" -f ~/.ssh/deploy_key_project
Add to SSH config:
Host github.com-project
HostName github.com
User git
IdentityFile ~/.ssh/deploy_key_project
IdentitiesOnly yes
Test connection:
ssh -T git@github.com-project
Clone with deploy key:
git clone git@github.com-project:username/repo.git
Got questions? Hit me in the comments. Got corrections? Even better — I'm still learning this stuff too.
Top comments (0)