<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Beginner – Serverküche</title><link>https://serverkueche.de/en/tags/beginner/</link><description>Beginner – Neueste Beiträge von Serverküche</description><generator>Hugo</generator><language>en-US</language><managingEditor>feedback@serverkueche.de (Serverküche)</managingEditor><webMaster>feedback@serverkueche.de (Serverküche)</webMaster><copyright>2026 Serverküche</copyright><lastBuildDate>Sat, 18 Jul 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://serverkueche.de/en/tags/beginner/index.xml" rel="self" type="application/rss+xml"/><item><title>unattended-upgrades: automatic security updates for Debian</title><link>https://serverkueche.de/en/tutorials/unattended-upgrades-automatic-updates/</link><pubDate>Sat, 18 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/unattended-upgrades-automatic-updates/</guid><description>Debian installs security updates automatically: set up unattended-upgrades, control the reboot, and verify it really runs.</description><content:encoded><![CDATA[<p>&ldquo;Remember to install updates weekly&rdquo; – this sentence from the maintenance sections of
the other tutorials is the first one people forget. That&rsquo;s exactly why the server now
takes it over itself: <strong>unattended-upgrades</strong> installs security updates automatically.
That closes the most dangerous gap in self-hosting – the server that runs unpatched for
months.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, your <strong>Debian 13</strong> automatically pulls <strong>security updates</strong> daily and
installs them without your involvement. You decide whether and when the server reboots
for necessary kernel updates, and you know how to verify that the automation really
kicks in. What gets automated are security updates and the conservative stable point
releases; larger upgrades of your other software stay deliberately in your hands.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="/en/tutorials/harden-ssh/">hardened server</a> with an
<a href="/en/tutorials/firewall-ufw-setup/">active firewall</a></li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-install-the-package">Step 1: Install the package</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install -y unattended-upgrades apt-listchanges</span></span></code></pre></div>
</div>
<p><code>apt-listchanges</code> shows the changelogs on updates – useful if you later do check
manually what changed.</p>
<h3 id="step-2-enable-the-automation">Step 2: Enable the automation</h3>
<p>The simplest way to switch on the daily run:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo dpkg-reconfigure -plow unattended-upgrades</span></span></code></pre></div>
</div>
<p>Choose <strong>Yes</strong> in the dialog. That creates the file
<code>/etc/apt/apt.conf.d/20auto-upgrades</code> with this content:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">APT::Periodic::Update-Package-Lists &#34;1&#34;;</span>
</span></span><span class="line"><span class="cl"><span class="na">APT::Periodic::Unattended-Upgrade &#34;1&#34;;</span></span></span></code></pre></div>
</div>
<p>The <code>1</code> means &ldquo;daily&rdquo;: update package lists <strong>and</strong> run unattended upgrades. It&rsquo;s
triggered via the systemd timers <code>apt-daily.timer</code> (package lists) and
<code>apt-daily-upgrade.timer</code> (upgrade run) – no separate cron job needed.</p>
<h3 id="step-3-define-what-gets-updated">Step 3: Define what gets updated</h3>
<p>Take a look at <code>/etc/apt/apt.conf.d/50unattended-upgrades</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/apt/apt.conf.d/50unattended-upgrades</span></span></code></pre></div>
</div>
<p>On Debian 13, <strong>three</strong> sources are active in the <code>Origins-Pattern</code> block by default:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">Unattended-Upgrade::Origins-Pattern {</span>
</span></span><span class="line"><span class="cl">  <span class="na">&#34;origin</span><span class="o">=</span><span class="s">Debian,codename=${distro_codename},label=Debian&#34;;
</span></span></span><span class="line"><span class="cl"><span class="s">  &#34;origin=Debian,codename=${distro_codename},label=Debian-Security&#34;;
</span></span></span><span class="line"><span class="cl"><span class="s">  &#34;origin=Debian,codename=${distro_codename}-security,label=Debian-Security&#34;;</span>
</span></span><span class="line"><span class="cl"><span class="na">};</span></span></span></code></pre></div>
</div>
<p>The two <code>Debian-Security</code> lines ensure <strong>timely security updates</strong> – the actual
purpose. The first line (<code>label=Debian</code>) covers the <strong>conservative stable updates</strong>
that only arrive with Debian point releases; so it&rsquo;s not a risky rolling update, but
well-seasoned. If you really want <strong>only</strong> security updates, comment out the first line
with <code>//</code>.</p>
<p>Two options are worth setting explicitly here. <strong>Clean up old kernels/packages
automatically</strong>, otherwise <code>/boot</code> eventually fills up (<code>Remove-Unused-Kernel-Packages</code>
is already the default in code, but only appears commented out in the file – set
explicitly, the decision is documented):</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">Unattended-Upgrade::Remove-Unused-Kernel-Packages &#34;true&#34;;</span>
</span></span><span class="line"><span class="cl"><span class="na">Unattended-Upgrade::Remove-Unused-Dependencies &#34;true&#34;;</span></span></span></code></pre></div>
</div>
<p>And the most important point – the <strong>automatic reboot</strong>:</p>
<div class="sk-code">
  <span class="sk-code-head">INI</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ini" data-lang="ini"><span class="line"><span class="cl"><span class="na">Unattended-Upgrade::Automatic-Reboot &#34;true&#34;;</span>
</span></span><span class="line"><span class="cl"><span class="na">Unattended-Upgrade::Automatic-Reboot-Time &#34;04:00&#34;;</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Automatic reboot – decide deliberately
  </p>
  <div class="prose-kitchen text-sm">Some security updates (especially kernel) only take effect after a reboot. With
