1. Home
  2. Blog
  3. DevOps

DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys let you log in to servers and push to Git without typing a password, and they are far harder to steal or guess. We generated throwaway keys to show what each file contains, what is safe to share and what must never leave your machine.

10 min read
SSH keys explained: how they work, ssh-keygen, Ed25519 and safe habits

Key takeaways

  • An SSH key pair has a public key you can share and a private key you never share.
  • Create one with ssh-keygen -t ed25519 -C "you@example.com". Ed25519 is the modern default.
  • Protect the private key with a passphrase and use ssh-agent so you only type it once.
  • Use separate keys per purpose, remove old keys from servers, and never paste a private key anywhere.

Passwords for servers get reused, guessed, phished and shared in chat messages. SSH keys replace them with a pair of mathematically linked files: one you can hand to anyone, and one that stays on your machine. Even if a server is compromised, attackers only learn your public key, which is useless to them.

We generated throwaway keys with OpenSSH 10.6 in Git Bash on Windows and inspected them. The outputs below come from that run; the keys were deleted afterwards. We did not log in to a real server for this article, so the server-side commands are standard examples.

What are SSH keys?

SSH, short for Secure Shell, is the protocol used to log in to remote machines and to talk to Git hosts such as GitHub and GitLab. SSH keys use public-key cryptography: a private key, kept secret, and a public key, derived from it and safe to share. Anything signed by the private key can be verified with the public key, but the private key cannot be worked out from the public one.

How key login works

  1. You put your public key in the server’s list of allowed keys, ~/.ssh/authorized_keys.
  2. When you connect, the server sends a challenge.
  3. Your SSH client signs the challenge with your private key.
  4. The server checks the signature with the public key it already has. If it matches, you are in.

Your private key never travels over the network. That is the core security advantage over passwords, which must be sent to the server every time.

Generating a key

ssh-keygen -t ed25519 -C "demo@example.com"

-t ed25519 chooses the key type; -C adds a comment, usually an email, to help you recognise the key later. Without -C, the comment defaults to your user and computer name, which you may not want to share with every server and service. ssh-keygen asks where to save the key, defaulting to ~/.ssh/id_ed25519, and for a passphrase. For our test we skipped the passphrase so the script could run unattended; do not do that for keys you actually use.

The two files

id_ed25519       411 bytes   private key
id_ed25519.pub    98 bytes   public key

$ head -1 id_ed25519
-----BEGIN OPENSSH PRIVATE KEY-----

$ cat id_ed25519.pub
ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAII+IjKako5WgqS1oQgx3QhLQ... demo@example.com

The .pub file is one line: the key type, the key itself in Base64, and the comment. That is the part you copy to servers and websites. The file without an extension is the private key. Treat it like the master password to every machine that trusts it.

You can always regenerate the public key from the private key with ssh-keygen -y -f id_ed25519; our test produced the same ssh-ed25519 AAAA... line. The reverse is impossible.

Ed25519 vs RSA

Ed25519 RSA 4096
Public key size in our test 98 bytes 735 bytes
Security Strong, modern Strong at 3072+ bits
Speed Very fast Slower
Compatibility OpenSSH 6.5+ (2014) and most modern services Almost everything, including very old systems
Command ssh-keygen -t ed25519 ssh-keygen -t rsa -b 4096

Use Ed25519 unless a system cannot accept it. Avoid DSA keys, which modern OpenSSH no longer supports, and RSA keys shorter than 2048 bits. Hardware-backed types such as ed25519-sk keep the private key on a security key like a YubiKey, so it can never be copied off your computer at all.

Fingerprints

$ ssh-keygen -lf id_ed25519.pub
256 SHA256:s+pZpCcc6uy+S6F9hcuw6RQxIWVyVo+iwLMoLrj3N0U demo@example.com (ED25519)

A fingerprint is a short hash of a public key, used to identify it. Services like GitHub show fingerprints in their key settings, so you can match each listed key to a file on your machine. The first time you connect to a new server, SSH shows the server’s fingerprint and asks you to confirm it. Check it against a trusted source rather than typing “yes” by habit, because that prompt is your protection against connecting to an impostor.

Passphrases

A passphrase encrypts the private key file on disk. If your laptop is stolen or a backup leaks, the key is useless without it. We encrypted a test key and tried the wrong passphrase:

$ ssh-keygen -y -P wrong -f locked
Load key "locked": incorrect passphrase supplied to decrypt private key

