{"id":308,"date":"2026-05-13T06:58:01","date_gmt":"2026-05-13T14:58:01","guid":{"rendered":"https:\/\/rainier-it.com\/blog\/?p=308"},"modified":"2026-05-13T06:58:05","modified_gmt":"2026-05-13T14:58:05","slug":"lock-down-ssh-in-10-minutes-a-hardening-checklist-for-linux","status":"publish","type":"post","link":"https:\/\/rainier-it.com\/blog\/lock-down-ssh-in-10-minutes-a-hardening-checklist-for-linux\/","title":{"rendered":"Lock Down SSH in 10 Minutes: A Hardening Checklist for Linux"},"content":{"rendered":"\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p>If you put a server on the public internet with port 22 open and password login enabled, you are going to get knocked on. Not maybe \u2014 <em>guaranteed<\/em>. The bots find you within minutes, and they never sleep. The good news: locking SSH down properly takes about ten minutes and roughly six lines of config.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h1 class=\"wp-block-heading\">\ud83d\udd10 Lock Down SSH in 10 Minutes: A Hardening Checklist<\/h1>\n\n\n\n<p>This is the same config I put on every Linux box I run \u2014 from a $5 VPS to the Proxmox cluster in my office. By the end of this post you&#8217;ll have key-only authentication, no root login, a non-default port, a whitelisted user group, fail2ban, and a workflow that won&#8217;t lock you out of your own server.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udccb Prerequisites<\/h2>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>A Linux server<\/strong> you can already SSH into (Debian\/Ubuntu in the examples \u2014 same idea on RHEL\/Alma\/Rocky).<\/li>\n\n\n\n<li><strong>A non-root user<\/strong> with <code>sudo<\/code> rights. If you&#8217;ve been logging in as root, create one first (<code>adduser christopher &amp;&amp; usermod -aG sudo christopher<\/code>).<\/li>\n\n\n\n<li><strong>A second terminal<\/strong> ready before you touch sshd. This is the rule that has saved me more times than I can count.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udddd\ufe0f Step 1: Get on Keys, Get Off Passwords<\/h2>\n\n\n\n<p>Passwords are the single biggest reason servers get popped. Even a 16-character password is brute-force-able if a bot has a year and you&#8217;re not rate-limiting. SSH keys aren&#8217;t \u2014 a 256-bit ed25519 key is, for practical purposes, uncrackable.<\/p>\n\n\n\n<p>Generate one on your <em>laptop<\/em> (not the server), then copy the public half up:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly># Generate a modern ed25519 key (faster, smaller, stronger than RSA)\nssh-keygen -t ed25519 -C \"christopher@laptop\"\n\n# Copy the public key to your server (still using password \u2014 for now)\nssh-copy-id -p 22 christopher@your-server.example.com\n\n# Test that key-based login works BEFORE you disable passwords\nssh christopher@your-server.example.com<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #616E88\"># Generate a modern ed25519 key (faster, smaller, stronger than RSA)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ssh-keygen<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-t<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ed25519<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-C<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #ECEFF4\">&quot;<\/span><span style=\"color: #A3BE8C\">christopher@laptop<\/span><span style=\"color: #ECEFF4\">&quot;<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Copy the public key to your server (still using password \u2014 for now)<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ssh-copy-id<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-p<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">22<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">christopher@your-server.example.com<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Test that key-based login works BEFORE you disable passwords<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ssh<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">christopher@your-server.example.com<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>If <code>ssh-copy-id<\/code> isn&#8217;t available, append the contents of <code>~\/.ssh\/id_ed25519.pub<\/code> to <code>~\/.ssh\/authorized_keys<\/code> on the server manually. <strong>Don&#8217;t skip the test login<\/strong> \u2014 verify keys work <em>before<\/em> you disable passwords below.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udee1\ufe0f Step 2: The Six-Line Hardening File<\/h2>\n\n\n\n<p>On modern OpenSSH you don&#8217;t edit <code>\/etc\/ssh\/sshd_config<\/code> directly \u2014 you drop a file in <code>\/etc\/ssh\/sshd_config.d\/<\/code> and it gets included automagically. That way distro upgrades don&#8217;t trample your changes.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly># \/etc\/ssh\/sshd_config.d\/99-hardening.conf\n# Drop this file in the .d\/ directory \u2014 it overrides the main sshd_config.\n\n# 1. No passwords. Keys only.\nPasswordAuthentication no\nKbdInteractiveAuthentication no\nPermitEmptyPasswords no\nChallengeResponseAuthentication no\n\n# 2. No root over SSH. Log in as a normal user and sudo.\nPermitRootLogin no\n\n# 3. Move off port 22 \u2014 kills 99% of drive-by bots.\nPort 2222\n\n# 4. Whitelist who can SSH in (group is easier to manage than user list).\nAllowGroups ssh-users\n\n# 5. Idle session timeout (server kicks dead sessions after ~15 min).\nClientAliveInterval 300\nClientAliveCountMax 2\n\n# 6. Limit retries before disconnect.\nMaxAuthTries 3\nLoginGraceTime 30<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #616E88\"># \/etc\/ssh\/sshd_config.d\/99-hardening.conf<\/span><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Drop this file in the .d\/ directory \u2014 it overrides the main sshd_config.<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># 1. No passwords. Keys only.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">PasswordAuthentication<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">no<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">KbdInteractiveAuthentication<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">no<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">PermitEmptyPasswords<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">no<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ChallengeResponseAuthentication<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">no<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># 2. No root over SSH. Log in as a normal user and sudo.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">PermitRootLogin<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">no<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># 3. Move off port 22 \u2014 kills 99% of drive-by bots.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">Port<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2222<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># 4. Whitelist who can SSH in (group is easier to manage than user list).<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">AllowGroups<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ssh-users<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># 5. Idle session timeout (server kicks dead sessions after ~15 min).<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ClientAliveInterval<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">300<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ClientAliveCountMax<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># 6. Limit retries before disconnect.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">MaxAuthTries<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">3<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">LoginGraceTime<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">30<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>What each setting does:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>PasswordAuthentication no<\/strong> \u2014 bots can&#8217;t try <code>admin\/admin<\/code> anymore. They&#8217;ll still knock, but they&#8217;ll fail instantly.<\/li>\n\n\n\n<li><strong>PermitRootLogin no<\/strong> \u2014 even if someone steals your root password, they can&#8217;t use it for SSH. They&#8217;d need a key in <code>\/root\/.ssh\/authorized_keys<\/code>, which shouldn&#8217;t exist.<\/li>\n\n\n\n<li><strong>Port 2222<\/strong> \u2014 this isn&#8217;t real security (it&#8217;s &#8220;security through obscurity&#8221;), but it cuts log noise by ~99% because the drive-by scanners only check 22. Pick anything between 1024 and 65535 that isn&#8217;t already used.<\/li>\n\n\n\n<li><strong>AllowGroups ssh-users<\/strong> \u2014 only users in this group can SSH in at all. Now you can <code>adduser someone<\/code> for sudo without giving them remote access.<\/li>\n\n\n\n<li><strong>ClientAliveInterval \/ CountMax<\/strong> \u2014 closes dead sessions so you don&#8217;t have ghost connections eating slots.<\/li>\n\n\n\n<li><strong>MaxAuthTries 3<\/strong> \u2014 three failed auths and the connection drops. Pairs well with fail2ban below.<\/li>\n<\/ul>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83e\uddea Step 3: Apply It Without Locking Yourself Out<\/h2>\n\n\n\n<p>This is where people brick their own server. Do these in order:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly># Validate the config BEFORE you restart \u2014 a typo here can lock you out.\nsudo sshd -t\n\n# Add yourself to the whitelist group.\nsudo groupadd ssh-users 2>\/dev\/null\nsudo usermod -aG ssh-users $USER\n\n# Open the new port in the firewall BEFORE reloading sshd.\nsudo ufw allow 2222\/tcp\nsudo ufw delete allow 22\/tcp   # only after you've tested 2222\n\n# Reload sshd \u2014 this keeps your current session alive.\nsudo systemctl reload ssh<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #616E88\"># Validate the config BEFORE you restart \u2014 a typo here can lock you out.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">sshd<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-t<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Add yourself to the whitelist group.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">groupadd<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ssh-users<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">2&gt;<\/span><span style=\"color: #A3BE8C\">\/dev\/null<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">usermod<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-aG<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ssh-users<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #D8DEE9\">$USER<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Open the new port in the firewall BEFORE reloading sshd.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ufw<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">allow<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2222<\/span><span style=\"color: #A3BE8C\">\/tcp<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ufw<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">delete<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">allow<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">22<\/span><span style=\"color: #A3BE8C\">\/tcp<\/span><span style=\"color: #D8DEE9FF\">   <\/span><span style=\"color: #616E88\"># only after you&#39;ve tested 2222<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Reload sshd \u2014 this keeps your current session alive.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">systemctl<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">reload<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">ssh<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>Now \u2014 <strong>do not close your existing SSH session.<\/strong> Open a brand-new terminal and try to connect on the new port:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly># In a SECOND terminal \u2014 keep your existing SSH session open.\n# If you can log in on the new port, you're safe to close the old one.\nssh -p 2222 christopher@your-server.example.com<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #616E88\"># In a SECOND terminal \u2014 keep your existing SSH session open.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># If you can log in on the new port, you&#39;re safe to close the old one.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">ssh<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-p<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2222<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">christopher@your-server.example.com<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>If that works, great \u2014 close the old session. If it doesn&#8217;t, you still have the old session to fix whatever broke. The number-one cause is forgetting to open the new port in the firewall <em>before<\/em> reloading sshd.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udea7 Step 4: fail2ban for Everything Else<\/h2>\n\n\n\n<p>Keys + non-default port + AllowGroups stops the vast majority of attacks at the door. fail2ban handles the rest: anybody who hits <code>MaxAuthTries<\/code> three times gets their IP firewalled for an hour. Free, painless, no false positives if your config is right.<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly># Install fail2ban for everything bots still try (script kiddies, password sprays).\nsudo apt install -y fail2ban\n\n# Drop a jail config for sshd. Use the new port you set above.\nsudo tee \/etc\/fail2ban\/jail.d\/sshd.local >\/dev\/null &lt;&lt;'EOF'\n&#91;sshd&#93;\nenabled  = true\nport     = 2222\nmaxretry = 3\nfindtime = 10m\nbantime  = 1h\nEOF\n\nsudo systemctl enable --now fail2ban\nsudo fail2ban-client status sshd<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #616E88\"># Install fail2ban for everything bots still try (script kiddies, password sprays).<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">apt<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">install<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">-y<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">fail2ban<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #616E88\"># Drop a jail config for sshd. Use the new port you set above.<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">tee<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">\/etc\/fail2ban\/jail.d\/sshd.local<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">&gt;<\/span><span style=\"color: #A3BE8C\">\/dev\/null<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #81A1C1\">&lt;&lt;<\/span><span style=\"color: #D8DEE9FF\">&#39;<\/span><span style=\"color: #ECEFF4\">EOF<\/span><span style=\"color: #D8DEE9FF\">&#39;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">&#91;sshd&#93;<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">enabled  = true<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">port     = 2222<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">maxretry = 3<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">findtime = 10m<\/span><\/span>\n<span class=\"line\"><span style=\"color: #A3BE8C\">bantime  = 1h<\/span><\/span>\n<span class=\"line\"><span style=\"color: #ECEFF4\">EOF<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">systemctl<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">enable<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">--now<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">fail2ban<\/span><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">sudo<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">fail2ban-client<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">status<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">sshd<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>Run <code>sudo fail2ban-client status sshd<\/code> any time to see who&#8217;s currently banned. On a fresh server with port 22 closed, this list stays empty for weeks. On port 22 it&#8217;d be ten IPs deep within an hour.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\ud83d\udcbb Bonus: A Sane Client Config<\/h2>\n\n\n\n<p>Now that your server is on a non-default port, you don&#8217;t want to type <code>ssh -p 2222 christopher@host<\/code> a dozen times a day. Drop an entry in <code>~\/.ssh\/config<\/code> on your laptop:<\/p>\n\n\n\n<div class=\"wp-block-kevinbatdorf-code-block-pro\" data-code-block-pro-font-family=\"Code-Pro-JetBrains-Mono\" style=\"font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)\"><span style=\"display:block;padding:16px 0 0 16px;margin-bottom:-1px;width:100%;text-align:left;background-color:#2e3440ff\"><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"54\" height=\"14\" viewBox=\"0 0 54 14\"><g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(1 1)\"><circle cx=\"6\" cy=\"6\" r=\"6\" fill=\"#FF5F56\" stroke=\"#E0443E\" stroke-width=\".5\"><\/circle><circle cx=\"26\" cy=\"6\" r=\"6\" fill=\"#FFBD2E\" stroke=\"#DEA123\" stroke-width=\".5\"><\/circle><circle cx=\"46\" cy=\"6\" r=\"6\" fill=\"#27C93F\" stroke=\"#1AAB29\" stroke-width=\".5\"><\/circle><\/g><\/svg><\/span><span role=\"button\" tabindex=\"0\" style=\"color:#d8dee9ff;display:none\" aria-label=\"Copy\" class=\"code-block-pro-copy-button\"><pre class=\"code-block-pro-copy-button-pre\" aria-hidden=\"true\"><textarea class=\"code-block-pro-copy-button-textarea\" tabindex=\"-1\" aria-hidden=\"true\" readonly># ~\/.ssh\/config on your laptop \u2014 so you don't have to type -p 2222 every time.\n\nHost pegasus\n    HostName pegasus.example.com\n    Port 2222\n    User christopher\n    IdentityFile ~\/.ssh\/id_ed25519\n    IdentitiesOnly yes\n    ServerAliveInterval 60<\/textarea><\/pre><svg xmlns=\"http:\/\/www.w3.org\/2000\/svg\" style=\"width:24px;height:24px\" fill=\"none\" viewBox=\"0 0 24 24\" stroke=\"currentColor\" stroke-width=\"2\"><path class=\"with-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4\"><\/path><path class=\"without-check\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2\"><\/path><\/svg><\/span><pre class=\"shiki nord\" style=\"background-color: #2e3440ff\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color: #616E88\"># ~\/.ssh\/config on your laptop \u2014 so you don&#39;t have to type -p 2222 every time.<\/span><\/span>\n<span class=\"line\"><\/span>\n<span class=\"line\"><span style=\"color: #88C0D0\">Host<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">pegasus<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">HostName<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">pegasus.example.com<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">Port<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">2222<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">User<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">christopher<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">IdentityFile<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">~\/.ssh\/id_ed25519<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">IdentitiesOnly<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #A3BE8C\">yes<\/span><\/span>\n<span class=\"line\"><span style=\"color: #D8DEE9FF\">    <\/span><span style=\"color: #88C0D0\">ServerAliveInterval<\/span><span style=\"color: #D8DEE9FF\"> <\/span><span style=\"color: #B48EAD\">60<\/span><\/span><\/code><\/pre><\/div>\n\n\n\n<p>Now <code>ssh pegasus<\/code> uses the right user, key, and port automatically. Add one block per server you care about.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<h2 class=\"wp-block-heading\">\u2705 The Checklist<\/h2>\n\n\n\n<ol start=\"1\" class=\"wp-block-list\">\n<li>Generate an <strong>ed25519 key<\/strong> on your laptop and copy it to the server.<\/li>\n\n\n\n<li>Verify <strong>key-based login works<\/strong> before changing anything else.<\/li>\n\n\n\n<li>Drop <code>99-hardening.conf<\/code> into <code>\/etc\/ssh\/sshd_config.d\/<\/code>.<\/li>\n\n\n\n<li><code>sshd -t<\/code> to validate, <strong>open the new port in the firewall<\/strong>, then <code>systemctl reload ssh<\/code>.<\/li>\n\n\n\n<li><strong>Test the new port from a second terminal<\/strong> before closing your existing session.<\/li>\n\n\n\n<li>Install <strong>fail2ban<\/strong> with a jail pointed at the new port.<\/li>\n\n\n\n<li>Add a matching block to <code>~\/.ssh\/config<\/code> on every client you use.<\/li>\n<\/ol>\n\n\n\n<p><strong>You&#8217;re done!<\/strong> Your server is now harder to break into than 99% of what&#8217;s on the public internet, and the noise in <code>\/var\/log\/auth.log<\/code> just dropped to almost nothing.<\/p>\n\n\n\n<p>Next post in this series I&#8217;ll cover the things you do <em>after<\/em> the basics: SSH certificates (so you can revoke keys across a fleet without touching every server), 2FA via TOTP, and how to wire all of this into Ansible so a fresh VPS is hardened in one playbook run. If you&#8217;ve got a specific SSH question \u2014 or a story about the time you locked yourself out \u2014 I&#8217;d love to hear it.<\/p>\n\n\n\n<p>Thanks!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Key-only auth, no root, a non-default port, fail2ban, and a workflow that won&#8217;t lock you out \u2014 every box I run gets this same six-line hardening file.<\/p>\n","protected":false},"author":1,"featured_media":315,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[9,6,19,7,10],"tags":[],"class_list":["post-308","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-best-practices","category-cybersecurity","category-guides","category-it-management","category-linux"],"_links":{"self":[{"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/posts\/308","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/comments?post=308"}],"version-history":[{"count":4,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/posts\/308\/revisions"}],"predecessor-version":[{"id":314,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/posts\/308\/revisions\/314"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/media\/315"}],"wp:attachment":[{"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/media?parent=308"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/categories?post=308"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/rainier-it.com\/blog\/wp-json\/wp\/v2\/tags?post=308"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}