<code>Automatic-Reboot &quot;true&quot;</code> the server then reboots on its own – <strong>always set an
<code>Automatic-Reboot-Time</code></strong> when you do: without it, the server reboots <strong>immediately</strong>
after the upgrade run (the code default is <code>&quot;now&quot;</code>; the <code>02:00</code> in the example file is
only a commented-out suggestion). Choose a time with low usage. If you want <strong>no</strong>
automatic reboots, leave the option at <code>&quot;false&quot;</code> and reboot yourself when
<code>/var/run/reboot-required</code> exists. Both are defensible – it just has to be a deliberate
decision.</div>
</div>
<p>For the automatic reboot to be able to trigger on Debian at all, one building block is
still missing: unattended-upgrades only reboots if the marker file
<code>/var/run/reboot-required</code> exists – and Debian 13 does <strong>not</strong> create it on kernel
updates by default (the responsible hook comes from an Ubuntu helper package that
doesn&rsquo;t exist in Debian; only individual packages like <code>dbus</code> set the file themselves).
A two-liner closes the gap:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo tee /etc/kernel/postinst.d/zz-reboot-required &gt; /dev/null <span class="s">&lt;&lt;&#39;EOF&#39;
</span></span></span><span class="line"><span class="cl"><span class="s">#!/bin/sh
</span></span></span><span class="line"><span class="cl"><span class="s">touch /var/run/reboot-required
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</span>
</span></span><span class="line"><span class="cl">sudo chmod +x /etc/kernel/postinst.d/zz-reboot-required</span></span></code></pre></div>
</div>
<p>The hook runs after every kernel installation and sets the marker file. From now on,
both <code>Automatic-Reboot</code> and the manual check of <code>/var/run/reboot-required</code> work
reliably – without the hook, the automatic reboot would simply never trigger on kernel
updates.</p>
<h3 id="step-4-do-a-dry-run">Step 4: Do a dry run</h3>
<p>Test what unattended-upgrades would do, without a real installation:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo unattended-upgrade --dry-run --debug</span></span></code></pre></div>
</div>
<p>In the debug output, this line is decisive (on a fresh Debian 13, shortened):</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Allowed origins are: origin=Debian,codename=trixie,label=Debian, origin=Debian,codename=trixie,label=Debian-Security, origin=Debian,codename=trixie-security,label=Debian-Security
</span></span><span class="line"><span class="cl">[...]
</span></span><span class="line"><span class="cl">No packages found that can be upgraded unattended and no pending auto-removals</span></span></code></pre></div>
</div>
<p>The Debian-Security sources must appear there. If something is listed, the
configuration applies; if the run finds nothing to do (as above), there&rsquo;s simply no
security update pending right now.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>The dry run reports <code>No packages found that can be upgraded unattended</code>.</strong> Usually
perfectly normal – no security update is currently pending. Check the configuration
anyway via the <code>Allowed origins are:</code> line in the <code>--debug</code> run. If no security origins
appear there, the <code>Origins-Pattern</code> from step 3 isn&rsquo;t right.</p>
<p><strong>Updates come, but the server never reboots despite a kernel update.</strong> Either
<code>Automatic-Reboot</code> is set to <code>&quot;false&quot;</code> (default) – then set it to <code>&quot;true&quot;</code> (step 3) –
or the marker file is never set because the kernel hook
<code>/etc/kernel/postinst.d/zz-reboot-required</code> from step 3 is missing. Without it, Debian
doesn&rsquo;t create <code>/var/run/reboot-required</code> on kernel updates, and the automatic reboot
never triggers.</p>
<p><strong><code>/boot</code> fills up, updates fail.</strong> Old kernels pile up. Set
<code>Remove-Unused-Kernel-Packages &quot;true&quot;</code> (step 3); clean up once with <code>sudo apt autoremove --purge</code>.</p>
<p><strong>A package is stubbornly held back (<code>kept back</code>).</strong> unattended-upgrades doesn&rsquo;t
install updates that would remove other packages. You resolve such cases deliberately
by hand with <code>sudo apt upgrade</code> and check what happens.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Still check anyway:</strong> automation doesn&rsquo;t replace attention. Take a monthly look at
the log <code>/var/log/unattended-upgrades/unattended-upgrades.log</code> and occasionally run
<code>sudo apt update &amp;&amp; sudo apt upgrade</code> for the <strong>non</strong>-security-critical updates.</li>
<li><strong>Plan for reboots:</strong> if you use automatic reboots, make sure your services survive a
reboot cleanly (<code>restart: unless-stopped</code> in <a href="/en/tutorials/docker-compose-basics/">Docker
Compose</a> if you run containers).</li>
<li><strong>No dedicated backup needed</strong>, but note whether you enabled automatic reboots – it
later explains why the server was briefly gone at night.</li>
</ul>
]]></content:encoded></item><item><title>Connecting a domain to your server (DNS basics)</title><link>https://serverkueche.de/en/tutorials/connect-domain-to-server/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/connect-domain-to-server/</guid><description>A and AAAA records, TTL and propagation explained clearly: how your own domain points to your server – the prerequisite for HTTPS with Traefik.</description><content:encoded><![CDATA[<p>So far you only reach your server via its IP address. For real services you need a
<strong>domain</strong> – and not just for looks: without a publicly resolving domain,
<a href="/en/tutorials/traefik-reverse-proxy/">Traefik</a> can&rsquo;t fetch a <strong>TLS certificate</strong>
from Let&rsquo;s Encrypt later. This tutorial connects your domain to the server and
explains the three terms beginners get stuck on: A record, TTL and propagation.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, your domain (<code>YOUR_DOMAIN</code>) points to your server IP via an <strong>A record</strong>
(IPv4) and – if your server has IPv6 – an <strong>AAAA record</strong>, including <code>www</code>. You can
verify this yourself with <code>dig</code> and understand why a change isn&rsquo;t always visible
immediately. Optionally you also set up <strong>reverse DNS (PTR)</strong> – important as soon as
your server sends emails. This lays the foundation for all publicly reachable
services.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="/en/tutorials/first-steps-netcup-vps/">set-up server</a> with a known public IP
address</li>
<li>A <strong>registered domain</strong> (at netcup, another registrar, or included in your hosting
package)</li>
<li>Access to the <strong>DNS management</strong> of your domain (at netcup: the customer account
CCP)</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-find-the-server-ip">Step 1: Find the server IP</h3>
<p>Connect via SSH and display the public IP addresses:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ip -4 addr show scope global
</span></span><span class="line"><span class="cl">ip -6 addr show scope global</span></span></code></pre></div>
</div>
<p>The IPv4 address is after <code>inet</code> (e.g. <code>203.0.113.10</code>), any IPv6 address after
<code>inet6</code> (does not start with <code>fe80::</code>, that would only be link-local). Alternatively
you&rsquo;ll find both in the netcup <strong>Server Control Panel (SCP)</strong> in the server overview.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">Note down both addresses. If your server has <strong>no</strong> global IPv6 address, you simply
don&rsquo;t create an AAAA record later – that&rsquo;s perfectly fine.</div>
</div>
<h3 id="step-2-create-a-and-aaaa-records">Step 2: Create A and AAAA records</h3>
<p>Open the DNS management of your domain. There you enter <strong>resource records</strong> – the
mapping of names to addresses. Create:</p>
<table>
	<thead>
			<tr>
					<th>Type</th>
					<th>Host / Name</th>
					<th>Value</th>
			</tr>
	</thead>
	<tbody>
			<tr>
					<td><code>A</code></td>
					<td><code>@</code></td>
					<td><code>YOUR_SERVER_IPV4</code></td>
			</tr>
			<tr>
					<td><code>A</code></td>
					<td><code>www</code></td>
					<td><code>YOUR_SERVER_IPV4</code></td>
			</tr>
			<tr>
					<td><code>AAAA</code></td>
					<td><code>@</code></td>
					<td><code>YOUR_SERVER_IPV6</code> <em>(only if available)</em></td>
			</tr>
			<tr>
					<td><code>AAAA</code></td>
					<td><code>www</code></td>
					<td><code>YOUR_SERVER_IPV6</code> <em>(only if available)</em></td>
			</tr>
	</tbody>
