{"id":156,"date":"2026-10-18T19:27:51","date_gmt":"2026-10-18T17:27:51","guid":{"rendered":"https:\/\/nodeguard.io\/blog\/?p=156"},"modified":"2026-10-18T20:19:03","modified_gmt":"2026-10-18T18:19:03","slug":"how-to-set-up-ssh-keys-on-ubuntu","status":"publish","type":"post","link":"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/","title":{"rendered":"How to Set Up SSH Keys on Ubuntu: A Comprehensive 6-Step Guide"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Passwords are the weakest part of most servers. Bots find your IP within hours of provisioning, then grind through credential lists around the clock. SSH keys end that problem outright \u2014 there is nothing left to guess. This guide walks through how to set up SSH keys on Ubuntu from a clean start: generating a modern key pair, installing the public half on your server, testing it properly, and only then turning password logins off.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The same six steps work unchanged on Debian, which Ubuntu is built on, and almost unchanged on AlmaLinux, Rocky Linux and Fedora. A later section covers the handful of places those distributions genuinely differ, including the SELinux trap that silently breaks key logins on the RHEL family.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you are still logging in with a password and want the groundwork first, our companion guide on <a href=\"https:\/\/nodeguard.io\/blog\/how-to-connect-to-a-linux-vps-using-ssh\">how to connect to a Linux VPS using SSH<\/a> covers the connection basics. This article picks up where that one stops and focuses purely on SSH keys.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Everything below is written against Ubuntu 26.04 LTS and tested on a standard cloud VPS, but the commands are identical on Ubuntu 24.04 and 22.04.<\/p>\n\n\n\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_86 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">In This Guide<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Why_SSH_keys_beat_passwords_every_time\" >Why SSH keys beat passwords every time<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#What_you_need_before_you_start\" >What you need before you start<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#How_to_set_up_SSH_keys_on_Ubuntu_in_6_steps\" >How to set up SSH keys on Ubuntu in 6 steps<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Step_1_%E2%80%94_Generate_your_SSH_key_pair\" >Step 1 \u2014 Generate your SSH key pair<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Step_2_%E2%80%94_Set_a_passphrase_and_load_the_agent\" >Step 2 \u2014 Set a passphrase and load the agent<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Step_3_%E2%80%94_Copy_the_public_key_to_your_server\" >Step 3 \u2014 Copy the public key to your server<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Step_4_%E2%80%94_Test_before_you_change_anything\" >Step 4 \u2014 Test before you change anything<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Step_5_%E2%80%94_Disable_password_authentication\" >Step 5 \u2014 Disable password authentication<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Step_6_%E2%80%94_Verify_from_a_fresh_session\" >Step 6 \u2014 Verify from a fresh session<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Setting_up_SSH_keys_on_Debian_AlmaLinux_Rocky_Linux_and_Fedora\" >Setting up SSH keys on Debian, AlmaLinux, Rocky Linux and Fedora<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Debian_the_same_steps_two_things_to_check_first\" >Debian: the same steps, two things to check first<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#AlmaLinux_Rocky_Linux_and_Fedora_four_real_differences\" >AlmaLinux, Rocky Linux and Fedora: four real differences<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Three_problems_that_catch_almost_everyone\" >Three problems that catch almost everyone<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Add_a_second_key_before_you_need_it\" >Add a second key before you need it<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Managing_SSH_keys_across_a_team_and_multiple_servers\" >Managing SSH keys across a team and multiple servers<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Frequently_asked_questions\" >Frequently asked questions<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-ssh-keys-on-ubuntu\/#Wrapping_up\" >Wrapping up<\/a><\/li><\/ul><\/nav><\/div>\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Why_SSH_keys_beat_passwords_every_time\"><\/span>Why SSH keys beat passwords every time<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">SSH keys come in pairs: two mathematically linked files. The private key stays on your laptop and never moves. The public key goes onto the server, into a file called <code>authorized_keys<\/code>. When you connect, the server sends a challenge that only the holder of the matching private key can answer. Your secret is never transmitted, so there is nothing for an attacker to capture in transit or brute-force at the login prompt.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The practical difference is stark. A strong 16-character password has maybe 100 bits of entropy and can be phished, reused, or shoulder-surfed. A modern Ed25519 key offers security comparable to a 3072-bit RSA key in a 68-byte file, and it cannot be typed into a convincing fake prompt. This is why every serious hardening checklist \u2014 including <a href=\"https:\/\/infosec.mozilla.org\/guidelines\/openssh\" target=\"_blank\" rel=\"noopener\">Mozilla&#8217;s OpenSSH guidelines<\/a> \u2014 treats SSH keys as the baseline rather than an upgrade.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">There is an operational win too. Once you set up SSH keys on Ubuntu, logins become instant, deployment scripts and CI runners can authenticate without storing a secret in plain text, and you can revoke one person&#8217;s access by deleting one line from one file. Password rotation policies stop being something you manage, because there is no password left to rotate.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-how-it-works-1024x576.webp\" alt=\"How SSH key authentication works between a local machine and a Linux server\" class=\"wp-image-157\" srcset=\"https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-how-it-works-1024x576.webp 1024w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-how-it-works-300x169.webp 300w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-how-it-works-768x432.webp 768w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-how-it-works-900x506.webp 900w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-how-it-works.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"What_you_need_before_you_start\"><\/span>What you need before you start<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Three things, and nothing else:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>An Ubuntu server you can already reach \u2014 a <a href=\"https:\/\/nodeguard.io\/cloud-vps\">Cloud VPS<\/a>, a dedicated box, or a local virtual machine. You need working password access, or console access through your provider&#8217;s panel, for this first run only.<\/li>\n\n\n\n<li>A terminal on your own machine. Linux and macOS have one built in. Windows 10 and 11 ship the OpenSSH client with Windows Terminal or PowerShell, so you can create SSH keys there natively without PuTTY.<\/li>\n\n\n\n<li>A non-root user with <code>sudo<\/code> on the server. If you are still logging in as root, create a normal user first \u2014 our walkthrough of <a href=\"https:\/\/nodeguard.io\/blog\/how-to-set-up-an-ubuntu-vps\">how to set up an Ubuntu VPS<\/a> covers that in the opening steps.<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">One rule before anything else: keep your existing session open the whole way through. Every lockout story starts with someone closing the only working terminal they had.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"How_to_set_up_SSH_keys_on_Ubuntu_in_6_steps\"><\/span>How to set up SSH keys on Ubuntu in 6 steps<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Steps 1 to 3 create and install your SSH keys, step 4 proves they work, and steps 5 and 6 close the door behind you.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_1_%E2%80%94_Generate_your_SSH_key_pair\"><\/span>Step 1 \u2014 Generate your SSH key pair<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Run this on your <strong>local machine<\/strong>, not the server. Generating the pair on the wrong box is the single most common mistake people make on their first attempt.<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-keygen -t ed25519 -C \"alex@laptop-2026\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Press Enter to accept the default path, <code>~\/.ssh\/id_ed25519<\/code>. You now have two files: the private key with no extension, and <code>id_ed25519.pub<\/code>, the public half.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Use Ed25519 unless you have a specific reason not to. It is faster, the keys are tiny, and it has been the sensible default for years. If you must talk to something ancient that predates it, generate RSA SSH keys instead:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-keygen -t rsa -b 4096 -C \"alex@laptop-2026\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Avoid DSA and ECDSA entirely. The <code>-C<\/code> comment is free text and does nothing cryptographically, but it is what tells you six months from now which laptop a line in <code>authorized_keys<\/code> belongs to. Label your SSH keys properly from day one. The full option list lives in the <a href=\"https:\/\/man.openbsd.org\/ssh-keygen\" target=\"_blank\" rel=\"noopener\">ssh-keygen manual<\/a>.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_2_%E2%80%94_Set_a_passphrase_and_load_the_agent\"><\/span>Step 2 \u2014 Set a passphrase and load the agent<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">When <code>ssh-keygen<\/code> asks for a passphrase, give it one. SSH keys without a passphrase are plaintext credentials: anyone who copies that file owns every server it touches.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The usual objection is that typing a passphrase on every connection defeats the point. It does not, because <code>ssh-agent<\/code> holds the decrypted key in memory for the session:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>eval \"$(ssh-agent -s)\"\nssh-add ~\/.ssh\/id_ed25519\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You type the passphrase once per boot and connect freely after that. On macOS, add <code>--apple-use-keychain<\/code> to store it in the Keychain. On Ubuntu desktop, GNOME Keyring usually unlocks your SSH keys automatically at login.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_3_%E2%80%94_Copy_the_public_key_to_your_server\"><\/span>Step 3 \u2014 Copy the public key to your server<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\"><code>ssh-copy-id<\/code> handles installation correctly \u2014 it creates <code>~\/.ssh<\/code>, appends to <code>authorized_keys<\/code>, and sets permissions in one shot:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh-copy-id -i ~\/.ssh\/id_ed25519.pub alex@your-server-ip\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Enter your password one last time. If SSH runs on a non-standard port, add <code>-p 2222<\/code> before the username.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Note the <code>-i<\/code> flag pointing at the <code>.pub<\/code> file. Without it, <code>ssh-copy-id<\/code> will happily push every one of the SSH keys loaded in your agent, which quietly grants more access than you intended.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>From Windows, or without ssh-copy-id.<\/strong> Windows has no <code>ssh-copy-id<\/code>. This PowerShell one-liner does the same job:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>type $env:USERPROFILE\\.ssh\\id_ed25519.pub | ssh alex@your-server-ip \"mkdir -p ~\/.ssh &amp;&amp; chmod 700 ~\/.ssh &amp;&amp; cat &gt;&gt; ~\/.ssh\/authorized_keys &amp;&amp; chmod 600 ~\/.ssh\/authorized_keys\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The same command works from any Unix shell with <code>cat<\/code> in place of <code>type<\/code>. If you only have console access through your host&#8217;s panel, open <code>~\/.ssh\/authorized_keys<\/code> in an editor and paste the public key as a single unbroken line. A line wrapped in the middle is a dead key \u2014 the most frequent failure when people install SSH keys by hand.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Already provisioned the server with a key through your provider&#8217;s control panel? Then this step is done, and you can jump straight to testing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_4_%E2%80%94_Test_before_you_change_anything\"><\/span>Step 4 \u2014 Test before you change anything<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Open a <strong>second terminal<\/strong> and leave the first one connected:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh alex@your-server-ip\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">You should land on the server with no password prompt. If something looks wrong, ask SSH to explain itself:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -v alex@your-server-ip\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">The verbose output names which of your SSH keys it offered and what the server said back. Do not continue to step 5 until this login works cleanly.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_5_%E2%80%94_Disable_password_authentication\"><\/span>Step 5 \u2014 Disable password authentication<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Now that your SSH keys work, close the password door. Rather than editing the main config file, drop a small override into the include directory \u2014 it survives package upgrades and keeps your changes in one readable place:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo nano \/etc\/ssh\/sshd_config.d\/00-hardening.conf\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Add:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>PubkeyAuthentication yes\nPasswordAuthentication no\nPermitEmptyPasswords no\nKbdInteractiveAuthentication no\nPermitRootLogin prohibit-password\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check the syntax before applying it \u2014 <code>sshd -t<\/code> prints nothing at all if the config is valid:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sshd -t &amp;&amp; sudo systemctl restart ssh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Watch for one trap. Cloud images frequently ship <code>\/etc\/ssh\/sshd_config.d\/50-cloud-init.conf<\/code> containing <code>PasswordAuthentication yes<\/code>. Files load in alphabetical order and the <strong>first<\/strong> setting wins in OpenSSH, so a <code>50-<\/code> file silently overrides a <code>99-<\/code> one. Naming yours <code>00-hardening.conf<\/code>, as above, keeps you ahead of it. Confirm what is actually live rather than trusting the file:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>sudo sshd -T | grep -iE \"passwordauth|pubkeyauth|permitrootlogin\"\n<\/code><\/pre>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Step_6_%E2%80%94_Verify_from_a_fresh_session\"><\/span>Step 6 \u2014 Verify from a fresh session<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">With your original terminal still open, start a new connection and deliberately ask for a password:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ssh -o PreferredAuthentications=password alex@your-server-ip\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A <code>Permission denied (publickey)<\/code> response is the result you want \u2014 it proves passwords are genuinely refused and your SSH keys are the only way in. Then connect normally one more time to confirm key login still works. Only now is it safe to close the original session.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Six steps, and your server no longer has a guessable front door.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1024\" height=\"576\" src=\"https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-6-steps-1024x576.webp\" alt=\"how to set up ssh keys on ubuntu\" class=\"wp-image-158\" srcset=\"https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-6-steps-1024x576.webp 1024w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-6-steps-300x169.webp 300w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-6-steps-768x432.webp 768w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-6-steps-900x506.webp 900w, https:\/\/nodeguard.io\/blog\/wp-content\/uploads\/2026\/10\/ssh-keys-ubuntu-6-steps.webp 1200w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Setting_up_SSH_keys_on_Debian_AlmaLinux_Rocky_Linux_and_Fedora\"><\/span>Setting up SSH keys on Debian, AlmaLinux, Rocky Linux and Fedora<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Key generation is completely distribution-agnostic. <code>ssh-keygen<\/code>, <code>ssh-agent<\/code>, <code>ssh-copy-id<\/code> and <code>~\/.ssh\/config<\/code> behave identically everywhere, because they are all OpenSSH, not Ubuntu. What changes is the handful of things around them.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Debian_the_same_steps_two_things_to_check_first\"><\/span>Debian: the same steps, two things to check first<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">Debian and Ubuntu share the same packaging, the same <code>ssh<\/code> service name, and the same <code>\/etc\/ssh\/sshd_config.d\/<\/code> include directory. Every command above transfers across with no edits at all.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two caveats catch people moving from Ubuntu to Debian. A minimal Debian netinst installation often ships <strong>without <code>sudo<\/code><\/strong>, and sometimes without an SSH server at all \u2014 you log in as root and add both yourself:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>apt update &amp;&amp; apt install -y sudo openssh-server\nusermod -aG sudo alex\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Second, Debian does not use socket activation for SSH, so <code>systemctl restart ssh<\/code> behaves the way you expect and the <code>Port<\/code> directive in <code>sshd_config<\/code> is honoured normally. If you are scripting against both distributions, that is a difference worth knowing.<\/p>\n\n\n\n<h3 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"AlmaLinux_Rocky_Linux_and_Fedora_four_real_differences\"><\/span>AlmaLinux, Rocky Linux and Fedora: four real differences<span class=\"ez-toc-section-end\"><\/span><\/h3>\n\n\n\n<p class=\"wp-block-paragraph\">The RHEL family needs the same six steps plus four adjustments:<\/p>\n\n\n\n<figure class=\"wp-block-table\"><table class=\"has-fixed-layout\"><thead><tr><th><\/th><th>Ubuntu \/ Debian<\/th><th>AlmaLinux \/ Rocky Linux \/ Fedora<\/th><\/tr><\/thead><tbody><tr><td><strong>Service name<\/strong><\/td><td><code>ssh<\/code><\/td><td><code>sshd<\/code><\/td><\/tr><tr><td><strong>Restart command<\/strong><\/td><td><code>sudo systemctl restart ssh<\/code><\/td><td><code>sudo systemctl restart sshd<\/code><\/td><\/tr><tr><td><strong>SELinux<\/strong><\/td><td>not enforcing<\/td><td><strong>enforcing by default<\/strong><\/td><\/tr><tr><td><strong>Firewall<\/strong><\/td><td><code>ufw<\/code><\/td><td><code>firewalld<\/code><\/td><\/tr><tr><td><strong>Crypto policy<\/strong><\/td><td>n\/a<\/td><td><code>update-crypto-policies<\/code> can refuse key types<\/td><\/tr><tr><td><strong>Socket activation<\/strong><\/td><td>yes on Ubuntu 26.04<\/td><td>no \u2014 classic <code>sshd.service<\/code><\/td><\/tr><\/tbody><\/table><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SELinux is the one that will cost you an afternoon.<\/strong> On AlmaLinux, Rocky Linux and Fedora, SELinux is enforcing out of the box and expects <code>~\/.ssh<\/code> to carry the <code>ssh_home_t<\/code> context. Create that directory by hand, restore it from a backup, or copy files in from elsewhere, and the context comes out wrong. The login then fails with a plain <code>Permission denied (publickey)<\/code> and absolutely nothing useful in the SSH logs \u2014 the denial happens below SSH, so it never explains itself.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix is one command:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>restorecon -R -v ~\/.ssh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Check whether SELinux is the culprit before you go hunting elsewhere:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>ls -Z ~\/.ssh\nsudo ausearch -m AVC -ts recent\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Using <code>ssh-copy-id<\/code> avoids the problem entirely, because it creates the directory in place with the correct context. It only bites when you build <code>authorized_keys<\/code> manually \u2014 which is exactly what people do from a provider console.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The crypto-policy difference matters less often but is worth knowing: on the RHEL family, <code>update-crypto-policies --show<\/code> reports the system-wide policy, and a <code>FUTURE<\/code> or <code>FIPS<\/code> setting will reject key types that work fine on Ubuntu. Ed25519 is accepted under the default policy, which is another reason to prefer it.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Three_problems_that_catch_almost_everyone\"><\/span>Three problems that catch almost everyone<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Permissions.<\/strong> OpenSSH refuses keys stored in world-readable locations, and gives a vague error when it does. Fix both ends:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>chmod 700 ~\/.ssh &amp;&amp; chmod 600 ~\/.ssh\/authorized_keys\nchown -R $USER:$USER ~\/.ssh\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">A home directory set to <code>777<\/code> breaks key authentication too, even when <code>~\/.ssh<\/code> itself is correct. <code>chmod 750 ~<\/code> resolves it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Socket activation.<\/strong> Ubuntu 26.04 starts SSH through <code>ssh.socket<\/code> rather than <code>ssh.service<\/code>. For key authentication this changes nothing \u2014 new connections pick up config changes immediately \u2014 but if you are also changing the listening port, editing <code>Port<\/code> in <code>sshd_config<\/code> will appear to do nothing, because the socket unit owns the port. Check with <code>systemctl status ssh.socket<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>The wrong key offered.<\/strong> If you hold several SSH keys, the client tries them in order and some servers cut the connection after too many attempts. Pin the right one per host in <code>~\/.ssh\/config<\/code>:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>Host prod\n    HostName 203.0.113.10\n    User alex\n    IdentityFile ~\/.ssh\/id_ed25519\n    IdentitiesOnly yes\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">After that, <code>ssh prod<\/code> is the whole command. <code>IdentitiesOnly yes<\/code> is the important line \u2014 it stops the agent from offering every key it holds.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Add_a_second_key_before_you_need_it\"><\/span>Add a second key before you need it<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The one failure mode SSH keys introduce is losing the private key. A dead laptop with no backup means no login, and no password fallback either, because you just disabled it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The fix takes two minutes. Generate a second pair on another device \u2014 a desktop, a work machine, even a phone with a decent SSH client \u2014 and append that public key to <code>authorized_keys<\/code> as well:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>cat ~\/.ssh\/id_ed25519_backup.pub | ssh alex@your-server-ip \"cat &gt;&gt; ~\/.ssh\/authorized_keys\"\n<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Hardware-backed keys are worth a look here too. <code>ssh-keygen -t ed25519-sk<\/code> creates a key bound to a FIDO2 security token, so the private half physically cannot be copied off the device. Enrol one as your recovery key and store it somewhere safe.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Keep your provider&#8217;s console or recovery mode in mind as the last resort. It is slower than SSH, but it is how you add new SSH keys when every other route is gone.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Managing_SSH_keys_across_a_team_and_multiple_servers\"><\/span>Managing SSH keys across a team and multiple servers<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">A single <code>authorized_keys<\/code> file scales further than people expect, because it holds one line per key. Adding a colleague means appending their public key; removing them means deleting that line. No shared secret ever changes hands, and nobody has to be told a new password.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Two habits keep this manageable once you pass a handful of machines. First, give every person and every device its own key rather than copying one private key around \u2014 shared SSH keys cannot be revoked without locking out everyone who uses them. Second, audit periodically. <a href=\"https:\/\/csrc.nist.gov\/pubs\/ir\/7966\/final\" target=\"_blank\" rel=\"noopener\">NIST&#8217;s guidance on SSH key management<\/a> exists precisely because abandoned SSH keys for departed staff are one of the most common ways access outlives employment.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">You can also restrict what a key is allowed to do. Prefixing a line in <code>authorized_keys<\/code> with options such as <code>from=\"203.0.113.0\/24\"<\/code> or <code>command=\"\/usr\/local\/bin\/backup.sh\"<\/code> limits that key to one source network or one command \u2014 ideal for automation credentials that should never get an interactive shell.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">For a quick health check, <a href=\"https:\/\/github.com\/jtesta\/ssh-audit\" target=\"_blank\" rel=\"noopener\">ssh-audit<\/a> grades the algorithms and settings your server actually advertises and flags anything weak. Canonical&#8217;s <a href=\"https:\/\/documentation.ubuntu.com\/server\/how-to\/security\/openssh-server\/\" target=\"_blank\" rel=\"noopener\">OpenSSH server documentation<\/a> is the reference for anything Ubuntu-specific, and Red Hat&#8217;s <a href=\"https:\/\/docs.redhat.com\/en\/documentation\/red_hat_enterprise_linux\/9\/html\/using_selinux\/index\" target=\"_blank\" rel=\"noopener\">SELinux documentation<\/a> covers the context rules behind the RHEL-family fix above.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">If you run more than a few boxes, add key deployment to your provisioning step so a new server is never password-accessible even briefly.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Frequently_asked_questions\"><\/span>Frequently asked questions<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Can I use the same SSH keys on several servers?<\/strong> Yes, and for one person that is normal practice. One key per device, reused across your fleet, is a reasonable balance. What you should not do is share a private key between people.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do I need to generate separate SSH keys for Ubuntu and AlmaLinux servers?<\/strong> No. A key pair is just maths \u2014 the same public key works on every distribution. Only the server-side service name and SELinux handling differ.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>What happens if I lose my private key?<\/strong> You lose access through that key. Recovery means console access from your provider&#8217;s panel, where you add a new public key to <code>authorized_keys<\/code>. This is the argument for enrolling a backup key from a second device before you need it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Do SSH keys replace a firewall and Fail2Ban?<\/strong> No. They stop credential guessing, but they do not reduce log noise or protect other services. Keep UFW or firewalld restrictive and leave Fail2Ban running.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Is RSA actually insecure now?<\/strong> No \u2014 RSA at 4096 bits remains sound. Ed25519 is simply smaller, faster, and less prone to implementation mistakes, which is why it is the default recommendation.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>How do I set up SSH keys on Ubuntu for a user I did not create?<\/strong> Identically. Log in as that user, or append to their <code>authorized_keys<\/code> as root, then <code>chown<\/code> the file back to them. That ownership step is easy to forget and will break the login every time.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\"><span class=\"ez-toc-section\" id=\"Wrapping_up\"><\/span>Wrapping up<span class=\"ez-toc-section-end\"><\/span><\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The full sequence to set up SSH keys on Ubuntu is short: generate an Ed25519 pair locally, protect it with a passphrase, install the public half with <code>ssh-copy-id<\/code>, confirm the login works from a second terminal, then disable passwords and verify the refusal. Ten minutes once, and your server stops being a target worth grinding at.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">On Debian the steps are identical. On AlmaLinux, Rocky Linux and Fedora, remember <code>sshd<\/code> instead of <code>ssh<\/code> and run <code>restorecon -R ~\/.ssh<\/code> if a key that should work does not.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Do it on the first boot of every new machine, before the box is reachable with a password at all. Every NodeGuard <a href=\"https:\/\/nodeguard.io\/cloud-vps\">Cloud VPS<\/a> lets you attach SSH keys at deployment, so the server comes up key-only from its very first second.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-block-paragraph\"><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Passwords are the weakest part of most servers. Bots find your IP within hours of provisioning, then grind through credential lists around\u2026<\/p>\n","protected":false},"author":2,"featured_media":159,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[18,24],"tags":[9,44,8,79,78,83,49,81,11,82,75,72,43,4,7,41,80],"class_list":["post-156","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-linux","category-security","tag-almalinux","tag-cloud-vps","tag-debian","tag-ed25519","tag-fedora","tag-hardening","tag-linux","tag-passwordless-login","tag-rocky-linux","tag-selinux","tag-server-security","tag-ssh","tag-ssh-keys","tag-system-administration","tag-ubuntu","tag-ubuntu-server","tag-vps"],"_links":{"self":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts\/156","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/comments?post=156"}],"version-history":[{"count":1,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts\/156\/revisions"}],"predecessor-version":[{"id":160,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/posts\/156\/revisions\/160"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/media\/159"}],"wp:attachment":[{"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/media?parent=156"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/categories?post=156"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/nodeguard.io\/blog\/wp-json\/wp\/v2\/tags?post=156"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}