Use a long, memorable passphrase, not a short password. To change it later without making a new key, run ssh-keygen -p -f ~/.ssh/id_ed25519.

Tip: Never pass a passphrase on the command line in a real setup, as we did with -P for this test. It can end up in your shell history and be visible to other processes. Let ssh-keygen prompt you instead.

ssh-agent: type the passphrase once

eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519

The agent holds the decrypted key in memory for your session, so every git push and ssh uses it without asking again. macOS Keychain, Windows’ OpenSSH Authentication Agent service and most Linux desktops can start the agent automatically. Be careful with agent forwarding (ssh -A): it lets the remote machine use your agent while you are connected, so only forward to servers you fully trust.

Adding a key to a server

ssh-copy-id -i ~/.ssh/id_ed25519.pub user@server.example.com

This appends your public key to ~/.ssh/authorized_keys on the server. Without ssh-copy-id, you can paste the public key line into that file yourself. Permissions matter: OpenSSH may refuse keys if the folder or file is writable by others. Typical settings are chmod 700 ~/.ssh and chmod 600 ~/.ssh/authorized_keys on the server, and 600 for your private key. Once key login works, many teams disable password login on the server entirely.

Keys for GitHub and CI

Add your public key in your Git host’s account settings, then test with ssh -T git@github.com. Use SSH remote URLs such as git@github.com:org/repo.git. Our Git tutorial covers setting up remotes.

For automation, do not reuse your personal key. Create a dedicated key with limited access, such as a read-only deploy key for one repository, and store the private key in your CI system’s secret store, never in the repository or a Docker image. Our environment variables guide and GitHub Actions guide explain how secrets are injected safely.

The SSH config file

# ~/.ssh/config (example, not run for this article)
Host staging
    HostName 203.0.113.10
    User deploy
    IdentityFile ~/.ssh/id_ed25519_staging
    IdentitiesOnly yes

Host github.com
    IdentityFile ~/.ssh/id_ed25519_github

Now ssh staging connects with the right user and key. IdentitiesOnly yes stops SSH trying every key in your agent, which can trigger “too many authentication failures” errors on servers that limit attempts.

Key hygiene

  • Never share or paste a private key, including into chat, tickets, AI tools or code.
  • One key per purpose or device, so a lost laptop means revoking one key, not all access.
  • Review authorized_keys and your Git host’s key list regularly; remove keys for people and machines that no longer need access.
  • Rotate keys when someone leaves or a device is lost, and consider SSH certificates or short-lived credentials for larger teams.
  • Back up carefully: an encrypted key in a password manager is fine; a plain key in a shared folder is not.
  • Scan repositories for accidentally committed keys; secret scanners in CI catch these quickly.

Troubleshooting “Permission denied (publickey)”

This is the most common SSH error. Work through it in order:

  1. Run with verbose output: ssh -v user@server shows which keys the client offers and where it stops.
  2. Is the right key loaded? ssh-add -l lists keys in the agent. Add the missing one, or set IdentityFile in your config.
  3. Is the public key on the server? Check ~/.ssh/authorized_keys for the correct user, compare fingerprints, and make sure the key is on a single line.
  4. Are permissions too open? The home directory, .ssh folder and authorized_keys must not be writable by other users.
  5. Right user and host? A typo in the username or an old IP address produces the same error.

On the server side, the SSH service’s logs usually explain exactly why a key was refused. These commands are standard examples and were not run against a server for this article.

Final thoughts

SSH keys are simple once you know the rule: the public key goes everywhere, the private key goes nowhere. Generate an Ed25519 key with a strong passphrase, load it into ssh-agent, use separate keys for automation, verify server fingerprints, and clean up old keys. Those habits make remote access both easier and far more secure than passwords.

Frequently asked questions

What is the difference between a public and private SSH key?

The public key can be shared and is placed on servers and services. The private key stays secret on your device and proves your identity by signing challenges.

Should I use Ed25519 or RSA?

Use Ed25519 for new keys: it is modern, fast and compact. Use RSA with at least 3072 bits only for systems that do not support Ed25519.

Do I need a passphrase on my SSH key?

Yes, for personal keys. It encrypts the private key file, so a stolen copy is useless. ssh-agent means you only type it once per session.

Where are SSH keys stored?

By default in the .ssh folder of your home directory, such as ~/.ssh/id_ed25519 for the private key and ~/.ssh/id_ed25519.pub for the public key.

What should I do if my private key is exposed?

Remove the matching public key from every server and service immediately, generate a new key pair, and check access logs for unexpected use.