</table>
<ul>
<li><strong><code>@</code></strong> stands for the domain itself (<code>YOUR_DOMAIN</code>), <code>www</code> for the subdomain
<code>www.YOUR_DOMAIN</code>.</li>
<li><strong>A</strong> points to an IPv4 address, <strong>AAAA</strong> to an IPv6 address. The name &ldquo;AAAA&rdquo; comes
from an IPv6 address being four times as long as an IPv4 address.</li>
<li>For individual services you can later create specific subdomains (e.g. <code>cloud</code>,
<code>git</code>) – following the same pattern.</li>
</ul>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">For A/AAAA enter <strong>only the bare IP</strong> – no <code>http://</code>, no slash, no port. For
redirects to other names there would be <code>CNAME</code>, but for &ldquo;domain → own server&rdquo; the
A/AAAA record is the right and most robust way.</div>
</div>
<h3 id="step-3-understand-ttl-and-propagation">Step 3: Understand TTL and propagation</h3>
<p>Every record has a <strong>TTL</strong> (Time To Live) in seconds – e.g. <code>3600</code> (one hour). It
tells resolvers worldwide <strong>how long</strong> they may cache the answer before asking again.</p>
<p>From this follows the notorious <strong>propagation</strong>: if you change a record, some
resolvers see the new value immediately, others only after the old TTL expires. &ldquo;DNS
propagating&rdquo; means nothing more than these caches expiring one by one. In everyday
use, reckon with <strong>minutes up to an hour</strong>, occasionally longer.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Before planned migrations
  </p>
  <div class="prose-kitchen text-sm">If you want to change an IP soon, lower the TTL <strong>one or two days beforehand</strong> to
e.g. <code>300</code> (5 minutes). Then the actual switch takes effect almost immediately later.
Raise it again after the migration.</div>
</div>
<h3 id="step-4-verify-with-dig">Step 4: Verify with dig</h3>
<p>Wait a few minutes, then check from your own machine where the domain points. <code>dig</code>
is the standard tool for that:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +noall +answer YOUR_DOMAIN A</span></span></code></pre></div>
</div>
<p>This is what a correct answer looks like (example for this site&rsquo;s real domain):</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">serverkueche.de.	600	IN	A	46.38.243.234</span></span></code></pre></div>
</div>
<p>The number <code>600</code> is the remaining TTL, followed by the target IP. For IPv6, do the
same with <code>AAAA</code>. If you only want it very brief:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short YOUR_DOMAIN</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">46.38.243.234</span></span></code></pre></div>
</div>
<p>If your correct server IP appears, the domain is connected. Also check <code>www</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short www.YOUR_DOMAIN</span></span></code></pre></div>
</div>
<p>Here too, the <strong>same</strong> server IP as for the apex should come back as a plain line:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">46.38.243.234</span></span></code></pre></div>
</div>
<h3 id="step-5-set-reverse-dns-ptr--optional-but-recommended">Step 5: Set reverse DNS (PTR) – optional but recommended</h3>
<p>The A record answers the question &ldquo;Which IP belongs to this name?&rdquo;. <strong>Reverse DNS</strong> –
the PTR record – answers the opposite direction: &ldquo;Which name belongs to this IP?&rdquo;.
For a plain website you don&rsquo;t need this. But as soon as your server <strong>sends emails</strong> –
even if only notifications from Nextcloud, Gitea &amp; co. – it&rsquo;s practically mandatory:
the receiving mail servers reject mail from IPs <strong>without</strong> a matching PTR or treat it
as spam.</p>
<p>The crucial difference from the previous records: you enter the PTR <strong>not</strong> at your
DNS provider, but where the <strong>IP address is managed</strong> – i.e. at your <strong>VPS provider</strong>.
At netcup you&rsquo;ll find it in the <strong>Server Control Panel (SCP)</strong> in the <strong>Netzwerk
(Network)</strong> tab – there&rsquo;s a <strong>Reverse DNS</strong> field per address (IPv4 and IPv6). Enter a
hostname there that points to the same server via an A record – either directly
<code>YOUR_DOMAIN</code> or your own name like <code>server.YOUR_DOMAIN</code> (shown as an example in the
screenshot) – and click <strong>Save</strong>.</p>
<p><figure class="my-6"><img src="/en/tutorials/connect-domain-to-server/netcup-reverse-dns_hu_d24e02b203a775be.webp" srcset="/en/tutorials/connect-domain-to-server/netcup-reverse-dns_hu_888311629ed75749.webp 480w, /en/tutorials/connect-domain-to-server/netcup-reverse-dns_hu_d24e02b203a775be.webp 768w, /en/tutorials/connect-domain-to-server/netcup-reverse-dns_hu_1a64247f0e364cec.webp 1200w, /en/tutorials/connect-domain-to-server/netcup-reverse-dns_hu_ce3761572748486e.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/connect-domain-to-server/netcup-reverse-dns_hu_aff7b021b12d562c.webp"
    alt="netcup Server Control Panel in the Network tab: the reverse-DNS fields for the IPv4 and IPv6 address, each with an example hostname entered and a Save button" title="Reverse DNS in the netcup SCP under Network – one field per IP address"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Reverse DNS in the netcup SCP under Network – one field per IP address</figcaption></figure></p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Forward and reverse must match
  </p>
  <div class="prose-kitchen text-sm">For the PTR to count as &ldquo;clean&rdquo; (forward-confirmed rDNS, FCrDNS), the name entered
