Public/private key authentication using SSH
ssh

Contents
<p>Stand up a fresh cloud server, leave password login on, and within a few hours your auth log will be full of bots trying <code>root</code>/<code>admin</code>/<code>password123</code> against port 22. I have watched a brand-new VPS take thousands of login attempts before I had even finished configuring it. None of them got in, because there was no password to guess — the box only accepted a cryptographic key that lives on my laptop and nowhere else. That is the whole pitch for public/private key authentication, and it is the first thing I set up on any machine I intend to keep.</p>
<p>The reason keys beat passwords is not that they are longer or harder to type. It is that the secret never travels. With a password, you send it to the server every time you log in, so anyone who compromises the server or sits in the middle of the connection can capture it. With key authentication the server sends <em>you</em> a challenge, your machine signs it with the private key, and the server verifies the signature against the public key it already holds. The private key never leaves your disk. There is nothing to sniff and nothing to phish.</p>
<h2 id="why-ed25519-not-rsa">Why ed25519, not RSA</h2><div class="ad-unit ad-in-article" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<ins class="adsbygoogle" style="display:block;text-align:center"
data-ad-client="ca-pub-3726833845844946"
data-ad-slot="3291553914"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
<p>Most tutorials — including, embarrassingly, an older version of this one — tell you to run <code>ssh-keygen -t rsa</code> and move on. Don’t. In 2020 and beyond the sensible default is Ed25519: a modern elliptic-curve signature scheme that is faster, produces tiny keys, and sidesteps a whole category of RSA foot-guns like accidentally generating a 1024-bit key that is now trivially breakable. RSA is still fine if you generate it at 4096 bits, but Ed25519 gives you equivalent security in a key you can read out over the phone if you had to.</p>
<p>The one caveat: Ed25519 needs OpenSSH 6.5 or newer on both ends. Anything from the last decade has it. If you are talking to genuinely ancient enterprise kit, fall back to <code>ssh-keygen -t rsa -b 4096</code>, and only then.</p>
<h2 id="preparing-your-system">Preparing your system</h2>
<p>SSH is fussy about permissions, and it is right to be. If your <code>~/.ssh</code> directory is world-readable, SSH assumes something is wrong and silently refuses to use your keys — one of the most common “it just won’t work” complaints. Create the directory and lock it down before you do anything else:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> mkdir -p ~/.ssh
</span></span><span class="line"><span class="cl"><span class="gp">$</span> chmod <span class="m">700</span> ~/.ssh
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>700</code> means only your user can read, write, or enter the directory. Get in the habit now; it saves a debugging session later.</p>
<h2 id="creating-your-keypair">Creating your keypair</h2><div class="ad-unit ad-in-article" aria-label="Advertisement">
<span class="ad-label">Advertisement</span>
<ins class="adsbygoogle" style="display:block;text-align:center"
data-ad-client="ca-pub-3726833845844946"
data-ad-slot="3291553914"
data-ad-format="auto"
data-full-width-responsive="true"></ins>
<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>
</div>
<p>Generate the keypair with <code>ssh-keygen</code>. I always add a comment so I can tell keys apart when I have half a dozen <code>authorized_keys</code> entries across different machines:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span><span class="lnt">9
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ssh-keygen -t ed25519 -C <span class="s2">"me@laptop-2020"</span>
</span></span><span class="line"><span class="cl"><span class="go">Generating public/private ed25519 key pair.
</span></span></span><span class="line"><span class="cl"><span class="go">Enter file in which to save the key (/home/me/.ssh/id_ed25519):
</span></span></span><span class="line"><span class="cl"><span class="go">Enter passphrase (empty for no passphrase):
</span></span></span><span class="line"><span class="cl"><span class="go">Enter same passphrase again:
</span></span></span><span class="line"><span class="cl"><span class="go">Your identification has been saved in /home/me/.ssh/id_ed25519
</span></span></span><span class="line"><span class="cl"><span class="go">Your public key has been saved in /home/me/.ssh/id_ed25519.pub
</span></span></span><span class="line"><span class="cl"><span class="go">The key fingerprint is:
</span></span></span><span class="line"><span class="cl"><span class="go">SHA256:mS7t3p... me@laptop-2020
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Two prompts matter here. Accept the default location (<code>~/.ssh/id_ed25519</code>) unless you have a reason not to. And <strong>set a passphrase</strong> — this is not optional in my book. The passphrase encrypts the private key on disk, so a stolen laptop does not equal a stolen key. “But then I have to type it every time” — no, you don’t. That is what <code>ssh-agent</code> is for, and I will come back to it.</p>
<p>You now have two files:</p>
<ul>
<li><code>~/.ssh/id_ed25519</code> — your <strong>private</strong> key. This never leaves the machine. Not in a git repo, not in a chat message, not on a USB stick you lend to a friend.</li>
<li><code>~/.ssh/id_ed25519.pub</code> — your <strong>public</strong> key. This is safe to hand out freely; it is the half that goes on servers.</li>
</ul>
<h2 id="installing-the-public-key">Installing the public key</h2>
<p>To log in with the key, the server needs your <em>public</em> key in the target account’s <code>~/.ssh/authorized_keys</code>. The clean way is <code>ssh-copy-id</code>, which handles the directory creation and permissions for you:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ssh-copy-id -i ~/.ssh/id_ed25519.pub [email protected]
</span></span></code></pre></td></tr></table>
</div>
</div><p>It will ask for your password one last time (it has to — you have not set up key auth yet), then append the key. If <code>ssh-copy-id</code> is not available, do it by hand, minding the permissions:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ssh [email protected] <span class="s2">"mkdir -p ~/.ssh && chmod 700 ~/.ssh"</span>
</span></span><span class="line"><span class="cl"><span class="gp">$</span> cat ~/.ssh/id_ed25519.pub <span class="p">|</span> ssh [email protected] <span class="s2">"cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys"</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>authorized_keys</code> file must be <code>600</code> (read/write for the owner only). If it is group- or world-writable, SSH ignores it — same silent failure as before.</p>
<h2 id="logging-in-and-using-the-agent">Logging in and using the agent</h2>
<p>Now connect normally:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> ssh [email protected]
</span></span><span class="line"><span class="cl"><span class="go">Enter passphrase for key '/home/me/.ssh/id_ed25519':
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The passphrase it asks for is the one protecting your <em>local</em> key, not a server password — nothing was sent over the wire. Type it and you are in. To avoid re-typing it every session, load the key into the agent once:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> <span class="nb">eval</span> <span class="s2">"</span><span class="k">$(</span>ssh-agent -s<span class="k">)</span><span class="s2">"</span>
</span></span><span class="line"><span class="cl"><span class="gp">$</span> ssh-add ~/.ssh/id_ed25519
</span></span><span class="line"><span class="cl"><span class="go">Enter passphrase for /home/me/.ssh/id_ed25519:
</span></span></span><span class="line"><span class="cl"><span class="go">Identity added: /home/me/.ssh/id_ed25519 (me@laptop-2020)
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>macOS and most desktop Linux distros wire the agent into your login session automatically, so in practice you unlock the key once when you sit down and forget about it. A short, tidy <code>~/.ssh/config</code> also saves a lot of typing:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">Host prod
</span></span><span class="line"><span class="cl"> HostName server.example.com
</span></span><span class="line"><span class="cl"> User deploy
</span></span><span class="line"><span class="cl"> IdentityFile ~/.ssh/id_ed25519
</span></span><span class="line"><span class="cl"> IdentitiesOnly yes
</span></span></code></pre></td></tr></table>
</div>
</div><p>Now <code>ssh prod</code> does the whole thing. <code>IdentitiesOnly yes</code> tells SSH to offer only that key rather than every key in your agent, which matters once you accumulate keys and start hitting the server’s <code>MaxAuthTries</code> limit.</p>
<h2 id="turning-off-password-login">Turning off password login</h2>
<p>Key auth adds value the moment you <em>remove</em> the fallback. As long as passwords still work, those bots hammering port 22 still have a target. Once your key login works reliably — test it in a second terminal before you touch anything, and keep that session open — disable password auth in <code>/etc/ssh/sshd_config</code>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-fallback" data-lang="fallback"><span class="line"><span class="cl">PubkeyAuthentication yes
</span></span><span class="line"><span class="cl">PasswordAuthentication no
</span></span><span class="line"><span class="cl">ChallengeResponseAuthentication no
</span></span><span class="line"><span class="cl">PermitRootLogin prohibit-password
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then reload the daemon:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-console" data-lang="console"><span class="line"><span class="cl"><span class="gp">$</span> sudo systemctl reload ssh <span class="c1"># or 'sshd' on some distros</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p><code>PermitRootLogin prohibit-password</code> still lets you log in as root <em>with a key</em> while blocking password root logins outright. This is a small slice of proper server hardening — I go through the full checklist in <a href="/story/from-zero-to-ssh-hero-securely-hardening-a-linux-server-in-2025/">hardening a Linux server</a>, including firewalls, fail2ban, and non-standard ports.</p>
<h2 id="troubleshooting-what-actually-goes-wrong">Troubleshooting: what actually goes wrong</h2>
<p>Nine times out of ten, key auth “doesn’t work” for one of these reasons, and the fix is boring:</p>
<ul>
<li><strong>Permissions.</strong> Run <code>chmod 700 ~/.ssh</code> and <code>chmod 600 ~/.ssh/authorized_keys</code> on the <em>server</em>. SSH refuses loose permissions by design. This is the single most common cause.</li>
<li><strong>Wrong account.</strong> The key goes in the <code>authorized_keys</code> of the user you log in <em>as</em>. Putting your key in <code>/root/.ssh</code> won’t help if you <code>ssh user@…</code>.</li>
<li><strong>You have no idea why.</strong> Ask SSH. <code>ssh -v user@server</code> prints a verbose handshake; look for lines about which key it offered and whether the server accepted it. On the server side, <code>sudo journalctl -u ssh -f</code> while you attempt a login usually names the exact problem (“Authentication refused: bad ownership or modes for directory”).</li>
<li><strong>Agent not running.</strong> <code>ssh-add -l</code> should list your key. “Could not open a connection to your authentication agent” means you need <code>eval "$(ssh-agent -s)"</code> first.</li>
<li><strong>SELinux.</strong> On RHEL-family systems, a manually created <code>authorized_keys</code> can have the wrong SELinux context. <code>restorecon -Rv ~/.ssh</code> fixes it.</li>
</ul>
<h2 id="one-key-per-machine-and-rotating-them">One key per machine, and rotating them</h2>
<p>A habit worth forming early: generate a <em>separate</em> keypair on each device you log in from — laptop, desktop, phone, CI runner — rather than copying one private key everywhere. The private key should never travel between machines. When you do it this way, every server accumulates several public keys in <code>authorized_keys</code>, one per device, and the payoff is precise control. Lose your laptop? Delete that one public key line on every server and the laptop is instantly locked out, while every other device keeps working. Copy one key to five machines and a single theft compromises all five, with no clean way to revoke just one.</p>
<p>Revocation is genuinely that simple — there is no certificate authority to phone, no revocation list to publish. Edit <code>authorized_keys</code>, remove the line, done. For a handful of servers you do it by hand; past a dozen, you script it or reach for a configuration-management tool, which is where key management stops being a per-server chore and becomes infrastructure. Rotating keys periodically (generate new, add the public half everywhere, then remove the old) is cheap insurance against a key that has been quietly sitting on a machine you no longer fully trust.</p>
<h2 id="beyond-a-single-key">Beyond a single key</h2>
<p>Once keys click, you start doing more with them. You can restrict what a key is allowed to do by prefixing its <code>authorized_keys</code> line with options — <code>command="…"</code> to force a specific command, <code>no-port-forwarding</code> to stop a key being used as a tunnel, <code>from="192.168.1.0/24"</code> to accept it only from your LAN. Deploy keys, backup keys, and CI keys all live this way, each scoped to exactly what it needs and nothing more. And if you want the private key to live on hardware that a thief cannot copy at all, a security token is the next step up — I cover that in <a href="/story/yubikey-for-everything-ssh-gpg-fido2-and-the-paperweight-drawer/">YubiKey for everything</a>, where the key material never touches the disk in the first place. For account access more broadly, layering a second factor on top is worth reading up on in <a href="/story/totp-and-webauthn-two-factor-authentication-without-authy/">two-factor authentication without Authy</a>.</p>
<h2 id="is-it-worth-it">Is it worth it?</h2>
<p>Unreservedly, yes. This is not one of those security measures that trades real inconvenience for marginal safety. Key authentication is <em>more</em> convenient than passwords once the agent is set up — no typing, no password manager lookups, no forgotten credentials — while being dramatically harder to attack. The setup is ten minutes, most of which is spent reading prompts. Every server I run, and every server I would trust anyone else to run, has password login disabled and keys as the only door.</p>
<p>The one habit it demands is that you take backups of your private key seriously and never treat it casually. Lose it with no backup and you are locked out; leak it and someone else is locked in. Protect it with a passphrase, keep an encrypted offline copy, and rotate it if you ever have reason to doubt it. Do that, and key-based SSH is simply how you should be logging in.</p>
<p>If there is one thing to take away, it is this: set up the key, confirm it works in a second terminal, <em>then</em> turn password authentication off — and never turn it back on. The moment your servers stop accepting passwords, the endless background noise of credential-guessing bots becomes completely irrelevant to you, because there is nothing left for them to guess. That quiet is the whole point.</p>
Advertisement
Related Content
Advertisement