Keep learning

Work with Stack Quora

Need help with testing, WordPress or SEO?

We help teams ship better software and grow their sites. Tell us what you are working on and we will suggest a clear next step.

Keep reading

More guides on testing, AI & modern development

Enjoyed this article? Pick a topic and jump straight into our newest tutorials.

Artificial Intelligence

LLM Structured Output Explained: Getting Reliable JSON From AI Models

LLM structured output explained with validation we ran in Pydantic: JSON mode and schemas, constrained decoding, the ways model output…

Read article →
DevOps

SSH Keys Explained: How They Work, ssh-keygen, Ed25519 and Safe Habits

SSH keys explained with keys we generated: public vs private keys, ssh-keygen, Ed25519 vs RSA, fingerprints, passphrases, ssh-agent, authorized_keys, config…

Read article →
DevOps

HTTP Caching Explained: Cache-Control, ETag and 304 Responses, With a Live Test

HTTP caching explained with a small server we ran: Cache-Control directives, max-age, no-cache vs no-store, ETag revalidation and 304 responses,…

Read article →
Test Automation

API Contract Testing Explained: Catch Breaking Changes Before They Ship

API contract testing explained with a schema check we ran: what a contract is, consumer-driven contracts, Pact, OpenAPI, which changes…

Read article →
Software Testing

Risk-Based Testing Explained: Score Risks, Build a Matrix and Test What Matters

Risk-based testing explained step by step: identifying risks, scoring likelihood and impact, building a risk matrix, mapping risk to test…

Read article →
Databases & SQL

SQL Constraints Explained: NOT NULL, UNIQUE, CHECK and FOREIGN KEY, Tested

SQL constraints explained with tests we ran: NOT NULL, UNIQUE, CHECK, DEFAULT, PRIMARY and FOREIGN KEY, the NULL loopholes, SQLite's…

Read article →
Databases & SQL

SQL Date Functions Explained: Adding, Comparing and Grouping Dates Without Bugs

SQL date functions explained with queries we ran: adding intervals, month-end surprises, date differences, the BETWEEN trap, grouping by day,…

Read article →
Software Development

Python asyncio Explained: async, await, gather and TaskGroup With Real Timings

Python asyncio explained with code we timed: coroutines, await, gather, TaskGroup, error handling, timeouts, limiting concurrency and the blocking-call mistake…

Read article →
Software Development

Debounce vs Throttle in JavaScript: How They Work, When to Use Each, Tested

Debounce vs throttle in JavaScript explained with timings we measured in Node.js: how each works, simple implementations, leading and trailing…

Read article →
Software Development

Python Context Managers Explained: The with Statement, contextlib and Real Uses

Python context managers explained with code we ran: how with works, __enter__ and __exit__, contextlib.contextmanager, suppress, ExitStack, database transactions and…

Read article →
Artificial Intelligence

LLM Temperature and Top-p Explained: How AI Models Choose Their Next Word

LLM temperature and top-p explained with a simulation we ran: how probabilities are reshaped, what top-p keeps, recommended settings by…

Read article →
DevOps

YAML Explained: Syntax, the Norway Problem and Other Traps We Tested

YAML explained with a parser test: basic syntax, lists and maps, the Norway problem, versions turning into floats, octal zip…

Read article →
DevOps

Cron Jobs Explained: Syntax, Real Schedules and the Mistakes That Break Them

Cron jobs explained with schedules we verified: the five fields, operators, real examples, the day-of-month OR day-of-week trap, environment problems,…

Read article →
Software Testing

Bug Life Cycle in Software Testing: Every Status, Who Owns It and Common Mistakes

The bug life cycle explained: every defect status from New to Closed, who owns each step, rejected, deferred and reopened…

Read article →
Test Automation

Property-Based Testing in Python: Hypothesis Finds the Bugs Your Examples Miss

Property-based testing explained with Hypothesis tests we ran: properties vs examples, strategies, shrinking, the real bugs it found in our…

Read article →
Databases & SQL

SQL Pagination Explained: OFFSET vs Keyset, With a Million-Row Test

SQL pagination explained with a million-row test we ran: LIMIT and OFFSET, why deep pages get slow, keyset (cursor) pagination,…

Read article →
Databases & SQL

SQL EXISTS vs IN: Differences, the NOT IN NULL Trap and Performance

SQL EXISTS vs IN explained with queries we ran: how each works, why NOT IN returns no rows when a…