there must <strong>point back to the same IP</strong> via an A/AAAA record. For <code>YOUR_DOMAIN</code> and
<code>www</code> this is already satisfied by step 2. If you use your own name like
<code>server.YOUR_DOMAIN</code>, first create an A/AAAA record for it as in step 2 – otherwise
the reverse direction doesn&rsquo;t match.</div>
</div>
<p>If you have <strong>IPv6</strong>, set the PTR <strong>for both</strong> addresses – IPv4 <em>and</em> IPv6. The IPv6
PTR is easily forgotten, and that&rsquo;s exactly what some mail servers trip over after all.</p>
<p>You can check the result with <code>dig</code> and the <code>-x</code> option (reverse lookup) – for IPv6
do the same with <code>YOUR_SERVER_IPV6</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">dig +short -x YOUR_SERVER_IPV4</span></span></code></pre></div>
</div>
<p>Your hostname with a trailing dot should come back:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">serverkueche.de.</span></span></code></pre></div>
</div>
<p>If nothing comes back, or still the provider&rsquo;s default name (at netcup something like
<code>v220….bestsrv.de</code>), the PTR is not (correctly) set yet – or, as with every DNS entry,
the change still needs some time.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong><code>dig</code> returns no IP or a wrong (old) one.</strong> Usually still propagation. Query a
public resolver specifically to bypass your local cache: <code>dig +short YOUR_DOMAIN @1.1.1.1</code>. If it already shows the correct value, only your local/provider cache
hasn&rsquo;t expired yet – wait.</p>
<p><strong>The domain resolves, but to the wrong IP – e.g. a registrar&rsquo;s parking page.</strong> Often
an old A record or a redirect/parking setting still exists. Remove contradictory
entries; per name and type there should be exactly <strong>one</strong> value (deliberate
exceptions aside).</p>
<p><strong>IPv4 works, IPv6 (AAAA) leads to timeouts.</strong> You entered an AAAA record even though
the server has no working IPv6 address. Browsers then prefer IPv6 and run into a
timeout. remove the AAAA record until the server really supports IPv6.</p>
<p><strong>In some interfaces the value is stored with a trailing dot (<code>YOUR_DOMAIN.</code>) and
you&rsquo;re unsure.</strong> The trailing dot (root of the DNS hierarchy) is normal and correct –
many DNS managers add it automatically. No reason to worry.</p>
<p><strong>Emails sent from the server land in spam or are rejected (the mail log shows e.g.
<code>does not resolve to address</code> or <code>no PTR record</code>).</strong> The reverse-DNS entry (PTR) is
missing or doesn&rsquo;t match the sender hostname. Set the PTR as described in step 5 at
the VPS provider and make sure it points to the same name that also resolves via the
A/AAAA record.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>DNS is low-maintenance.</strong> Once set correctly, the A/AAAA record rarely changes.
Exception: a <strong>server migration</strong> – then the TTL trick from step 3 applies.</li>
<li><strong>Document your records.</strong> Keep track somewhere of which names (<code>@</code>, <code>www</code>,
<code>cloud</code>, …) point to which IP. With several services you otherwise lose the
overview.</li>
<li><strong>No classic backup needed</strong>, but a screenshot or export of your DNS zone doesn&rsquo;t
hurt in case you switch providers.</li>
<li><strong>Carry reverse DNS along on an IP change.</strong> If the server gets a new IP (migration,
new product), you have to reset the PTR at the VPS provider – it&rsquo;s tied to the IP,
not the domain.</li>
</ul>
<p>Your domain now points to the server – with that, the last prerequisite is met for
serving services <strong>publicly with automatic HTTPS</strong> via the <a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy
Traefik</a>.</p>
]]></content:encoded></item><item><title>Setting up a firewall with UFW</title><link>https://serverkueche.de/en/tutorials/firewall-ufw-setup/</link><pubDate>Fri, 17 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/firewall-ufw-setup/</guid><description>Set up a host firewall in minutes with UFW: block everything except SSH, HTTP and HTTPS on your server – without ever locking yourself out.</description><content:encoded><![CDATA[<p>By default, your server accepts connections on all open ports. A <strong>firewall</strong>
turns that around: it blocks everything and only lets through what you explicitly
allow. <strong>UFW</strong> (&ldquo;Uncomplicated Firewall&rdquo;) does this with a few, readable commands –
the perfect first line of defense.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, <strong>UFW</strong> runs on your <strong>Debian 13</strong> with the base rule &ldquo;<strong>block all
incoming</strong>&rdquo;, with only <strong>SSH</strong> (your access) plus <strong>HTTP/HTTPS</strong> (ports 80/443,
which you&rsquo;ll need later for Traefik) open. Outgoing connections stay allowed. The
key part: we proceed in a way that ensures you <strong>don&rsquo;t lock yourself out</strong>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="/en/tutorials/harden-ssh/">hardened server</a> with a sudo user</li>
<li>You know which <strong>port your SSH</strong> runs on (default: 22)</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-install-ufw">Step 1: Install UFW</h3>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install -y ufw</span></span></code></pre></div>
</div>
<p>Check the version – UFW is <strong>inactive</strong> at first, which is intended:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ufw version</span></span></code></pre></div>
</div>
<h3 id="step-2-define-the-base-rules">Step 2: Define the base rules</h3>
<p>First the default direction: deny everything incoming, allow everything outgoing.</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw default deny incoming
</span></span><span class="line"><span class="cl">sudo ufw default allow outgoing</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Default incoming policy changed to &#39;deny&#39;
</span></span><span class="line"><span class="cl">(be sure to update your rules accordingly)
</span></span><span class="line"><span class="cl">Default outgoing policy changed to &#39;allow&#39;
</span></span><span class="line"><span class="cl">(be sure to update your rules accordingly)</span></span></code></pre></div>
</div>
<p>These rules don&rsquo;t take effect yet – UFW is still inactive. So the exceptions come
now, <strong>before</strong> we switch it on.</p>
<h3 id="step-3-allow-ssh--first-or-youll-lock-yourself-out">Step 3: Allow SSH – first, or you&rsquo;ll lock yourself out</h3>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Allow SSH first, then activate
  </p>
  <div class="prose-kitchen text-sm">If you enable UFW with the &ldquo;deny incoming&rdquo; rule <strong>without</strong> having allowed SSH
first, the next command cuts your own connection – and you can no longer get in via
SSH. This order is non-negotiable.</div>
</div>
<p>If your SSH runs on the default port 22 (and <code>openssh-server</code> is installed), there&rsquo;s
a ready-made profile for it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw allow OpenSSH</span></span></code></pre></div>
</div>
<p>If you changed the SSH port (e.g. to 2222), allow exactly that port instead:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw allow 2222/tcp</span></span></code></pre></div>
</div>
<h3 id="step-4-allow-http-and-https">Step 4: Allow HTTP and HTTPS</h3>
<p>For the later <a href="/en/tutorials/traefik-reverse-proxy/">reverse proxy with Traefik</a> you
need the web ports. Open them right away:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw allow 80/tcp
</span></span><span class="line"><span class="cl">sudo ufw allow 443/tcp</span></span></code></pre></div>
</div>
<p>UFW confirms each rule with <code>Rules updated</code> and <code>Rules updated (v6)</code> (the IPv6
variant).</p>
<h3 id="step-5-activate-the-firewall-and-check-it">Step 5: Activate the firewall and check it</h3>
<p>Now switch it on:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw enable</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Command may disrupt existing ssh connections. Proceed with operation (y|n)? y
</span></span><span class="line"><span class="cl">Firewall is active and enabled on system startup</span></span></code></pre></div>
</div>
<p>Check the result:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo ufw status verbose</span></span></code></pre></div>
</div>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">Status: active
</span></span><span class="line"><span class="cl">Logging: on (low)
</span></span><span class="line"><span class="cl">Default: deny (incoming), allow (outgoing), disabled (routed)
</span></span><span class="line"><span class="cl">New profiles: skip
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">To                         Action      From
</span></span><span class="line"><span class="cl">--                         ------      ----
</span></span><span class="line"><span class="cl">22/tcp (OpenSSH)           ALLOW IN    Anywhere
</span></span><span class="line"><span class="cl">80/tcp                     ALLOW IN    Anywhere
</span></span><span class="line"><span class="cl">443/tcp                    ALLOW IN    Anywhere
</span></span><span class="line"><span class="cl">22/tcp (OpenSSH (v6))      ALLOW IN    Anywhere (v6)
</span></span><span class="line"><span class="cl">80/tcp (v6)                ALLOW IN    Anywhere (v6)
</span></span><span class="line"><span class="cl">443/tcp (v6)               ALLOW IN    Anywhere (v6)</span></span></code></pre></div>
</div>
<p><code>Status: active</code> and your three allowances – done. <strong>To be safe, test in a second
SSH session</strong> that you can still connect before closing the first one.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>After <code>ufw enable</code> you can no longer get in via SSH.</strong> The SSH rule was missing or
targeted the wrong port. Connect via the <strong>console in the netcup SCP</strong> (VNC,
independent of SSH), allow your SSH port there (<code>sudo ufw allow …</code>) and test again.
That&rsquo;s exactly what step 3 warns about.</p>
<p><strong><code>ERROR: Could not find a profile matching 'OpenSSH'</code>.</strong> The OpenSSH profile only
exists if <code>openssh-server</code> is installed. Use the port number instead: <code>sudo ufw allow 22/tcp</code> (or your port). <code>sudo ufw app list</code> shows the available profiles.</p>
<p><strong>A Docker container is reachable from outside even though UFW doesn&rsquo;t open the
port.</strong> Not your mistake – <strong>Docker bypasses UFW</strong>. Docker writes its rules directly
into <code>iptables</code> and inserts them ahead of the UFW chains. A published container port
(<code>ports:</code> in the compose.yaml) is thus open, no matter what UFW says. The clean
solution is a second firewall layer <strong>in front of</strong> the server – at netcup the
<a href="/en/tutorials/netcup-firewall-setup/">firewall in the SCP</a> as an upstream
perimeter. On the host it also helps to bind container ports only to <code>127.0.0.1</code>
instead of <code>0.0.0.0</code>.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>View and delete rules:</strong> <code>sudo ufw status numbered</code> numbers all rules;
<code>sudo ufw delete 3</code> removes rule 3. That&rsquo;s how you tidy up when a service goes
away.</li>
<li><strong>New services:</strong> For every additional public port, one targeted rule – never
&ldquo;just open everything&rdquo;. What doesn&rsquo;t need to be open stays closed.</li>
<li><strong>No backup needed,</strong> but note down your allowances (or keep them in the same
document as your DNS records). The rule set is retyped in seconds.</li>
<li><strong>Know the limits:</strong> UFW protects the host, but not against the Docker gap above.
An upstream perimeter firewall like the
<a href="/en/tutorials/netcup-firewall-setup/">netcup firewall in the SCP</a> complements UFW
into two layers that secure each other – ideal once containers with open ports are
running.</li>
</ul>
]]></content:encoded></item><item><title>Installing Docker on Debian</title><link>https://serverkueche.de/en/tutorials/install-docker/</link><pubDate>Wed, 15 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/install-docker/</guid><description>Install Docker Engine and Docker Compose cleanly from the official repository – the foundation for most self-hosting recipes.</description><content:encoded><![CDATA[<p>Docker is the foundation for almost every application tutorial in the Serverküche.
We install it from the official Docker repository – not from the Debian package
sources.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end, the current <strong>Docker Engine with the Compose plugin</strong> runs on your
<strong>Debian 13</strong>, installed from the official Docker repository. That has two
advantages over the Debian package <code>docker.io</code>: more current versions with timely
security updates, and the official <code>docker compose</code> that all Compose recipes here
build on.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="/en/tutorials/harden-ssh/">hardened server</a> with a sudo user</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-set-up-the-repository-key">Step 1: Set up the repository key</h3>
<p>First, install the tools needed to verify the Docker repository:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install -y ca-certificates curl</span></span></code></pre></div>
</div>
<p>Then create the keyring directory and download Docker&rsquo;s GPG key – apt uses it later
to check that the packages really come from Docker:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo install -m <span class="m">0755</span> -d /etc/apt/keyrings
</span></span><span class="line"><span class="cl">sudo curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc</span></span></code></pre></div>
</div>
<h3 id="step-2-add-the-docker-package-source">Step 2: Add the Docker package source</h3>
<p>Add the Docker repository to your package sources. The command detects the
architecture and Debian version automatically, so you can copy it unchanged:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;deb [arch=</span><span class="k">$(</span>dpkg --print-architecture<span class="k">)</span><span class="s2"> signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/debian </span><span class="k">$(</span>. /etc/os-release <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$VERSION_CODENAME</span><span class="s2">&#34;</span><span class="k">)</span><span class="s2"> stable&#34;</span> <span class="p">|</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</span></span></code></pre></div>
</div>
<h3 id="step-3-install-docker">Step 3: Install Docker</h3>
<p>Refresh the package lists (now including the Docker repository) and install the
Engine together with the Compose plugin:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo apt update
</span></span><span class="line"><span class="cl">sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin</span></span></code></pre></div>
</div>
<p>Check that the service is running:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl status docker</span></span></code></pre></div>
</div>
<p>You should see <code>active (running)</code>. A quick smoke test:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo docker run --rm hello-world</span></span></code></pre></div>
</div>
<p>The output <code>Hello from Docker!</code> confirms that the installation works.</p>
<h3 id="step-4-use-docker-without-sudo">Step 4: Use Docker without sudo</h3>
<p>Add your user to the <code>docker</code> group so you don&rsquo;t have to prefix every command with
<code>sudo</code>:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo usermod -aG docker <span class="nv">$USER</span></span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">The group membership only takes effect after you <strong>log out and back in</strong> (end the
SSH session and reconnect).</div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Security note
  </p>
  <div class="prose-kitchen text-sm">Members of the <code>docker</code> group effectively have root rights on the server – only add
your own admin user, no shared or unprivileged accounts.</div>
</div>
<p>After that, <code>docker ps</code> works without sudo and shows a (still empty) container list.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong><code>E: Unable to locate package docker-ce</code>.</strong> The package source from step 2 is
missing or malformed. Check the contents of <code>/etc/apt/sources.list.d/docker.list</code> –
it must contain your Debian version (e.g. <code>trixie</code>) – and then run <code>sudo apt update</code>
again.</p>
<p><strong><code>permission denied while trying to connect to the Docker daemon socket</code>.</strong> The
<code>docker</code> group membership hasn&rsquo;t taken effect yet. End the SSH session and
reconnect; <code>groups</code> must then include <code>docker</code>. If not, repeat step 4.</p>
<p><strong>Conflicts during installation with already-present packages.</strong> Another Docker
variant is already installed (<code>docker.io</code>, <code>podman-docker</code>, …). Remove the old
packages first: <code>sudo apt remove docker.io docker-doc docker-compose podman-docker containerd runc</code> – existing containers/images are preserved.</p>
<p><strong><code>docker compose</code> reports <code>'compose' is not a docker command</code>.</strong> The Compose plugin
is missing – Docker was probably installed differently earlier. Run <code>sudo apt install docker-compose-plugin</code>. Note: the old <code>docker-compose</code> (with a hyphen) is a
different, outdated tool.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Updates:</strong> Docker updates along with the normal
<code>sudo apt update &amp;&amp; sudo apt upgrade</code> – one reason we use the official
repository. When the Engine is updated, running containers restart briefly (or
stop until you start them again) – plan for that.</li>
<li><strong>Cleanup:</strong> Unused images and build leftovers pile up quickly.
<code>docker system df</code> shows the usage, <code>docker system prune</code> cleans up
(<strong>careful:</strong> it also removes stopped containers).</li>
<li><strong>Backups:</strong> The actual data of your applications will later live in volumes or
bind mounts – we build the backup strategy for that with
<a href="/en/tutorials/restic-backups/">encrypted Restic backups</a> and in the individual
application tutorials.</li>
</ul>
]]></content:encoded></item><item><title>Hardening SSH access</title><link>https://serverkueche.de/en/tutorials/harden-ssh/</link><pubDate>Tue, 14 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/harden-ssh/</guid><description>Key-based login instead of passwords and no root login: the most important moves to harden the front door to your server.</description><content:encoded><![CDATA[<p>After the <a href="/en/tutorials/first-steps-netcup-vps/">initial setup</a> we harden the SSH
access – the most important door to your server.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end you log in with an <strong>SSH key</strong> instead of a password, and neither root
login nor password login is possible from outside. This makes the automated
password-guessing attacks that hit every publicly reachable server around the
clock run completely into the void. Tested with <strong>OpenSSH on Debian 13</strong>.</p>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>A <a href="/en/tutorials/first-steps-netcup-vps/">set-up server</a> with a sudo user</li>
<li>Access to the VNC console in the netcup SCP as a safety line in case you lock yourself out</li>
</ul>
<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-generate-an-ssh-key-pair">Step 1: Generate an SSH key pair</h3>
<p>If you don&rsquo;t have a key yet, generate an Ed25519 key pair <strong>on your own machine</strong>
(not on the server!):</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-keygen -t ed25519</span></span></code></pre></div>
</div>
<p>You can accept the suggested storage location with Enter. Set a <strong>passphrase</strong> –
it protects the key if your machine falls into the wrong hands. Two files are
created: <code>~/.ssh/id_ed25519</code> (private, stays with you) and
<code>~/.ssh/id_ed25519.pub</code> (public, goes on the server).</p>
<h3 id="step-2-transfer-the-public-key">Step 2: Transfer the public key</h3>
<p><code>ssh-copy-id</code> appends your public key to the <code>~/.ssh/authorized_keys</code> file of your
server user – with the correct file permissions:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh-copy-id koch@YOUR_SERVER_IP</span></span></code></pre></div>
</div>
<p>Test right afterwards that key login works:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh koch@YOUR_SERVER_IP</span></span></code></pre></div>
</div>
<p>You should now be logged in <strong>without</strong> a server password (only the passphrase of
your key may be requested). Only once that works do we continue – in the next step
we disable password login.</p>
<h3 id="step-3-disable-password-login-and-root-login">Step 3: Disable password login and root login</h3>
<p>Open the SSH server configuration:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo nano /etc/ssh/sshd_config</span></span></code></pre></div>
</div>
<p>Set (or uncomment) these two lines:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">PermitRootLogin no
</span></span><span class="line"><span class="cl">PasswordAuthentication no</span></span></code></pre></div>
</div>
<p><code>PermitRootLogin no</code> blocks direct root login completely,
<code>PasswordAuthentication no</code> allows only key logins.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">On cloud images there are often extra files under <code>/etc/ssh/sshd_config.d/</code> that
<strong>override</strong> these settings. Check with
<code>grep -r PasswordAuthentication /etc/ssh/sshd_config.d/</code> and adjust any matches as
well.</div>
</div>
<h3 id="step-4-test-the-configuration-and-restart-ssh">Step 4: Test the configuration and restart SSH</h3>
<p>Check the configuration for syntax errors <strong>before</strong> you restart – a typo would
otherwise take the SSH service down:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo sshd -t</span></span></code></pre></div>
</div>
<p>No output means: all good. Then apply it:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">sudo systemctl restart ssh</span></span></code></pre></div>
</div>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-paprika-400 bg-paprika-50 dark:border-paprika-700 dark:bg-paprika-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🔥</span>Caution
  </p>
  <div class="prose-kitchen text-sm">Test key login now in a <strong>second</strong> terminal session before you close the first –