Read article →
Software Development

Python Logging Explained: Levels, Handlers, Formatters and Mistakes to Avoid

Python logging explained with code we ran: levels, basicConfig quirks, named loggers, handlers and formatters, logger.exception, lazy formatting, duplicate logs…

Read article →
Software Development

JavaScript Map vs Object: Key Differences, Pitfalls and a Benchmark

JavaScript Map vs Object explained with code we ran in Node.js: key types, ordering, the prototype trap, JSON, size, WeakMap…

Read article →
Software Development

Python Type Hints Explained: Annotations, mypy and the Errors It Catches

Python type hints explained with a real mypy run: basic annotations, Optional and unions, TypedDict, Literal, Protocol, generics, Callable, strict…

Read article →
Artificial Intelligence

LLM Tokens Explained: How AI Models Read Text, Count Costs and Fill Context Windows

LLM tokens explained with a tokenizer we ran: how text is split, why counts differ by language and tokenizer, context…

Read article →
DevOps

Semantic Versioning Explained: MAJOR.MINOR.PATCH, Ranges and Release Rules

Semantic versioning explained with code we ran: MAJOR.MINOR.PATCH rules, pre-releases, correct version sorting, caret and tilde ranges, 0.x versions, lockfiles…

Read article →
DevOps

Git Reset vs Revert: How to Undo Commits Safely, With a Tested Example

Git reset vs revert explained with a repository we built: soft, mixed and hard reset, revert commits, reverting merges, recovering…

Read article →
Software Testing

The Test Pyramid Explained: Layers, Ratios and When to Break the Shape

The test pyramid explained: unit, integration and end-to-end layers, sensible ratios, the ice cream cone anti-pattern, the testing trophy and…

Read article →
Test Automation

pytest Fixtures Explained: Scope, yield Teardown, conftest.py and Factories

pytest fixtures explained with a suite we ran: how fixtures work, scopes, yield teardown, conftest.py, factory fixtures, parametrized fixtures, built-ins…

Read article →
Databases & SQL

SQL Upsert Explained: INSERT ON CONFLICT, MERGE and the REPLACE Trap

SQL upsert explained with queries we ran: INSERT ON CONFLICT DO UPDATE and DO NOTHING, the excluded row, conditional updates,…

Read article →
Databases & SQL

SQL LIKE Operator Explained: Wildcards, Case Sensitivity and Performance

The SQL LIKE operator explained with queries we ran: % and _ wildcards, escaping, NOT LIKE and NULL, case sensitivity…

Read article →
Artificial Intelligence

AI Hallucinations Explained: Why Language Models Make Things Up and How to Reduce It

AI hallucinations explained: what they are, why language models produce them, common types, how to reduce them with grounding and…

Read article →
Test Automation

Mocking in Python: unittest.mock, patch and the Mistakes That Make Tests Lie

Mocking in Python with unittest.mock, tested: Mock and return_value, side_effect, patching where a name is used, spec and autospec, mocking…

Read article →
Software Testing

Code Coverage Explained: Line vs Branch Coverage and What 100% Really Means

Code coverage explained with a real coverage.py run: line, branch and condition coverage, reading a report, why 100% coverage can…

Read article →
Artificial Intelligence

What Are AI Agents? How They Work, With a Simple Agent Loop You Can Run

What are AI agents? A plain guide to the agent loop, tools, memory and guardrails, with a small Python loop…

Read article →
Test Automation

TDD vs BDD: Key Differences, One Feature Built Both Ways and When to Use Each

TDD vs BDD explained with one discount feature built both ways: red-green-refactor with code we ran, Given-When-Then scenarios, tools, trade-offs…

Read article →
Software Testing

User Acceptance Testing (UAT): Process, Checklist and Real Examples

User acceptance testing explained: who runs it, how it differs from QA, a seven-step process, a UAT test case template,…

Read article →
Artificial Intelligence

What Are Embeddings? Vectors and Cosine Similarity Explained Simply

What are embeddings? A plain-language guide to vectors, cosine similarity and semantic search, with a small example we ran, common…

Read article →
Test Automation

Flaky Tests: What Causes Them and How to Fix Them for Good

Flaky tests explained with demos we ran: the main causes, how to confirm and diagnose one, proper fixes for timing,…

Read article →
Software Testing

Load Testing vs Stress Testing: Differences, Metrics and k6 Examples

Load testing vs stress testing explained: what each proves, spike and soak tests, the metrics that matter, why averages mislead,…

Read article →