otherwise you might lock yourself out.</div>
</div>
<p>To verify: <code>sudo systemctl status ssh</code> should show <code>active (running)</code>, and a login
attempt as root must now be rejected with <code>Permission denied (publickey)</code>.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong>SSH keeps asking for the password despite <code>ssh-copy-id</code>.</strong> Usually the file
permissions on the server are wrong. SSH ignores <code>authorized_keys</code> if the
permissions are too open: <code>chmod 700 ~/.ssh &amp;&amp; chmod 600 ~/.ssh/authorized_keys</code>.
Also check that you&rsquo;re connecting with the right user (<code>koch@…</code>, not <code>root@…</code>).</p>
<p><strong><code>Permission denied (publickey)</code> – and you can&rsquo;t get in at all anymore.</strong>
Password login was disabled before the key worked. No drama: open the <strong>VNC
console in the netcup SCP</strong>, log in locally there, set <code>PasswordAuthentication yes</code>, restart SSH and start again at step 2.</p>
<p><strong>After the restart the SSH service no longer starts.</strong> Syntax error in the
<code>sshd_config</code> (that&rsquo;s why you always run <code>sshd -t</code> before restarting). Log in via
the VNC console; <code>sudo sshd -t</code> shows you the file and line number of the error.</p>
<p><strong>The settings seem to take effect, but password login still works.</strong> A file under
<code>/etc/ssh/sshd_config.d/</code> (often <code>50-cloud-init.conf</code>) overrides your values.
<code>sudo sshd -T | grep -i passwordauthentication</code> shows the actually effective
setting – adjust matches in the extra files and restart SSH.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Back up the private key:</strong> Without <code>~/.ssh/id_ed25519</code> you can only reach the
server via the VNC console. Back up the key encrypted (e.g. in a password
manager) or add a second key from another device to <code>authorized_keys</code>.</li>
<li><strong>Keep an eye on logins:</strong> <code>sudo journalctl -u ssh --since today</code> shows you all
login attempts. Failed attempts on port 22 are normal and, thanks to the
key requirement, harmless – you can automatically ban such bots later with
<a href="/en/tutorials/fail2ban-setup/">Fail2ban</a>.</li>
<li><strong>Updates:</strong> OpenSSH gets its security updates through the normal <code>apt upgrade</code> –
so keep the server up to date, ideally
<a href="/en/tutorials/unattended-upgrades-automatic-updates/">automated</a>.</li>
</ul>
]]></content:encoded></item><item><title>First steps with a netcup VPS</title><link>https://serverkueche.de/en/tutorials/first-steps-netcup-vps/</link><pubDate>Mon, 13 Jul 2026 00:00:00 +0000</pubDate><author>feedback@serverkueche.de (Serverküche)</author><guid>https://serverkueche.de/en/tutorials/first-steps-netcup-vps/</guid><description>From a freshly ordered netcup server to a ready-to-use system: SSH login, system updates, a sudo user and the most important first steps.</description><content:encoded><![CDATA[<p>You&rsquo;ve ordered your first netcup server – congratulations! Before we fire up the
kitchen, let&rsquo;s set the system up cleanly.</p>
<h2 id="what-are-we-building">What are we building?</h2>
<p>By the end of this tutorial, your VPS runs a current <strong>Debian 13</strong> with all
security updates, and you work with your own user with sudo rights instead of
permanently as <code>root</code>. That&rsquo;s the foundation for all the other recipes in the
Serverküche – from hardening SSH to your first application.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-herb-400 bg-herb-50 dark:border-herb-700 dark:bg-herb-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">🧑‍🍳</span>Tip
  </p>
  <div class="prose-kitchen text-sm">All commands are written for <strong>Debian 13</strong>. On Ubuntu they work almost
identically.</div>
</div>
<h2 id="prerequisites">Prerequisites</h2>
<ul>
<li>An ordered netcup server with the credentials from your customer account</li>
<li>A terminal on your machine (Linux/macOS) or an SSH client like PuTTY (Windows)</li>
</ul>
<p>Not sure which server size you even need? The
<a href="/en/server-calculator/">server calculator</a> estimates the RAM and CPU you&rsquo;ll need for your
planned services and suggests a matching netcup plan.</p>
<div class="not-prose my-6 overflow-hidden rounded-xl border border-paprika-200 bg-paprika-50 dark:border-paprika-800 dark:bg-paprika-900/20">
  <div class="flex items-center justify-between border-b border-paprika-200 bg-paprika-100 px-4 py-1.5 text-xs font-semibold uppercase tracking-wide text-paprika-700 dark:border-paprika-800 dark:bg-paprika-900/40 dark:text-paprika-300">
    <span>🍳 Recommendation</span>
    <span title="Links marked with * are affiliate links.">Ad</span>
  </div>
  <div class="flex flex-col gap-4 p-4 sm:flex-row sm:items-center sm:justify-between">
    <div>
      <p class="text-lg font-bold text-slate-900 dark:text-white">VPS 1000 G12</p>
      <p class="mt-1 text-sm text-slate-600 dark:text-slate-300">4 vCores · 8 GB RAM · 256 GB NVMe</p>
      <p class="mt-1 text-sm font-semibold text-paprika-700 dark:text-paprika-400">from €10.36/month</p>
      <p class="mt-2 text-sm text-slate-600 dark:text-slate-400">A good start for most self-hosting projects.</p>
    </div>
    <a href="https://www.netcup.com/en/server/vps/vps-1000-g12-iv-12m?ref=44083" rel="sponsored noopener" target="_blank"
   class="inline-flex shrink-0 items-center justify-center rounded-lg bg-paprika-600 px-5 py-2.5 font-semibold text-white transition-colors hover:bg-paprika-700">
  Go to netcup →
</a>

  </div><div class="px-4 pb-4"><p class="not-prose my-3 flex flex-wrap items-center gap-x-2 gap-y-1 rounded-lg border border-herb-500/40 bg-herb-50 px-3 py-2 text-sm text-slate-700 dark:bg-herb-900/20 dark:text-slate-200">
  <span>💶 <strong>5 € voucher</strong> for new netcup customers:</span>
  <code class="rounded bg-white px-2 py-0.5 font-mono text-sm font-semibold text-herb-800 dark:bg-slate-800 dark:text-herb-400">36nc17844976032</code>
  <span class="text-xs text-slate-500 dark:text-slate-400">(new customers only, no domains)</span>
</p></div>
</div>

<h2 id="step-by-step">Step by step</h2>
<h3 id="step-1-log-in-via-ssh">Step 1: Log in via SSH</h3>
<p>Log in with your server&rsquo;s IP address as <code>root</code>. You&rsquo;ll find the IP and the
password in the credentials from your customer account:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh root@YOUR_SERVER_IP</span></span></code></pre></div>
</div>
<p>On the very first login, SSH asks whether you want to trust the server:</p>
<div class="sk-code">
  <span class="sk-code-head">Ausgabe</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">The authenticity of host &#39;YOUR_SERVER_IP&#39; can&#39;t be established.
</span></span><span class="line"><span class="cl">...
</span></span><span class="line"><span class="cl">Are you sure you want to continue connecting (yes/no/[fingerprint])?</span></span></code></pre></div>
</div>
<p>Answer <code>yes</code> – your machine remembers the server from now on. After that you land
on a prompt like <code>root@v12345:~#</code>.</p>
<h3 id="step-2-update-the-system">Step 2: Update the system</h3>
<p>Freshly delivered images are rarely up to date. So the first thing to do is fetch
all updates – security updates in particular should never wait:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">apt update <span class="o">&amp;&amp;</span> apt upgrade -y</span></span></code></pre></div>
</div>
<p><code>apt update</code> fetches the current package lists, <code>apt upgrade -y</code> installs all
available updates without prompting.</p>
<div class="not-prose my-6 rounded-lg border-l-4 p-4 border-amber-400 bg-amber-50 dark:border-amber-700 dark:bg-amber-900/20">
  <p class="mb-1 flex items-center gap-2 font-semibold text-slate-900 dark:text-white">
    <span aria-hidden="true">⚠️</span>Warning
  </p>
  <div class="prose-kitchen text-sm">Reboot the server after a kernel update (<code>reboot</code>) so the changes take effect. On
a fresh Debian there&rsquo;s no automation for this yet – the safest bet is a reboot
right after the first big update. Debian does <strong>not</strong> create the marker file
<code>/var/run/reboot-required</code> (which indicates a pending reboot) on kernel updates by
default – you&rsquo;ll set up the matching hook later together with
<a href="/en/tutorials/unattended-upgrades-automatic-updates/">automatic updates</a>.</div>
</div>
<h3 id="step-3-create-a-sudo-user">Step 3: Create a sudo user</h3>
<p>Don&rsquo;t work as <code>root</code> permanently: a typo with full rights can wreck the entire
system, and having your own user is the prerequisite for
<a href="/en/tutorials/harden-ssh/">disabling root login completely</a> later. Create a
user – we&rsquo;ll call it <code>koch</code> (&ldquo;cook&rdquo;) here, but you can pick any name:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">adduser koch</span></span></code></pre></div>
</div>
<p>You&rsquo;ll be asked for a password and a few optional details (you can skip the
details with Enter). After that, give the user sudo rights:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">usermod -aG sudo koch</span></span></code></pre></div>
</div>
<h3 id="step-4-test-the-new-user">Step 4: Test the new user</h3>
<p>Check <strong>in a new terminal session</strong> (keep the root session open!) that login and
sudo work:</p>
<div class="sk-code">
  <span class="sk-code-head">Terminal</span>
  <div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="cl">ssh koch@YOUR_SERVER_IP
</span></span><span class="line"><span class="cl">sudo whoami</span></span></code></pre></div>
</div>
<p>After entering your password, <code>sudo whoami</code> should return <code>root</code>. From now on you
continue working with this user.</p>
<h2 id="when-things-go-wrong">When things go wrong</h2>
<div class="troubleshoot not-prose">
<p><strong><code>ssh: connect to host … port 22: Connection timed out</code>.</strong> Usually the IP was
mistyped or the server isn&rsquo;t fully provisioned yet. Check the IP in your customer
account and whether the server is shown as &ldquo;online&rdquo; there. Wait a few minutes
after ordering.</p>
<p><strong><code>Permission denied, please try again</code> on root login.</strong> Wrong password – often a
copy-paste issue with invisible trailing spaces, or a different keyboard layout.
Copy the password without surrounding spaces directly from the credentials.</p>
<p><strong><code>WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!</code>.</strong> The server was reinstalled
and has a new host key – SSH rightly raises the alarm. If <strong>you</strong> reinstalled it,
remove the old entry with <code>ssh-keygen -R YOUR_SERVER_IP</code> and reconnect. If you
didn&rsquo;t reinstall, investigate before you connect.</p>
<p><strong><code>sudo: command not found</code> as the new user.</strong> On minimal images the package is
sometimes missing. Install it as <code>root</code>: <code>apt install sudo</code>. Then check that the
user is in the group: <code>groups koch</code> must contain <code>sudo</code> – otherwise repeat step 3
and log out and back in once.</p>

</div>

<h2 id="maintenance--backups">Maintenance &amp; backups</h2>
<ul>
<li><strong>Updates:</strong> You should schedule <code>sudo apt update &amp;&amp; sudo apt upgrade</code> at least
<strong>weekly</strong> – or
<a href="/en/tutorials/unattended-upgrades-automatic-updates/">automate security updates</a>
with <code>unattended-upgrades</code>.</li>
<li><strong>Snapshots:</strong> Create a snapshot in the
<a href="/en/tutorials/netcup-snapshots-scp/">netcup Server Control Panel (SCP)</a> before
bigger changes. It&rsquo;s your safety line while you don&rsquo;t have a proper backup
strategy yet – but a snapshot does <strong>not replace</strong> a backup outside the server.</li>
<li><strong>Credentials:</strong> Keep the root password safe (password manager). Via the VNC
console in the SCP you can still reach the server even when SSH is stuck.</li>
</ul>
<p>You&rsquo;ll find both in the server overview of the SCP: the <strong>VNC console</strong> is under
the <strong>&ldquo;Bildschirm&rdquo; (Display)</strong> tab, and the remaining <strong>snapshot quota</strong> is shown
in the status block.</p>
<p><figure class="my-6"><img src="/en/tutorials/first-steps-netcup-vps/netcup-scp-server_hu_8973a5aa80175a41.webp" srcset="/en/tutorials/first-steps-netcup-vps/netcup-scp-server_hu_ddb4950e5055f484.webp 480w, /en/tutorials/first-steps-netcup-vps/netcup-scp-server_hu_8973a5aa80175a41.webp 768w, /en/tutorials/first-steps-netcup-vps/netcup-scp-server_hu_18e42b31ae963ce5.webp 1200w, /en/tutorials/first-steps-netcup-vps/netcup-scp-server_hu_47fe22d828e0f3e5.webp 1920w" sizes="(min-width: 768px) 768px, 100vw"
    width="768" height="432"
    data-full="/en/tutorials/first-steps-netcup-vps/netcup-scp-server_hu_7cd6ca3ca4c57df9.webp"
    alt="The server overview in the netcup Server Control Panel: tabs including &ldquo;Display&rdquo; (VNC console), status with snapshot quota, and the hardware specs" title="Server overview in the netcup SCP – the Display tab is the VNC console"
    loading="lazy" decoding="async" class="rounded-lg"><figcaption class="mt-2 text-sm text-center text-slate-500 italic">Server overview in the netcup SCP – the Display tab is the VNC console</figcaption></figure></p>
]]></content:encoded></item></channel></rss>