<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>my tech blog &#187; Linux</title>
	<atom:link href="http://billauer.se/blog/category/linux/feed/" rel="self" type="application/rss+xml" />
	<link>https://billauer.se/blog</link>
	<description>Anything I found worthy to write down.</description>
	<lastBuildDate>Thu, 12 Mar 2026 11:36:00 +0000</lastBuildDate>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
	<generator>http://wordpress.org/?v=3.1.2</generator>
		<item>
		<title>Enabling STARTTLS on sendmail with Let&#8217;s Encrypt certificate</title>
		<link>https://billauer.se/blog/2026/03/tls-sendmail-lets-encrypt/</link>
		<comments>https://billauer.se/blog/2026/03/tls-sendmail-lets-encrypt/#comments</comments>
		<pubDate>Wed, 04 Mar 2026 07:39:01 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[email]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7223</guid>
		<description><![CDATA[Introduction I&#8217;ll start with the crucial point: My interest in giving sendmail a Let&#8217;s Encrypt certificate (along with a secret key, of course) has nothing to do with security. The real reason is that some mail servers won&#8217;t deliver their mail to my server unless the link is encrypted. As of today (March 2026), I [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>I&#8217;ll start with the crucial point: My interest in giving sendmail a Let&#8217;s Encrypt certificate (along with a secret key, of course) has nothing to do with security. The real reason is that some mail servers won&#8217;t deliver their mail to my server unless the link is encrypted. As of today (March 2026), I know only of one such case, but that&#8217;s enough for me to understand that I must at least support an opportunistic TLS upgrade for arriving mails. In other words, my mail server must allow STARTTLS for servers who want to drop a mail at my server.</p>
<p>The problem with servers that won&#8217;t play ball without STARTTLS is that the mail is lost, sometimes without the sender being notified. I discovered this issue when one of those confirm-your-email messages didn&#8217;t arrive, and I sent the sender&#8217;s tech support a complaint. To which they responded with the reason: My server didn&#8217;t support a STARTTLS upgrade.</p>
<p>So the goal is to reduce the risk of mail loss, nothing else. And the main concern is that mail will not be delivered in cases it would have before adding STARTTLS. For example, where a cleartext connection would have been OK, but the sides attempted and failed to initiate a STARTTLS encrypted session and then the connection is terminated altogether, no mail delievered.</p>
<p>I&#8217;m running sendmail 8.14.4 on a Debian 8 machine (yes, it&#8217;s really ancient, but don&#8217;t play around with a stable system).</p>
<p>As for sources of information, there are a lot of guides out there. For those preferring the horse&#8217;s mouth, there&#8217;s the README on configuration files at /usr/share/sendmail-cf/README. Look for <a rel="noopener" href="https://www.sendmail.org/~ca/email/doc8.12/cf/m4/starttls.html" target="_blank">the section about STARTTLS</a>. An example configuration can be found at /usr/share/sendmail/examples/tls/starttls.m4.</p>
<p>And since this topic revolves around certificates, maybe check out <a rel="noopener" href="https://billauer.se/blog/2021/04/certificate-ca-tutorial-primer/" target="_blank">another post of mine</a> which attempts to explain this topic. And my own little <a rel="noopener" href="https://billauer.se/blog/2019/03/email-server-setup/" target="_blank">tutorial on setting up sendmail</a>.</p>
<h3>Is encryption worth anything?</h3>
<p>If a mail server refuses to talk with anyone unless a <a rel="noopener" href="https://en.wikipedia.org/wiki/Transport_Layer_Security" target="_blank">TLS</a> link is established with a mutually verified certificate (both sides check each other), encryption indeed adds security. Otherwise, it&#8217;s quite pointless.</p>
<p>Let&#8217;s begin with accepting arriving emails: If a server agrees to cleartext connections by clients for dropping off mails, it&#8217;s up to the sender to decide the level of security. Even if the arriving connection is encrypted with TLS, that doesn&#8217;t mean that connection is secure. Surely, the server is requied to submit a certificate when setting up the TLS session, but did the client verify it? And it the verification failed, did it terminate the connection? If it didn&#8217;t, a simple man-in-the-middle attack allows an eavesdropper can come in the middle, feed the client with a cooked-up certificate, accept the email, and then relay this email to the real destination, this time with a proper TLS connection. The creates an illusion that the mail was transmitted securely.</p>
<p>As a receiver of this mail, you can&#8217;t be sure who&#8217;s on the other side without checking the client&#8217;s certificate. A lot of clients won&#8217;t supply a certificate, though (my own server included, more about this below).</p>
<p>As for sending emails, an eavesdropping server might pretend to be the destination (possibly by DNS poisoning of the MX record). In the simplest man-in-the-middle attack, the eavesdropper doesn&#8217;t allow STARTTLS, and the message is transmitted in cleartext. If someone bothers to look in the mail server&#8217;s logs, this can be detected. Alternatively, the eavesdropper might suggest STARTTLS and offer an invalid certificate. For example, a self-signed certificate might seem like an innocent mistake. If the sending server agrees to sending the email nevertheless, the attack is successful (but with a &#8220;verify=FAIL&#8221; in the logs, allowing spotting the attack, if verifications usually are successful).</p>
<p>So to be really secure all mail servers must insist on TLS and verify each other&#8217;s certificates, or else the mail doesn&#8217;t go through. At present, going this path with a public mail server means a lot of undelivered mails (from legit sources). In particular, insisting that the sender of the email offers a valid certificate is not going to end well.</p>
<h3>The situation before I made any changes</h3>
<p>With the default configuration, sendmail has no certificates available for use. With no certificates, I mean no certificates to identify itself, but also no root certificates that allow verifying other servers.</p>
<p>This doesn&#8217;t prevent my server from connecting to other servers with TLS for outbound mails. In the mail log, the relevant entry looks like this:</p>
<pre>sm-mta: 6237AaWA022257: from=&lt;my@address.com&gt;, size=2503, class=0, nrcpts=1, msgid=&lt;f11f0@address.com&gt;, bodytype=8BITMIME, proto=ESMTP, daemon=IPv4-port-587, relay=localhost.localdomain [127.0.0.1]
sm-mta: 6237AaWA022257: Milter insert (1): header: DKIM-Signature: <span class="yadayada">[ ... ]</span>
sm-mta: <span class="punch">STARTTLS=client</span>, relay=mx.other.com., version=TLSv1/SSLv3, <span class="punch">verify=FAIL</span>, cipher=ECDHE-RSA-AES128-GCM-SHA256, bits=128/128
sm-mta: 6237AaWA022257: to=&lt;friend@other.com&gt;, ctladdr=&lt;my@address.com&gt; (1000/1000), delay=00:00:03, xdelay=00:00:03, mailer=esmtp, pri=122503, relay=mx.other.com. [128.112.34.45], dsn=2.0.0, stat=Sent (Ok: queued as )</pre>
<p>Note the &#8220;VERIFY=fail&#8221; on the third row, discussed in length below. For sending email, the only drawback for not setting up anything encryption-related is that the server&#8217;s identity isn&#8217;t verified. Plus, my server didn&#8217;t send a certificate if it was asked to do so, but that&#8217;s quite usual.</p>
<p>So to the server receiving the email, everything is normal. My server, acting as a client, upgraded the connection to encrypted with STARTTLS, and went through with it. No problem at all.</p>
<h3>Should I install root certificates?</h3>
<p>In order to allow my server to prevent a man-in-the-middle attack for outbound email, I can install root certificates and make them available to sendmail by virtue of configuration parameters. I also need to configure sendmail to refuse anything else than a TLS with a verified server. Otherwise, it&#8217;s pointless, see above.</p>
<p>At the very least, I will need to update the root certificates often enough, so that new root certificates that are generally accepted are recognized by sendmail, and that expired root certificates are replaced by new ones.</p>
<p>And even if I do everything right, some mails will probably not go through because the destination mail server isn&#8217;t configured correctly. Or doesn&#8217;t support STARTTLS at all, just as my own didn&#8217;t, before the changes I describe here.</p>
<p>So clearly, no root certificates on my server. I want all emails to fail verification, so if I mistakenly enable some kind of enforcement, all deliveries will fail, and not one isolated case a couple of weeks after making the mistake.</p>
<h3>Now to the practical part</h3>
<p>In the existing installation, /etc/mail is where sendmail keeps its configuration file. So I created the /etc/mail/certs/ directory, and populated it with three files:</p>
<ul>
<li>my.pem: The certificate obtained from Let&#8217;s Encrypt.</li>
<li>my.key: The secret key for which this certificate is made (or to be really accurate, the certificate is made for the public key that pairs with this key). Readable by root only (for security and to make sendmail happy).</li>
<li>ca.pem: The intermediate certificate which completes the trust chain from my.pem to the root certificate (Let&#8217;s Encrypt&#8217;s entire chain consists of just three certificates, root included).</li>
</ul>
<p>I was a bit sloppy in the description for my.pem, because I use <a rel="noopener" href="https://gitlab.com/sinclair2/bacme" target="_blank">bacme</a> to obtain the certificate from Let&#8217;s Encrypt, and that script gives me a certificate file that contains both my.pem and ca.pem, concatenated. The script that separates these two into separate files is shown further below.</p>
<p>And then I added these rows to sendmail.mc:</p>
<pre>define(`confCACERT_PATH', `/etc/mail/certs')dnl
define(`confCACERT', `/etc/mail/certs/ca.pem')dnl
define(`confSERVER_CERT', `/etc/mail/certs/my.pem')dnl
define(`confSERVER_KEY', `/etc/mail/certs/my.key')dnl</pre>
<p>Note that with this setting, the server doesn&#8217;t supply any certificate when acting as a client (i.e. when submitting an outbound email), even if asked for it. To enable this, the confCLIENT_CERT and confCLIENT_KEY options need to be assigned. This option should not be used with Let&#8217;s Encrypt&#8217;s certificates, as discussed below.</p>
<p>Actually, my initial attempt was to add only the two last rows, defining confSERVER_CERT and confSERVER_KEY. As the certificate file I get from my renewal utility already contains both my own and intermediate certificates, why not give sendmail only this combo file and forget about CAs? I mean, this is how I do it with Apache&#8217;s web server!</p>
<p>That idea failed royally in two different ways:</p>
<ul>
<li>If confCACERT_PATH and confCACERT aren&#8217;t defined, sendmail will start, but won&#8217;t activate the STARTTLS option. Actually, even if both are defined as above, and ca.pem is empty, no STARTTLS. Raising the loglevel with this definition allowed sendmail to complain specifically about this:
<pre>define(`confLOG_LEVEL', `14')dnl</pre>
</li>
<li>When I put an irrelevant certificate in ca.pem, sendmail activated STARTTLS, ignored ca.pem (which is fine, it doesn&#8217;t help) but presented only the first certificate in my.pem to the client connecting.</li>
</ul>
<p>To put it simple, sendmail wants my own certificate in my.pem and the CA&#8217;s certificate(s) in ca.pem, and kindly asks me not to fool around. And I have no problem with this, as the intermediate certificate can&#8217;t be used by my server to verify other server&#8217;s certificates. So adding it doesn&#8217;t work against my decision that all attempts to verify certificates by my server will fail.</p>
<p>But this arrangement requires writing a little script to separate the certificates, which is listed below. As far as I understand, those using the mainstream certbot don&#8217;t have this problem, as it generates separate files.</p>
<h3>Reloading the daemon</h3>
<p>It says everywhere, that sendmail must be restarted after updating the certificates. Even though I have the impression that sendmail is designed to shut itself down properly and safely in response to a SIGTERM, and even more importantly, that it&#8217;s designed not to lose or duplicate any mails, I didn&#8217;t fancy the idea of sending the server the signal that is usually used when shutting down the entire computer.</p>
<p>Instead, it&#8217;s possible to send the server a SIGHUP, which makes the server reload its configuration files (except for sendmail.conf, I read somewhere?) and of course the certificates. It&#8217;s actually a quick shutdown and restart, so maybe the difference isn&#8217;t so great, but reloading is the way it was meant to be. And it&#8217;s easily done with this command:</p>
<pre># /etc/init.d/sendmail reload &gt; /dev/null</pre>
<p>Redirection to /dev/null silences output (suitable for cron jobs).</p>
<h3>It works!</h3>
<p>There are two ways to see that mails actually arrive with TLS. One is through the mail logs:</p>
<pre>sm-mta: <span class="punch">STARTTLS=server</span>, relay=mail-lj1-f180.google.com [209.85.208.180], version=TLSv1/SSLv3, <span class="punch">verify=FAIL</span>, cipher=ECDHE-RSA-AES128-GCM-SHA256, bits=128/128</pre>
<p>&#8220;STARTTLS=server&#8221; in the log, indicates that a client has connected with STARTTLS for inbound mail. A reminder from above, if it says &#8220;STARTTLS=client&#8221;, it&#8217;s the server that has connected to another one for outbound mail. And along with that, sendmail tells us how the verification of the other side went.</p>
<p>Even easier, the TLS session leaves its tracks in the relevant Received: header in the mail itself:</p>
<pre>Received: from mail-lj1-f180.google.com (mail-lj1-f180.google.com
 [209.85.208.180])	by mx.server.com (8.14.4/8.14.4/Debian-8+deb8u2) with
 ESMTP id 6245JWdJ019568	(<span class="punch">version=TLSv1/SSLv3</span>
 cipher=ECDHE-RSA-AES128-GCM-SHA256 bits=128 <span class="punch">verify=FAIL</span>)	for
 &lt;me@server.com&gt;; Wed, 4 Mar 2026 12:19:34 GMT</pre>
<p>No need to be alarmed about the verify=FAIL part. It just indicates that my server failed to verify the certificates that Gmail sent, which is quite natural, as it has no root certificates to go with. Which, as mentioned above, is intentional. See below for more about the verify=FAIL thing.</p>
<h3>If something goes wrong&#8230;</h3>
<p>In order to obtain debug messages, increase the log level to 14 by adding this to sendmail.mc (and compile with make).</p>
<pre>define(`confLOG_LEVEL', `14')dnl</pre>
<p>This is the log output after a reload (with SIGHUP), ending with STARTTLS working fine:</p>
<pre>sm-mta: restarting /usr/sbin/sendmail-mta due to signal
sm-mta: error: safesasl(/etc/sasl2/Sendmail.conf) failed: No such file or directory
sm-mta: error: safesasl(/etc/sasl/Sendmail.conf) failed: No such file or directory
sm-mta: starting daemon (8.14.4): SMTP+queueing@00:10:00
sm-mta: STARTTLS: CRLFile missing
sm-mta: STARTTLS=server, Diffie-Hellman init, key=1024 bit (1)
sm-mta: <span class="punch">STARTTLS=server, init=1</span>
sm-mta started as: /usr/sbin/sendmail-mta -Am -L sm-mta -bd -q10m</pre>
<p>Except for the fact that there are no errors related to STARTTLS, it says init=1, which is the indication it works.</p>
<p>The complaints about files missing in /etc/sasl2/ can be ignored, as I don&#8217;t use any kind of authentication (i.e. asking the client for credentials).</p>
<h3>Checking the server for real</h3>
<p>The easiest way to test the mail server is with <a rel="noopener" href="https://www.checktls.com/TestReceiver" target="_blank">CheckTLS</a>.</p>
<p>This tool connects to the server and attempts starting a STARTTLS session (but doesn&#8217;t send a mail). The tool shows the details of session with lots of details, along with clarifications on the meaning of those details. So no need to know all the technicalities to get an idea on how you&#8217;re doing. If CheckTLS says all is fine, it&#8217;s really fine. If it says otherwise, take the remarks seriously (but never mind if MTASTS and DANE aren&#8217;t tested and marked yellow).</p>
<p>But even more importantly, CheckTLS gives the details of the certificates that the server provides. This is a good way to verify that the correct certificates are used, and that the certificate chain is valid.</p>
<p>Note however that this only checks how the server behaves when receiving emails. So if something is wrong with how the server sends email, for example it offers a problematic client certificate, that will go undetected.</p>
<p>For a more do-it-yourself approach, the first thing is to check that the server offers STARTTLS:</p>
<pre>$ <strong>nc localhost 25</strong>
220 mx.server.com ESMTP MTA; Wed, 4 Mar 2026 13:14:20 GMT
<strong>EHLO there.com</strong>
250-mx.server.com Hello localhost.localdomain [127.0.0.1], pleased to meet you
250-ENHANCEDSTATUSCODES
250-PIPELINING
250-8BITMIME
250-SIZE
<span class="punch">250-STARTTLS</span>
250-DELIVERBY
250 HELP
^C</pre>
<p>It&#8217;s easier to do this on the local machine, because some ISP block connections to port 25 (to avoid spamming from their IP addresses). Note that it&#8217;s necessary to type an &#8220;EHLO&#8221; command to get the server saying something.</p>
<p>All fine? Connect to the server (see &#8220;man s_client):</p>
<pre>$ <strong>openssl s_client -connect localhost:25 -starttls smtp &lt; /dev/null</strong>
CONNECTED(00000003)
Can't use SSL_get_servername
depth=1 C = US, O = Let's Encrypt, CN = R12
verify error:num=20:unable to get local issuer certificate
verify return:1
depth=0 CN = mx.server.com
verify return:1
---
<span class="punch">Certificate chain</span>
<span class="punch"> 0 s:CN = mx.server.com</span>
<span class="punch">   i:C = US, O = Let's Encrypt, CN = R12</span>
<span class="punch"> 1 s:C = US, O = Let's Encrypt, CN = R12</span>
<span class="punch">   i:C = US, O = Internet Security Research Group, CN = ISRG Root</span> X1

<span class="yadayada">[ ... ]</span></pre>
<p>And then a lot of mumbo-jumbo follows. The certificates sent by the server are listed in the &#8220;Certificate chain&#8221; section. The part above it shows openssl&#8217;s attempts to validate the certification chain. In the case shown above, there was no root certificate available on the server, hence the &#8220;unable to get local issuer certificate&#8221; error.</p>
<p>The reason I focus on the &#8220;Certificate chain&#8221; section is that if a root certificate server is present on the computer that runs openssl, it is listed in the section above it. It will then look like this:</p>
<pre>CONNECTED(00000005)
<span class="punch">depth=2 C = US, O = Internet Security Research Group, CN = ISRG Root X1</span>
<span class="punch">verify return:1</span>
depth=1 C = US, O = Let's Encrypt, CN = R12
verify return:1
depth=0 CN = mx.server.com
verify return:1
---
Certificate chain
 0 s:CN = mx.server.com
   i:C = US, O = Let's Encrypt, CN = R12
 1 s:C = US, O = Let's Encrypt, CN = R12
   i:C = US, O = Internet Security Research Group, CN = ISRG Root X1
---</pre>
<p>So openssl is happy now, but it lists the root certificate which it picked from its own repository. That is a bit confusing when checking the server.</p>
<p>Notes:</p>
<ul>
<li>The exact same test can be run on port 587, if it&#8217;s open for connections.</li>
<li>The reason for the &lt;/dev/null part is that openssl actually opens a netcat-like session, so /dev/null terminates the session right away.</li>
<li>The number in parentheses after CONNECTED is just the file descriptor number of the TCP socket. Nothing really interesting, even though it happens to be different when the certificate chain is validated and when it&#8217;s not.</li>
<li>Add the -showcerts flag to dump all certificates that the server sent, not just the first one. Certificates from the local machine that help validation but weren&#8217;t sent from server are not dumped. For example:
<pre>openssl s_client -connect localhost:25 -starttls smtp -showcerts &lt; /dev/null</pre>
<p>As shown on <a rel="noopener" href="https://billauer.se/blog/2021/04/certificate-ca-tutorial-primer/" target="_blank">a different post of mine</a>, this allows examining them closer by copying <strong>each certificate to a separate file</strong>, and going</p>
<pre>openssl x509 -in thecertificate.crt -text</pre>
</li>
<li>If the verfication of the cerficate fails, openssl establishes the session regardless. The status of this verification is indicated on the output line saying &#8220;Verify return code: 0 (ok)&#8221; for a successful verification, and another number code and message otherwise (this part isn&#8217;t shown above). It&#8217;s also possible to add the -verify_return_error flag, which causes openssl to abort the session if the certificate chain verification fails, and indicate that with an error exit code.</li>
</ul>
<h3>verify=FAIL? Is that a problem?</h3>
<p>The short answer is, no (in my case).</p>
<p>For the longer answer, let&#8217;s start with a brief explanation on the establishment of the TLS session. Encrypted mail delivery, regardless of whether it&#8217;s initiated with a STARTTLS on port 25 or by connecting to port 587, is based upon the <a rel="noopener" href="https://en.wikipedia.org/wiki/Transport_Layer_Security" target="_blank">TLS protocol</a>. This is exactly the same protocol used by web browsers when they establish an https connection.</p>
<p>According to the TLS protocol, the server (as in client / server) is <strong>required</strong> to send its certificate (along with intermediate certificates) in order to prove that it&#8217;s indeed the server of the domain requested (domain as in example.com). The purpose is to avoid a man-in-the-middle attack.</p>
<p>The server <strong>may</strong> request the client to send its certificate (if it has any) in order to identify itself. This might seem odd in a web browser / server connection, but is supported by the commonly used browsers: It&#8217;s possible to supply them with a client certificate, which may be used on websites that request such.</p>
<p>This is why the practical step for making a mail server support STARTTLS is to supply it with a certificate. This certificate is part of the TLS handshake. The thing is, that it&#8217;s verified by the client, not our server. So we don&#8217;t know if the verification was successful or not. The fact that the TLS connection was established doesn&#8217;t mean that the client was happy with the certificate it got. It might have continued setting up the encrypted link regardless. To compare with web browsers, they issue a scary warning when they fail to verify the web server&#8217;s certificate. But that&#8217;s how it is today. In the distant past, the common reaction to an unverified certificate was a slightly different icon in the browser&#8217;s address bar, nothing more.</p>
<p>Likewise, a mail client is likely to continue the TLS handshake if the verification of the server&#8217;s certificate fails. This is a reasonable choice in particular if the client initiated a STARTTLS session, but would have sent the email in cleartext if this option wasn&#8217;t availble: An encrypted session with the possibility of a man-in-the-middle attack is better than sending the mail in cleartext, might be a way to think about it. And when looking at tutorials on the Internet from the early 2000&#8242;s on how to set up a mail server, it&#8217;s quite often suggested to use self-signed certificates, such that can&#8217;t be validated no matter what. This was a reasonable idea before the Let&#8217;s Encrypt era, when a certificate was an expensive thing.</p>
<p>It&#8217;s somewhat amusing that the TLS protocol doesn&#8217;t include a mechanism for the client to say &#8220;listen, your certificate stinks, but I&#8217;ll continue anyhow&#8221;. It could have been useful for server maintainers, but I suppose crypto people didn&#8217;t even want to think about this possibility.</p>
<p>Now to the point: The &#8220;verify&#8221; part in the mail log (as well as in the related Received header row in arriving mails) indicates the result of verifying the other side&#8217;s certificate, if such was requested.</p>
<p>As for the meaning of this attribute, here&#8217;s a copy-paste from the relevant part in the README file mentioned above:</p>
<pre>${verify} holds the result of the verification of the presented cert.
	Possible values are:
	OK	 verification succeeded.
	NO	 no cert presented.
	NOT	 no cert requested.
	FAIL	 cert presented but could not be verified,
		 e.g., the cert of the signing CA is missing.
	NONE	 STARTTLS has not been performed.
	TEMP	 temporary error occurred.
	PROTOCOL protocol error occurred (SMTP level).
	SOFTWARE STARTTLS handshake failed.</pre>
<p>Recall from above that I deliberately didn&#8217;t give my mail server any root certificates, with the intention that all verifications of certificates will fail.</p>
<p>It&#8217;s important to distinguish between two cases:</p>
<ul>
<li>Outbound emails, my server acting as a client, STARTTLS=client in the log: In this case, the certificate is required by the TLS protocol. Had I provided the mail server with root certificates, anything but verify=OK would have indicated a problem, and could have been a reason to terminate the TLS session. As I&#8217;ve already mentioned, the reason I didn&#8217;t provide the root certificates is to ensure that my server won&#8217;t be this picky.</li>
<li>Inbound emails, my server acting as server, STARTTLS=server in the log: The certificate <strong>isn&#8217;t required</strong> to establish the TLS connection, but the server is allowed to ask for it.</li>
</ul>
<p>So there are two normal options:</p>
<ul>
<li>verify=FAIL, meaning that my side got a certificate (as a client in order to establish a TLS session, or as a server because it asked for it), and the other side submitted something in response. And the verification failed, which is normal when you don&#8217;t have a root certificate.</li>
<li>verify=NO, meaning that the other side didn&#8217;t submit any certificate.</li>
</ul>
<p>Had I supplied root certificates to my server, I should have seen verify=OK most of the time for STARTTLS=client, and possibly verify=NO for STARTTLS=server. Would this information help me? Would this help telling spam servers from legit ones? I doubt that.</p>
<p>It&#8217;s a bit questionable why my server asks for certificates it can&#8217;t verify in the STARTTLS=server case, but that&#8217;s the default setting, and I guess this is how normal servers behave. According to the README file, setting the confTLS_SRV_OPTIONS to &#8216;V&#8217; tells the server not to ask clients for certificates. Haven&#8217;t tried that personally, but I suppose one gets verify=<strong>NOT</strong> (as opposed to <strong>NO</strong>).</p>
<p>But what could be the point in asking the client for certificates? One possibility is that Sendmail has an access database, which defines rules for who is allowed to talk with the server. It&#8217;s possible to add rules related to TLS that depend on a successful verification of the certificate by making the rules depend on ${verify}, which is the macro containing the result of this verification (should be &#8220;OK&#8221;). The rules can of course also depend other attributes of the sender. This is more useful for relaying mails inside an organization. More about this in the README file.</p>
<p>To summarize, both verify=FAIL and verify=NO indicate no problem in my case, because the server has no way to validate the client&#8217;s certificate, and this validation isn&#8217;t necessary anyhow in my setting, as the server isn&#8217;t configured to refuse unverified partners by default, and I surely didn&#8217;t change that. It might have been a bit more elegant to set the confTLS_SRV_OPTIONS option to &#8216;V&#8217; to spare clients the pointless task of sending a certificate that isn&#8217;t checked, but I stayed with the default configuration. I try to make a few changes as possible.</p>
<h3>Should my server provide a certificate as a client?</h3>
<p>Short answer, no, because Let&#8217;s Encrypt&#8217;s certificates <a rel="noopener" href="https://letsencrypt.org/2025/05/14/ending-tls-client-authentication" target="_blank">don&#8217;t support TLS client authentication</a> anymore.</p>
<p>Besides, this feature isn&#8217;t required, and opens for a possibility that weird things might happen if the other side doesn&#8217;t manage to verify my certificate for some reason. One could argue that sending certificates improves my server&#8217;s position as a non-spammer, and that it makes my server behave a bit more like Google&#8217;s. However I searched the web for indications that this would improve the spam score, and found none. So even if I had a certificate that allows client authentication, I wouldn&#8217;t use it. And the fact that Let&#8217;s Encrypt phased it out proves the point: I would most likely not have noticed the change, and my server would have sent an inadequate certificate, and now go figure why my mails aren&#8217;t delivered.</p>
<p>That said, it&#8217;s <a rel="noopener" href="https://knowledge.workspace.google.com/admin/gmail/advanced/send-email-over-a-secure-tls-connection" target="_blank">possible to configure Gmail</a> (for business?) to accept mails only from servers that have presented a valid certificate.</p>
<p>Let&#8217;s get a bit technical about this: This is taken from the output of &#8220;openssl x509 -in thecertificate.crt -text&#8221; of a Let&#8217;s Encrypt certificate, issued in March 2026:</p>
<pre>        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                <span class="punch">TLS Web Server Authentication</span>
            X509v3 Basic Constraints: critical
                CA:FALSE</pre>
<p>Compare with the same part on a certificate from Gmail, when acting as a server:</p>
<pre>        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                <span class="punch">TLS Web Server Authentication</span>
            X509v3 Basic Constraints: critical
                CA:FALSE</pre>
<p>So far, the same &#8220;Extended Key Usage&#8221;. But when Gmail&#8217;s server identifies as a client, the relevant part is this:</p>
<pre>        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                <span class="punch">TLS Web Server Authentication, TLS Web Client Authentication</span>
            X509v3 Basic Constraints: critical
                CA:FALSE</pre>
<p>So &#8220;TLS Web Client Authentication&#8221; is a thing, and it&#8217;s not wise to issue a certificate without this option when identifying as a client.</p>
<p>Fun fact, I dug up the certificate for the same server from October 2023 from a backup. And indeed, client authentication was enabled:</p>
<pre>        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                <span class="punch">TLS Web Server Authentication, TLS Web Client Authentication</span>
            X509v3 Basic Constraints: critical
                CA:FALSE</pre>
<p>Not surprising, given the phase-out message from Let&#8217;s Encrypt from the link above. Otherwise, the old and new certificates are pretty much alike.</p>
<p>I&#8217;ve attached the printouts of both Google&#8217;s certificates below for reference. Anyhow, the main takeaways are:</p>
<ul>
<li>Even though it says &#8220;TLS Web Server Authentication&#8221;, it&#8217;s fine for mail servers. I would otherwise think &#8220;Web Server&#8221; refers to https. So Let&#8217;s Encrypt&#8217;s certificates are really legit for a mail server.</li>
<li>When Gmail acts as a client, it indeed has the TLS Web Client Authentication option</li>
<li>Don&#8217;t try using Let&#8217;s Encrypt&#8217;s certificates for client authentication.</li>
</ul>
<h3>The script splitting certificates</h3>
<p>I promised the script that splits the PEM certificate file obtained by bacme from Let&#8217;s Encrypt into my own certificate and the intermediate certificate. I do such things in Perl, so here it is:</p>
<pre><span class="hljs-comment">#!/usr/bin/perl</span>
<span class="hljs-keyword">use</span> warnings;
<span class="hljs-keyword">use</span> strict;

<span class="hljs-keyword">local</span> $/; <span class="hljs-comment"># Slurp mode</span>

<span class="hljs-keyword">my</span> $cert = &lt;&gt;;

<span class="hljs-keyword">my</span> @chunks = ($cert =~ <span class="hljs-regexp">/(-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----)/gs</span>);

<span class="hljs-keyword">my</span> $found = @chunks;

<span class="hljs-keyword">die</span>(<span class="hljs-string">"$0: Expected to find two certificates, found $found instead.\n"</span>)
  <span class="hljs-keyword">unless</span> ($found == <span class="hljs-number">2</span>);

writefile(<span class="hljs-string">"my.pem"</span>, <span class="hljs-string">"$chunks[0]\n"</span>);
writefile(<span class="hljs-string">"ca.pem"</span>, <span class="hljs-string">"$chunks[1]\n"</span>);

<span class="hljs-keyword">exit</span>(<span class="hljs-number">0</span>);

<span class="hljs-function"><span class="hljs-keyword">sub</span> <span class="hljs-title">writefile</span> </span>{
  <span class="hljs-keyword">my</span> ($fname, $data) = @_;

  <span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $out, <span class="hljs-string">"&gt;"</span>, $fname)
    <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open \"$fname\" for write: $!\n"</span>;
  <span class="hljs-keyword">print</span> $out $data;
  <span class="hljs-keyword">close</span> $out;
}</pre>
<p>I run this as a regular user, not root, which is why I&#8217;m relatively sloppy with just writing out a couple of files in the current directory. Even though the hardcoded filenames makes this rather safe anyhow.</p>
<p>This script is given the combined PEM file through standard input (or with the file name as the first argument), and emits the two PEM files to the current directory. Deliberately unsophisticated, and deliberately very picky about the existence of exactly two certificates in the input, so I get notified if something in Let&#8217;s Encrypt&#8217;s setting suddenly changes.</p>
<p>For example, in the old Let&#8217;s Encrypt certificate from 2023 I mentioned above, there were three certificates. The third certificate affirmed ISRG Root X1 with the help of DST Root CA X3, the latter considered the root certificate at the time. The former is nowadays an established root certificate by itself, hence a third certificate unnecessary. But it can change, and if it does, I suppose the solution will be to concatenate everything but the first certificate into ca.pem. And if that happens, I want to be aware of the change and verify that the server gives the correct intermediate certificates.</p>
<h3>Summary</h3>
<p>After all said and done, I could have just split the certificate from Let&#8217;s Encrypt into two as shown above, and added the sendmail configuration options mentioned everywhere on tutorials, and everything would have been just fine. And had I used certbot, like everyone else, I would have had the ready-to-used certificate files directly.</p>
<p>As it turns out, there was no real need to delve into the details. Sendmail does the right thing anyhow. But understanding what&#8217;s going on under the hood is still better, and worth the effort, I think. In particular with a crucial component like sendmail.</p>
<h3><span class="yadayada">Appendix: The Gmail&#8217;s client certificate</span></h3>
<p><span class="yadayada">As it was quite difficult to obtain this certificate (running tcpdump on my server, feeding the result to Wireshark, exporting the certificate as raw bytes and opening the file with openssl as DER), I thought I&#8217;d show its printout:</span></p>
<pre><span class="yadayada">    Data:
        Version: 3 (0x2)
        Serial Number:
            2b:64:a8:5a:82:a3:d2:c2:10:5b:9b:25:ab:75:c1:af
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, O = Google Trust Services, CN = WR4
        Validity
            Not Before: Feb 10 17:58:28 2026 GMT
            Not After : May 11 17:58:27 2026 GMT
        Subject: CN = </span><span class="punch">smtp.gmail.com</span><span class="yadayada">
        Subject Public Key Info:
            Public Key Algorithm: rsaEncryption
                RSA Public-Key: (2048 bit)
                Modulus:
                    00:9e:75:cf:b1:84:c9:a8:f2:bb:c8:89:fe:ef:09:
                    ad:71:d7:2a:1e:e2:b0:51:e2:0b:d5:b9:a7:52:70:
                    e8:c1:ff:5b:60:b6:7c:65:c0:b1:8b:90:cb:cd:ab:
                    0c:da:ef:10:8f:17:79:ed:a5:b9:95:57:f2:28:f2:
                    da:3d:d3:1d:ed:03:a2:6f:88:da:7f:0c:cc:b9:f4:
                    f6:44:ac:bc:fa:95:62:c0:7b:31:8d:44:9c:3f:bf:
                    cf:05:66:8b:a2:7d:9a:dd:af:2b:dc:05:16:b8:37:
                    3c:1f:c5:23:9f:4d:2b:15:a4:97:87:ab:a7:70:3a:
                    4a:5d:2a:8d:d4:21:1a:68:48:da:74:89:6e:1a:27:
                    2f:ef:06:4b:38:b5:65:5f:c4:da:49:96:c5:4e:9f:
                    78:7f:cb:2b:6a:61:ff:f7:0f:f6:f3:d4:d0:7d:94:
                    84:a8:0c:21:8a:a2:a4:20:04:f7:83:ac:00:83:85:
                    eb:9e:01:7a:ea:a5:2a:b9:89:3b:ad:94:2d:c4:c1:
                    2f:49:86:17:52:f7:85:1a:97:76:9d:2f:cf:c4:20:
                    a3:9c:c5:7b:74:57:28:f2:35:d8:ab:fa:d8:53:b9:
                    ee:c9:24:cb:f3:aa:d4:0b:f9:1a:8e:3d:b9:ad:16:
                    7c:99:7c:40:ef:3f:25:5a:c7:94:87:e8:20:bb:19:
                    92:6f
                Exponent: 65537 (0x10001)
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature, Key Encipherment
            X509v3 Extended Key Usage:
                </span><span class="punch">TLS Web Server Authentication, TLS Web Client Authentication</span><span class="yadayada">
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                65:7C:AF:FE:54:FD:A3:0A:53:90:AB:9A:94:E7:AD:DF:DC:B9:8B:58
            X509v3 Authority Key Identifier:
                keyid:9B:C8:11:BC:3D:AA:36:B9:31:8C:4E:8F:44:D5:57:32:2F:C3:C0:61

            Authority Information Access:
                OCSP - URI:http://o.pki.goog/s/wr4/K2Q
                CA Issuers - URI:http://i.pki.goog/wr4.crt

            X509v3 Subject Alternative Name:
                DNS:smtp.gmail.com
            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.1

            X509v3 CRL Distribution Points: 

                Full Name:
                  URI:http://c.pki.goog/wr4/F-WFK5nQurE.crl

            CT Precertificate SCTs:
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : 96:97:64:BF:55:58:97:AD:F7:43:87:68:37:08:42:77:
                                E9:F0:3A:D5:F6:A4:F3:36:6E:46:A4:3F:0F:CA:A9:C6
                    Timestamp : Feb 10 18:58:35.396 2026 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:46:02:21:00:BE:28:85:4E:52:7D:B5:FC:0C:C7:FA:
                                26:98:AE:D5:C4:86:E1:E1:70:A6:6A:3C:CA:CE:9E:21:
                                17:27:D4:09:BE:02:21:00:89:B7:00:57:51:76:41:FB:
                                D3:73:9B:27:FA:E1:40:2F:51:E1:4F:14:D1:65:18:EE:
                                81:C7:7C:A1:60:BA:6A:BF
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : CB:38:F7:15:89:7C:84:A1:44:5F:5B:C1:DD:FB:C9:6E:
                                F2:9A:59:CD:47:0A:69:05:85:B0:CB:14:C3:14:58:E7
                    Timestamp : Feb 10 18:58:35.450 2026 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:45:02:21:00:B0:B1:6E:A6:C2:1B:49:2A:28:2C:C9:
                                EC:AE:C6:F9:F4:EC:89:64:AC:88:6A:BE:08:86:09:36:
                                17:66:63:49:D0:02:20:5C:CE:E6:21:C3:21:88:15:E1:
                                D9:17:13:D6:0B:E3:F6:54:71:58:C9:55:9F:DA:14:63:
                                F8:69:F1:BC:DD:4B:32
    Signature Algorithm: sha256WithRSAEncryption
         8f:fa:cf:2b:ab:6a:66:07:2a:32:ae:15:39:c8:bf:a6:22:e1:
         b1:55:6d:1f:04:26:4b:34:54:fe:91:cd:61:92:6c:b1:2a:8b:
         47:81:28:84:ee:d1:b7:c2:fc:da:81:fd:74:c4:bf:6e:ba:f1:
         ef:b2:81:77:f1:0b:80:73:78:e1:86:1f:92:c8:92:a7:45:e6:
         26:93:4d:92:a2:2b:d2:02:db:1c:b8:81:4e:56:79:bc:4a:f6:
         8c:6c:f3:2a:a8:09:b2:5f:c2:74:bb:2d:74:0b:ea:3a:50:e7:
         dd:33:61:fa:ed:df:6c:ed:6e:ba:50:8c:54:9d:19:76:03:1b:
         56:7e:55:be:ee:3f:a3:c5:d6:ad:6b:fc:1b:43:ce:aa:50:52:
         af:f6:83:f0:38:f5:62:8d:0b:91:f3:72:f1:b7:10:64:1a:ca:
         02:97:8e:f9:13:a3:5d:1a:1b:ee:5d:01:dd:b0:48:f2:f3:30:
         cf:8d:6a:98:21:8d:83:23:38:c7:80:22:59:97:f0:45:76:fb:
         8c:a9:4e:f8:37:38:de:ba:4e:94:c5:1f:b1:d0:3c:87:69:11:
         ea:90:0d:75:72:82:5a:a3:c3:99:c6:e5:ce:57:05:ed:63:a9:
         2e:20:ab:b6:41:8c:53:e1:92:5c:55:de:bf:3b:d1:d3:ec:08:
         a8:87:9e:c0
-----BEGIN CERTIFICATE-----
MIIFMjCCBBqgAwIBAgIQK2SoWoKj0sIQW5slq3XBrzANBgkqhkiG9w0BAQsFADA7
MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVR29vZ2xlIFRydXN0IFNlcnZpY2VzMQww
CgYDVQQDEwNXUjQwHhcNMjYwMjEwMTc1ODI4WhcNMjYwNTExMTc1ODI3WjAZMRcw
FQYDVQQDEw5zbXRwLmdtYWlsLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
AQoCggEBAJ51z7GEyajyu8iJ/u8JrXHXKh7isFHiC9W5p1Jw6MH/W2C2fGXAsYuQ
y82rDNrvEI8Xee2luZVX8ijy2j3THe0Dom+I2n8MzLn09kSsvPqVYsB7MY1EnD+/
zwVmi6J9mt2vK9wFFrg3PB/FI59NKxWkl4erp3A6Sl0qjdQhGmhI2nSJbhonL+8G
Szi1ZV/E2kmWxU6feH/LK2ph//cP9vPU0H2UhKgMIYqipCAE94OsAIOF654Beuql
KrmJO62ULcTBL0mGF1L3hRqXdp0vz8Qgo5zFe3RXKPI12Kv62FO57skky/Oq1Av5
Go49ua0WfJl8QO8/JVrHlIfoILsZkm8CAwEAAaOCAlIwggJOMA4GA1UdDwEB/wQE
AwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIwDAYDVR0TAQH/BAIw
ADAdBgNVHQ4EFgQUZXyv/lT9owpTkKualOet39y5i1gwHwYDVR0jBBgwFoAUm8gR
vD2qNrkxjE6PRNVXMi/DwGEwXgYIKwYBBQUHAQEEUjBQMCcGCCsGAQUFBzABhhto
dHRwOi8vby5wa2kuZ29vZy9zL3dyNC9LMlEwJQYIKwYBBQUHMAKGGWh0dHA6Ly9p
LnBraS5nb29nL3dyNC5jcnQwGQYDVR0RBBIwEIIOc210cC5nbWFpbC5jb20wEwYD
VR0gBAwwCjAIBgZngQwBAgEwNgYDVR0fBC8wLTAroCmgJ4YlaHR0cDovL2MucGtp
Lmdvb2cvd3I0L0YtV0ZLNW5RdXJFLmNybDCCAQUGCisGAQQB1nkCBAIEgfYEgfMA
8QB3AJaXZL9VWJet90OHaDcIQnfp8DrV9qTzNm5GpD8PyqnGAAABnEjrcQQAAAQD
AEgwRgIhAL4ohU5SfbX8DMf6Jpiu1cSG4eFwpmo8ys6eIRcn1Am+AiEAibcAV1F2
QfvTc5sn+uFAL1HhTxTRZRjugcd8oWC6ar8AdgDLOPcViXyEoURfW8Hd+8lu8ppZ
zUcKaQWFsMsUwxRY5wAAAZxI63E6AAAEAwBHMEUCIQCwsW6mwhtJKigsyeyuxvn0
7IlkrIhqvgiGCTYXZmNJ0AIgXM7mIcMhiBXh2RcT1gvj9lRxWMlVn9oUY/hp8bzd
SzIwDQYJKoZIhvcNAQELBQADggEBAI/6zyuramYHKjKuFTnIv6Yi4bFVbR8EJks0
VP6RzWGSbLEqi0eBKITu0bfC/NqB/XTEv2668e+ygXfxC4BzeOGGH5LIkqdF5iaT
TZKiK9IC2xy4gU5WebxK9oxs8yqoCbJfwnS7LXQL6jpQ590zYfrt32ztbrpQjFSd
GXYDG1Z+Vb7uP6PF1q1r/BtDzqpQUq/2g/A49WKNC5HzcvG3EGQaygKXjvkTo10a
G+5dAd2wSPLzMM+NapghjYMjOMeAIlmX8EV2+4ypTvg3ON66TpTFH7HQPIdpEeqQ
DXVyglqjw5nG5c5XBe1jqS4gq7ZBjFPhklxV3r870dPsCKiHnsA=
-----END CERTIFICATE-----</span></pre>
<p><span class="yadayada">For comparison, this is the certificate obtained on the same day, when Gmail responded as a server:</span></p>
<pre><span class="yadayada">    Data:
        Version: 3 (0x2)
        Serial Number:
            e9:b6:68:79:fa:91:bf:49:10:8d:b9:1e:cc:e8:63:b0
        Signature Algorithm: sha256WithRSAEncryption
        Issuer: C = US, O = Google Trust Services, CN = WR2
        Validity
            Not Before: Feb  2 08:37:58 2026 GMT
            Not After : Apr 27 08:37:57 2026 GMT
        Subject: CN = </span><span class="punch">mx.google.com</span><span class="yadayada">
        Subject Public Key Info:
            Public Key Algorithm: id-ecPublicKey
                Public-Key: (256 bit)
                pub:
                    04:55:ba:49:43:8f:d9:72:9d:f9:d0:fa:1c:76:ec:
                    73:44:39:69:e7:21:68:49:1f:d0:0e:c4:70:bb:1f:
                    61:15:71:58:a7:44:df:bd:9f:d5:f6:e9:d1:8a:77:
                    73:79:ac:82:e7:30:88:53:95:62:ff:f3:cd:32:71:
                    9e:68:21:a7:62
                ASN1 OID: prime256v1
                NIST CURVE: P-256
        X509v3 extensions:
            X509v3 Key Usage: critical
                Digital Signature
            X509v3 Extended Key Usage:
                </span><span class="punch">TLS Web Server Authentication</span><span class="yadayada">
            X509v3 Basic Constraints: critical
                CA:FALSE
            X509v3 Subject Key Identifier:
                51:B6:13:35:D8:FB:85:27:72:70:77:EE:D7:5B:1D:06:5E:63:FD:51
            X509v3 Authority Key Identifier:
                keyid:DE:1B:1E:ED:79:15:D4:3E:37:24:C3:21:BB:EC:34:39:6D:42:B2:30

            Authority Information Access:
                OCSP - URI:http://o.pki.goog/wr2
                CA Issuers - URI:http://i.pki.goog/wr2.crt

            X509v3 Subject Alternative Name:
</span><span class="punch">                DNS:mx.google.com, DNS:smtp.google.com, DNS:aspmx.l.google.com, DNS:alt1.aspmx.l.google.com, DNS:alt2.aspmx.l.google.com, DNS:alt3.aspmx.l.google.com, DNS:alt4.aspmx.l.google.com, DNS:gmail-smtp-in.l.google.com, DNS:alt1.gmail-smtp-in.l.google.com, DNS:alt2.gmail-smtp-in.l.google.com, DNS:alt3.gmail-smtp-in.l.google.com, DNS:alt4.gmail-smtp-in.l.google.com, DNS:gmr-smtp-in.l.google.com, DNS:alt1.gmr-smtp-in.l.google.com, DNS:alt2.gmr-smtp-in.l.google.com, DNS:alt3.gmr-smtp-in.l.google.com, DNS:alt4.gmr-smtp-in.l.google.com, DNS:mx1.smtp.goog, DNS:mx2.smtp.goog, DNS:mx3.smtp.goog, DNS:mx4.smtp.goog, DNS:aspmx2.googlemail.com, DNS:aspmx3.googlemail.com, DNS:aspmx4.googlemail.com, DNS:aspmx5.googlemail.com, DNS:gmr-mx.google.com
</span><span class="yadayada">            X509v3 Certificate Policies:
                Policy: 2.23.140.1.2.1

            X509v3 CRL Distribution Points: 

                Full Name:
                  URI:http://c.pki.goog/wr2/oQ6nyr8F0m0.crl

            CT Precertificate SCTs:
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : D1:6E:A9:A5:68:07:7E:66:35:A0:3F:37:A5:DD:BC:03:
                                A5:3C:41:12:14:D4:88:18:F5:E9:31:B3:23:CB:95:04
                    Timestamp : Feb  2 09:38:00.395 2026 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:45:02:20:21:B9:8B:BD:E8:4E:B3:F4:24:46:6B:25:
                                17:CF:53:2E:2E:B7:83:A3:F5:DB:7B:F7:91:70:62:A2:
                                D5:74:B8:20:02:21:00:C9:3D:D4:79:5C:05:59:7C:68:
                                ED:6F:EA:45:59:55:D5:A6:9B:F8:9B:A3:62:AD:8B:2B:
                                30:A0:CC:4A:62:A1:EB
                Signed Certificate Timestamp:
                    Version   : v1 (0x0)
                    Log ID    : 96:97:64:BF:55:58:97:AD:F7:43:87:68:37:08:42:77:
                                E9:F0:3A:D5:F6:A4:F3:36:6E:46:A4:3F:0F:CA:A9:C6
                    Timestamp : Feb  2 09:38:00.184 2026 GMT
                    Extensions: none
                    Signature : ecdsa-with-SHA256
                                30:44:02:20:3A:11:AE:85:B9:06:AF:A9:EF:88:25:64:
                                EB:2A:F3:4B:07:50:AF:B9:63:0F:4C:7A:B0:13:F4:CA:
                                0E:58:55:B7:02:20:50:81:1C:CF:06:47:39:AF:8A:F3:
                                27:00:78:34:FD:40:3F:1E:36:E3:2E:42:08:8E:14:B0:
                                09:B0:CA:CE:FD:B9
    Signature Algorithm: sha256WithRSAEncryption
         5e:bf:fc:22:aa:45:d9:35:37:c7:f3:9b:95:5a:e1:eb:2d:72:
         70:ba:ea:c5:ce:10:2e:53:b6:da:f0:54:77:f4:f4:7d:43:df:
         ff:fe:45:18:f3:cb:85:1c:ae:df:0d:a3:10:f1:01:7a:6f:81:
         03:af:c8:1c:d9:26:2b:4d:69:c1:4a:ef:bf:e2:98:cb:a8:c6:
         42:fe:78:4f:d9:82:d9:2c:39:fc:3e:d3:c2:6f:de:b8:e6:dc:
         82:51:04:00:0d:13:1d:2b:0e:fd:2f:56:7c:bf:73:a6:35:46:
         85:12:99:99:1f:1e:cb:9c:a5:e3:64:7f:b0:66:45:f5:ba:97:
         f0:ac:88:41:7e:c7:b0:7d:7f:04:15:c6:8b:0f:58:cd:19:1e:
         fb:b2:8c:f4:a6:dd:7f:8c:84:98:12:49:60:1b:20:c8:14:da:
         b1:fe:11:06:09:be:92:6b:cc:33:cd:e1:93:7c:bd:ca:1c:c9:
         70:71:cf:46:60:6c:db:22:72:9c:0d:00:e0:6a:72:bc:32:13:
         11:f0:8d:2f:95:d5:d9:20:76:9b:86:dd:73:10:8f:fc:a9:51:
         de:1c:90:d2:c8:a6:f9:ff:ab:a9:a8:5f:75:56:ae:a9:25:6a:
         7f:37:ff:67:5e:53:4e:2b:b7:c0:72:3c:9c:1b:68:f9:9a:0a:
         ef:60:6f:f2
-----BEGIN CERTIFICATE-----
MIIGxDCCBaygAwIBAgIRAOm2aHn6kb9JEI25HszoY7AwDQYJKoZIhvcNAQELBQAw
OzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFUdvb2dsZSBUcnVzdCBTZXJ2aWNlczEM
MAoGA1UEAxMDV1IyMB4XDTI2MDIwMjA4Mzc1OFoXDTI2MDQyNzA4Mzc1N1owGDEW
MBQGA1UEAxMNbXguZ29vZ2xlLmNvbTBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IA
BFW6SUOP2XKd+dD6HHbsc0Q5aechaEkf0A7EcLsfYRVxWKdE372f1fbp0Yp3c3ms
gucwiFOVYv/zzTJxnmghp2KjggSvMIIEqzAOBgNVHQ8BAf8EBAMCB4AwEwYDVR0l
BAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUUbYTNdj7hSdy
cHfu11sdBl5j/VEwHwYDVR0jBBgwFoAU3hse7XkV1D43JMMhu+w0OW1CsjAwWAYI
KwYBBQUHAQEETDBKMCEGCCsGAQUFBzABhhVodHRwOi8vby5wa2kuZ29vZy93cjIw
JQYIKwYBBQUHMAKGGWh0dHA6Ly9pLnBraS5nb29nL3dyMi5jcnQwggKGBgNVHREE
ggJ9MIICeYINbXguZ29vZ2xlLmNvbYIPc210cC5nb29nbGUuY29tghJhc3BteC5s
Lmdvb2dsZS5jb22CF2FsdDEuYXNwbXgubC5nb29nbGUuY29tghdhbHQyLmFzcG14
LmwuZ29vZ2xlLmNvbYIXYWx0My5hc3BteC5sLmdvb2dsZS5jb22CF2FsdDQuYXNw
bXgubC5nb29nbGUuY29tghpnbWFpbC1zbXRwLWluLmwuZ29vZ2xlLmNvbYIfYWx0
MS5nbWFpbC1zbXRwLWluLmwuZ29vZ2xlLmNvbYIfYWx0Mi5nbWFpbC1zbXRwLWlu
LmwuZ29vZ2xlLmNvbYIfYWx0My5nbWFpbC1zbXRwLWluLmwuZ29vZ2xlLmNvbYIf
YWx0NC5nbWFpbC1zbXRwLWluLmwuZ29vZ2xlLmNvbYIYZ21yLXNtdHAtaW4ubC5n
b29nbGUuY29tgh1hbHQxLmdtci1zbXRwLWluLmwuZ29vZ2xlLmNvbYIdYWx0Mi5n
bXItc210cC1pbi5sLmdvb2dsZS5jb22CHWFsdDMuZ21yLXNtdHAtaW4ubC5nb29n
bGUuY29tgh1hbHQ0Lmdtci1zbXRwLWluLmwuZ29vZ2xlLmNvbYINbXgxLnNtdHAu
Z29vZ4INbXgyLnNtdHAuZ29vZ4INbXgzLnNtdHAuZ29vZ4INbXg0LnNtdHAuZ29v
Z4IVYXNwbXgyLmdvb2dsZW1haWwuY29tghVhc3BteDMuZ29vZ2xlbWFpbC5jb22C
FWFzcG14NC5nb29nbGVtYWlsLmNvbYIVYXNwbXg1Lmdvb2dsZW1haWwuY29tghFn
bXItbXguZ29vZ2xlLmNvbTATBgNVHSAEDDAKMAgGBmeBDAECATA2BgNVHR8ELzAt
MCugKaAnhiVodHRwOi8vYy5wa2kuZ29vZy93cjIvb1E2bnlyOEYwbTAuY3JsMIIB
AwYKKwYBBAHWeQIEAgSB9ASB8QDvAHYA0W6ppWgHfmY1oD83pd28A6U8QRIU1IgY
9ekxsyPLlQQAAAGcHbdWSwAABAMARzBFAiAhuYu96E6z9CRGayUXz1MuLreDo/Xb
e/eRcGKi1XS4IAIhAMk91HlcBVl8aO1v6kVZVdWmm/ibo2KtiyswoMxKYqHrAHUA
lpdkv1VYl633Q4doNwhCd+nwOtX2pPM2bkakPw/KqcYAAAGcHbdVeAAABAMARjBE
AiA6Ea6FuQavqe+IJWTrKvNLB1CvuWMPTHqwE/TKDlhVtwIgUIEczwZHOa+K8ycA
eDT9QD8eNuMuQgiOFLAJsMrO/bkwDQYJKoZIhvcNAQELBQADggEBAF6//CKqRdk1
N8fzm5Va4estcnC66sXOEC5TttrwVHf09H1D3//+RRjzy4Ucrt8NoxDxAXpvgQOv
yBzZJitNacFK77/imMuoxkL+eE/ZgtksOfw+08Jv3rjm3IJRBAANEx0rDv0vVny/
c6Y1RoUSmZkfHsucpeNkf7BmRfW6l/CsiEF+x7B9fwQVxosPWM0ZHvuyjPSm3X+M
hJgSSWAbIMgU2rH+EQYJvpJrzDPN4ZN8vcocyXBxz0ZgbNsicpwNAOBqcrwyExHw
jS+V1dkgdpuG3XMQj/ypUd4ckNLIpvn/q6moX3VWrqklan83/2deU04rt8ByPJwb
aPmaCu9gb/I=
-----END CERTIFICATE-----</span></pre>
<p><span class="yadayada">Note the long list of alternative names. I wasn&#8217;t sure if they are respected by mail servers as well, but here they are. I fetched this certificate from alt1.gmail-smtp-in.l.google.com, actually, and not &#8220;directly&#8221; from mx.google.com.</span></p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2026/03/tls-sendmail-lets-encrypt/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Un-ignore /usr/lib/systemd/ in .gitignore with git repo on root filesystem</title>
		<link>https://billauer.se/blog/2025/12/git-unignore-subdirectory/</link>
		<comments>https://billauer.se/blog/2025/12/git-unignore-subdirectory/#comments</comments>
		<pubDate>Tue, 23 Dec 2025 14:27:35 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[systemd]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7183</guid>
		<description><![CDATA[Actually, this is about un-ignoring any subdirectory that is grandchild to an ignored directory. Running Linux Mint 22.2 (based upon Ubuntu 24.04), and having a git repository on root filesystem to keep track of the computer&#8217;s configuration, the vast majority of directories are ignored. One of the is /lib, however /lib/systemd/ should not be ignored, [...]]]></description>
			<content:encoded><![CDATA[<p>Actually, this is about un-ignoring any subdirectory that is grandchild to an ignored directory.</p>
<p>Running Linux Mint 22.2 (based upon Ubuntu 24.04), and having a git repository on root filesystem to keep track of the computer&#8217;s configuration, the vast majority of directories are ignored. One of the is /lib, however /lib/systemd/ should not be ignored, as it contains crucial files for the system&#8217;s configuration.</p>
<p>On other distributions, the relevant part in .gitignore usually goes:</p>
<pre><span class="yadayada">[ ... ]</span>
bin/
boot/
dev/
home/
<span class="punch">lib/*
!lib/systemd/
</span>lib64/
lib32/
libx32/
lost+found/
media/
mnt/
opt/
proc/
root/
run/
sbin/
<span class="yadayada">[ ... ]</span></pre>
<p>So lib/ isn&#8217;t ignored as a directory, but all its content, including subdirectories is. That allows for un-ignoring lib/systemd/ on the following row. That&#8217;s why lib/ isn&#8217;t ignore-listed like the other ones.</p>
<p>But on Linux Mint 22.2, /lib is a symbolic link to /usr/lib. And since git treats a symbolic link just like a file, /lib/systemd/ is treated as /usr/lib/systemd. Ignoring /lib as a directory has no effect, and un-ignoring /lib/systemd has no effect, because to git, this directory doesn&#8217;t even exist.</p>
<p>So go</p>
<pre>$ <strong>man gitignore</strong></pre>
<p>and try to figure out what to do. It&#8217;s quite difficult actually, but it boils down to this:</p>
<pre>usr/*
!usr/lib/
usr/lib/*
!usr/lib/systemd/</pre>
<p>It&#8217;s a bit tangled, but the point is that /usr/lib is un-ignored, then all its files are ignored, and then /usr/lib/systemd is un-ignored.</p>
<p>The only good part about this solution is that it works.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2025/12/git-unignore-subdirectory/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Footnote and whole-page layout with wkhtmltopdf</title>
		<link>https://billauer.se/blog/2025/11/wkhtmltopdf-footnote/</link>
		<comments>https://billauer.se/blog/2025/11/wkhtmltopdf-footnote/#comments</comments>
		<pubDate>Sat, 29 Nov 2025 11:58:40 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7178</guid>
		<description><![CDATA[This HTML code makes wkhtmltopdf create a single page with a footnote. If the external &#60;div&#62; is duplicated, separate pages are generated. &#60;html&#62; &#60;head&#62; &#60;meta http-equiv="Content-Type" content="text/html; charset=utf-8" /&#62; &#60;/head&#62; &#60;body&#62; &#60;div style="height: 1350px; display: flex; flex-direction: column; break-inside: avoid; border:1px solid #668;"&#62; This is just a test. &#60;div style="margin-top: auto;"&#62; This is a footnote [...]]]></description>
			<content:encoded><![CDATA[<p>This HTML code makes wkhtmltopdf create a single page with a footnote. If the external &lt;div&gt; is duplicated, separate pages are generated.</p>
<pre><span class="hljs-tag">&lt;<span class="hljs-name">html</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"Content-Type"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"text/html; charset=utf-8"</span> /&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"height: 1350px; display: flex; flex-direction: column; break-inside: avoid; border:1px solid #668;"</span>&gt;</span>
This is just a test.
<span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">style</span>=<span class="hljs-string">"margin-top: auto;"</span>&gt;</span>
This is a footnote
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span>
<span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></pre>
<p>So how does it work? The important part is the &#8220;style&#8221; attribute of the outer &lt;div&gt; tag:</p>
<ul>
<li><strong>height: 1350px</strong>: This sets the &lt;div&gt; block&#8217;s height to a full A4 page. Why 1350 pixels? I don&#8217;t know. I just tweaked with this figure until it got right. It&#8217;s possible another figure is needed on a different version of wkhtmltopdf. I&#8217;ve tried to set this with cm as well as pt units, but none corresponded to the standard figures for an A4 page. So I went with pixels, which clarifies that it&#8217;s a wild guess.</li>
<li><strong>display: flex; flex-direction: column</strong>: This turns this &lt;div&gt; block into a Flexbox container, with vertical packing. This is needed to push the footnote&#8217;s block to the bottom.</li>
<li><strong>break-inside: avoid</strong>: This tells wkhtmltopdf to avoid page breaks in the middle of the block. This makes no difference for a single page, but if this &lt;div&gt; block is repeated, this style attribute ensures that each block gets a separate page (unless any of the pages exceeds a page&#8217;s height).</li>
<li><strong>border:1px solid #668</strong>: This generates a border around the &lt;div&gt; block&#8217;s occupied area. Used only for finding the correct height attribute, and should should be removed afterwards (unless this border is desired on every page).</li>
</ul>
<p>The footnote is pushed to the bottom of the page by virtue of the <strong>margin-top: auto</strong> style attribute and the fact that the &lt;div&gt; block having this attribute is within a vertical packed Flexbox container.</p>
<p><strong>Notes:</strong></p>
<ul>
<li>This was done with wkhtmltopdf 0.12.4, without the &#8220;wkhtmltopdf patches&#8221; according to the man page.</li>
<li>If the height is too large on any page, all break-inside are ignored. In other words, the whole pdf document gets garbled, not just around the page that went wrong.</li>
<li>I tried changing the resolution on my X11 display, and it didn&#8217;t make any difference. This might sound like a silly thing to check, but wkhtmltopdf depends on the X11 server.</li>
</ul>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2025/11/wkhtmltopdf-footnote/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Notes on installing Linux Mint 22.2 (full disk encryption)</title>
		<link>https://billauer.se/blog/2025/10/install-linux-efi-laptop-luks/</link>
		<comments>https://billauer.se/blog/2025/10/install-linux-efi-laptop-luks/#comments</comments>
		<pubDate>Wed, 08 Oct 2025 12:37:35 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7171</guid>
		<description><![CDATA[Introduction These are my notes to self for the next time I install a Linux system. As if I read my previous posts before attempting. So I installed Linux Mint 22.2 (kernel 6.14.0-29) on a Lenovo V14 G4 IRU laptop. With Cinnamon, of course, not that it&#8217;s relevant. All that I wanted was a full-disk [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>These are my notes to self for the next time I install a Linux system. As if I read my <a rel="noopener" href="https://billauer.se/blog/2018/11/linux-grub-uefi-raid-luks-lvm/" target="_blank">previous</a> <a rel="noopener" href="https://billauer.se/blog/2010/01/howto-lvm-dm-crypt-raid-5-mdadm-fc12-fedora/" target="_blank">posts</a> before attempting.</p>
<p>So I installed Linux Mint 22.2 (kernel 6.14.0-29) on a Lenovo V14 G4 IRU laptop. With Cinnamon, of course, not that it&#8217;s relevant.</p>
<p>All that I wanted was a full-disk encryption, but being allowed to choose the setup of the partitions explicitly, and not let the installation wizard make the choices for me. In particular, I wanted a swap partition with the size I choose, and even more important: Almost all disk space in a /storage mount, so that one can fill the hard disk with junk without risking a system failure because root partition is full.</p>
<h3>Cutting the cake</h3>
<p>/boot/efi is where the BIOS reads from. It&#8217;s natural to put it as the first partition, and it can be very small (even 1 MB can be enough in some cases, but let&#8217;s not push it like I eventually did). But if you make it really small, it&#8217;s a FAT16 partition, not FAT32, and that&#8217;s OK. Don&#8217;t force it into FAT32, because the system won&#8217;t boot with it if it has less clusters than required.</p>
<p>So it goes like this:</p>
<ul>
<li>Create three partitions:
<ul>
<li>First one for /boot/efi (e.g. nvme0n1p1), 10 MB. This must be FAT32 or FAT16 (the latter for 10MB). Note that 10MB is a bit too small, because the BIOS won&#8217;t have room for its own backup this way.</li>
<li>Second one for /boot, will contain initramfs images, so ~500 MB. Any filesystem that GRUB can read (so ext4 is definitely OK)</li>
<li>Third partition for LUKS</li>
</ul>
</li>
<li>In the LUKS partition, create an LVM with partitions for / (100G) and swap. The rest is for /storage.</li>
</ul>
<p>It&#8217;s somewhat confusing that /boot/efi is a subdirectory of /boot, but that&#8217;s the way it is.</p>
<h3>Running the installation wizard</h3>
<ul>
<li>Unlock the encrypted partition, if it&#8217;s not already (e.g. with the &#8220;Disks&#8221; GUI utility). This requires giving the passphrase, of course.</li>
<li>Double-click the &#8220;Install Linux Mint&#8221; icon on the desktop.</li>
<li>When reaching the &#8220;Installation type&#8221;, pick &#8220;Something else&#8221;.</li>
<li>Set the following mount points:
<ul>
<li>Set the small FAT partition (nvme0n1p1 in my case) as &#8220;EFI System Partition&#8221;</li>
<li>/boot on the partition allocated for that (non-encrypted partition, possibly ext4).</li>
<li>/ on the relevant LVM partition inside the encrypted block</li>
<li>Set the swap partition</li>
</ul>
</li>
<li>Set the &#8220;Device for boot loader installation&#8221; to the one allocated for &#8220;EFI System Partition&#8221; (nvme0n1p1) in my case. One may wonder why this isn&#8217;t done automatically. Note that it&#8217;s the first partition (/dev/nvme0n1p1) and <strong>not</strong> the entire disk (<span style="text-decoration: line-through;">/dev/nvme0n1</span>).</li>
<li>Don&#8217;t do anything with the planned /storage partition. As I don&#8217;t want to assign it with a mounting point, handle it after the installation is done.</li>
</ul>
<p>If the installation ends with a failure to install GRUB, run &#8220;journalctl&#8221; on a terminal window and look for error messages from the grub installer. Don&#8217;t ask ChatGPT to help you with solving any issues, and don&#8217;t ask me why I know it&#8217;s a bad idea.</p>
<h3>When I insisted on FAT32</h3>
<p>Sometimes I&#8217;m too much of a control freak, and when the Disks utility formatted the EFI partition into FAT16, I thought, oh no, it should be FAT32, what if the BIOS won&#8217;t play ball?</p>
<p>Well, that was silly of me, and also silly to ignore the warning about a FAT32 filesystem with just 10 MB having too few clusters.</p>
<p>So even though the installer wizard finished successfully, there was no option to boot from the disk. Secure boot was disabled, of course. And yet, there was no suitable option in the BIOS&#8217; boot menu. There was a &#8220;UEFI&#8221; option there, which is always in black (not possible to select), but that doesn&#8217;t seem to be relevant.</p>
<p>Following the warm advice of ChatGPT, I added an entry while in Live USB mode:</p>
<pre># <strong>efibootmgr -c -d /dev/nvme0n1p1 -p 1 -L "Linux Mint" -l '\EFI\ubuntu\grubx64.efi' </strong>
BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0003,2001,2002,2003
Boot0000* EFI USB Device (SanDisk Cruzer Blade)	UsbWwid(781,5567,0,4C53011006040812233)/CDROM(1,0x2104,0xa000)RC
Boot0001* EFI PXE 0 for IPv4 (AA-BB-CC-DD-EE-FF) 	PciRoot(0x0)/Pci(0x1d,0x0)/Pci(0x0,0x0)/MAC(aabbccddeeff,0)/IPv4(0.0.0.00.0.0.0,0,0)RC
Boot0002* EFI PXE 0 for IPv6 (AA-BB-CC-DD-EE-FF) 	PciRoot(0x0)/Pci(0x1d,0x0)/Pci(0x0,0x0)/MAC(aabbccddeeff,0)/IPv6([::]:&lt;-&gt;[::]:,0,0)RC
Boot2001* EFI USB Device	RC
Boot2002* EFI DVD/CDROM	RC
Boot2003* EFI Network	RC
Boot0003* Linux Mint	HD(1,GPT,12345678-aaaa-bbbb-cccc-dddddddddddd,0x800,0x5000)/File(\EFI\ubuntu\grubx64.efi)</pre>
<p><span class="yadayada">(some identifying numbers replaced trivially)</span></p>
<p>Cute, heh? But it made no difference. After rebooting with a Live USB again:</p>
<pre># <strong>efibootmgr</strong>
BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 2001,2002,2003
Boot0000* EFI USB Device (SanDisk Cruzer Blade)	UsbWwid(781,5567,0,4C53011006040812233)/CDROM(1,0x2104,0xa000)RC
Boot0001* EFI PXE 0 for IPv4 (AA-BB-CC-DD-EE-FF) 	PciRoot(0x0)/Pci(0x1d,0x0)/Pci(0x0,0x0)/MAC(aabbccddeeff,0)/IPv4(0.0.0.00.0.0.0,0,0)RC
Boot0002* EFI PXE 0 for IPv6 (AA-BB-CC-DD-EE-FF) 	PciRoot(0x0)/Pci(0x1d,0x0)/Pci(0x0,0x0)/MAC(aabbccddeeff,0)/IPv6([::]:&lt;-&gt;[::]:,0,0)RC
Boot2001* EFI USB Device	RC
Boot2002* EFI DVD/CDROM	RC
Boot2003* EFI Network	RC</pre>
<p>So the entry was gone.</p>
<p>I changed the EFI partition to FAT16, ran through the installation all the way again. And immediately after the installation was done (before booting to start from disk for the first time):</p>
<pre># <strong>efibootmgr</strong>
BootCurrent: 0000
Timeout: 0 seconds
BootOrder: 0003,2001,2002,2003
Boot0000* EFI USB Device (SanDisk Cruzer Blade)	UsbWwid(781,5567,0,4C53011006040812233)/CDROM(1,0x2104,0xa000)RC
Boot0001* EFI PXE 0 for IPv4 (AA-BB-CC-DD-EE-FF) 	PciRoot(0x0)/Pci(0x1d,0x0)/Pci(0x0,0x0)/MAC(aabbccddeeff,0)/IPv4(0.0.0.00.0.0.0,0,0)RC
Boot0002* EFI PXE 0 for IPv6 (AA-BB-CC-DD-EE-FF) 	PciRoot(0x0)/Pci(0x1d,0x0)/Pci(0x0,0x0)/MAC(aabbccddeeff,0)/IPv6([::]:&lt;-&gt;[::]:,0,0)RC
<span class="punch">Boot0003* Ubuntu	HD(1,GPT,12345678-aaaa-bbbb-cccc-dddddddddddd,0x800,0x5000)/File(\EFI\ubuntu\shimx64.efi)</span>
Boot2001* EFI USB Device	RC
Boot2002* EFI DVD/CDROM	RC
Boot2003* EFI Network	RC</pre>
<p>This time, when the laptop went on, the BIOS came up with a &#8220;Please don&#8217;t power off while completing system update&#8221;. What it actually did was to write its own backup file into the EFI partition, which appears as /boot/efi/BackupSbb.bin. Actually, it doesn&#8217;t seem like it was successful, as the space in the partition ran out. So I deleted this file and turned off the &#8220;BIOS Self-Healing&#8221; option in the BIOS&#8217; configuration (it will be much worse if it attempts to self-heal on a faulty backup file).</p>
<p>At this point, there was an &#8220;ubuntu&#8221; entry in the list of boot options in BIOS&#8217; boot menu (Not &#8220;Ubuntu&#8221;, but &#8220;ubuntu&#8221;, probably referring to the directory and not the name). And the black &#8220;UEFI&#8221; option remained in the option list, not possible to choose. So this is why I don&#8217;t think it&#8217;s relevant.</p>
<h3>Asking for a passphrase is too much to ask?</h3>
<p>Having reached this far, I got a nice Linux Mint logo on the screen, however nothing happened, and then I got thrown into a initramfs rescue shell. In other words, no attempt to unlock the encrypted partition.</p>
<p>So I ran the live USB again, unlocked the root partition and mounted it as /mnt/root/.</p>
<p>Then, as root (sudo su), bind-mounted the essential directories into the root filesystem:</p>
<pre># <span class="hljs-keyword">for</span> d <span class="hljs-keyword">in</span> /dev /dev/pts /proc /sys /run; <span class="hljs-keyword">do</span> mount --<span class="hljs-built_in">bind</span> <span class="hljs-variable">$d</span> /mnt/root/<span class="hljs-variable">$d</span> ; <span class="hljs-keyword">done</span></pre>
<p>And then chrooted into it.</p>
<pre># chroot /mnt/root</pre>
<p>Of course, there was no /etc/crypttab, so no wonder that the installation didn&#8217;t take unlocking the encrypted partition into account.</p>
<p>So I followed my own instruction from <a rel="noopener" href="https://billauer.se/blog/2018/11/linux-grub-uefi-raid-luks-lvm/" target="_blank">a previous post</a>. First, mount /boot and /boot/efi with</p>
<pre># mount -a</pre>
<p>and then check for the UUID of the encrypted partition:</p>
<pre># <strong>cryptsetup luksUUID /dev/nvme0n1p2</strong>
11223344-5566-7788-99aa-bbccddeeff00</pre>
<p>and then add /etc/crypttab reading</p>
<pre>luks-11223344-5566-7788-99aa-bbccddeeff00 UUID=11223344-5566-7788-99aa-bbccddeeff00 none luks</pre>
<p>Note that the luks-{UUID} part is as the name of the partition as it appears in /dev/mapper. In this case, this was what the Disks GUI utility chose. Had I done this with command line, I could have chosen a shorter name. But who cares.</p>
<p>And finally, edit /etc/default/grub for your preferences, update initramfs and GRUB, exactly as already mentioned in <a rel="noopener" href="https://billauer.se/blog/2018/11/linux-grub-uefi-raid-luks-lvm/" target="_blank">that post</a>:</p>
<pre># update-initramfs -u
# update-grub
# grub-install</pre>
<p>It was really exactly the same as the previous post. And then reboot, and all was finally fine.</p>
<p>And by the way, the initrd file is 77 MB. Running update-initramfs again didn&#8217;t make it smaller. Not a big deal with a flash disk, anyhow.</p>
<h3>But GRUB can open LUKS too!</h3>
<p>GRUB has cryptodisk and luks modules which can open an encrypted partition, so in principle it can read the kernel from an encrypted root partition. However there is no mechanism I&#8217;m aware of to pass over the unlocked encrypted partition to the kernel, so it would be necessary to supply the passphrase twice when booting.</p>
<p>This is why I went for two partitions for booting. I guess this still is the only sane way.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2025/10/install-linux-efi-laptop-luks/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Measuring how much RAM a Linux service eats</title>
		<link>https://billauer.se/blog/2024/12/cgroup-peak-ram-consumption/</link>
		<comments>https://billauer.se/blog/2024/12/cgroup-peak-ram-consumption/#comments</comments>
		<pubDate>Fri, 13 Dec 2024 17:20:02 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7142</guid>
		<description><![CDATA[Introduction Motivation: I wanted to move a service to another server that is dedicated only to that service. But how much RAM does this new server need? RAM is $$$, so too much is a waste of money, too little means problems. The method is to run the service and expose it to a scenario [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>Motivation: I wanted to move a service to another server that is dedicated only to that service. But how much RAM does this new server need? RAM is $$$, so too much is a waste of money, too little means problems.</p>
<p>The method is to run the service and expose it to a scenario that causes it to consume RAM. And then look at the maximal consumption.</p>
<p>This can be done with &#8220;top&#8221; and similar programs, but these show the current use. I needed the maximal RAM use. Besides, a service may spread out its RAM consumption across several processes. It&#8217;s the cumulative consumption that is interesting.</p>
<p>The appealing solution is to use the fact that systemd creates a cgroup for the service. The answer hence lies in the RAM consumption of the cgroup as a whole. It&#8217;s also possible to create a dedicated cgroup and run a program within that one, as shown in <a rel="noopener" href="https://billauer.se/blog/2016/05/linux-cgroups-swap-quartus/" target="_blank">another post of mine</a>.</p>
<p>This method is somewhat crude, because this memory consumption includes disk cache as well. In other words, this method shows how much RAM is consumed when there&#8217;s plenty of memory, and hence when there&#8217;s no pressure to reclaim any RAM. Therefore, if the service runs on a server with less RAM (or the service&#8217;s RAM consumption is limited in the systemd unit file), it&#8217;s more than possible that everything will work just fine. It might run somewhat slower due to disk access that was previously substituted by the cache.</p>
<p>So using a server with as much memory as measured by the test described below (plus some extra for the OS itself) will result in quick execution, but it might be OK to go for less RAM. A tight RAM limit will cause a lot of disk activity at first, and only afterwards will processes be killed by the OOM killer.</p>
<h3>Where the information is</h3>
<p>All said in this post relates to Linux kernel v4.15. Things are different with later kernels, not necessarily for the better.</p>
<p>There are in principle two versions of the interface with cgroup&#8217;s memory management: First, the one I won&#8217;t use, which is <a rel="noopener" href="https://www.kernel.org/doc/Documentation/cgroup-v2.txt" target="_blank">cgroup-v2</a> (or maybe <a rel="noopener" href="https://docs.kernel.org/admin-guide/cgroup-v2.html" target="_blank">this doc</a> for v2 is better?). The sysfs files for this interface for a service named &#8220;theservice&#8221; reside in /sys/fs/cgroup/unified/system.slice/theservice.service.</p>
<p>I shall be working with the <a rel="noopener" href="https://www.kernel.org/doc/Documentation/cgroup-v1/memory.txt" target="_blank">memory control of cgroup-v1</a>. The sysfs files in question are in /sys/fs/cgroup/memory/system.slice/theservice.service/.</p>
<p>If /sys/fs/cgroup/memory/ doesn&#8217;t exist, it might be necessary to mount it explicitly. Also, if system.slice doesn&#8217;t exist under /sys/fs/cgroup/memory/ it&#8217;s most likely because systemd&#8217;s memory accounting is not in action. This can be enabled globally, or by setting MemoryAccounting=true on the service&#8217;s systemd unit (or maybe any unit?).</p>
<p>Speaking of which, it might be a good idea to set MemoryMax in the service&#8217;s systemd unit in order to see what happens when the RAM is really restricted. Or change the limit dynamically, as shown below.</p>
<p>And there&#8217;s always the alternative of creating a separate cgroup and running the service in that group. I&#8217;ll refer to <a rel="noopener" href="https://billauer.se/blog/2016/05/linux-cgroups-swap-quartus/" target="_blank">my own blog post</a> again.</p>
<h3>Getting the info</h3>
<p>All files mentioned below are in /sys/fs/cgroup/unified/system.slice/theservice.service/ (assuming that the systemd service in question is theservice).</p>
<p><strong>The maximal memory used</strong>: memory.max_usage_in_bytes. As it&#8217;s name implies this is the maximal amount of RAM used, measured in bytes. This includes disk cache, so the number is higher than what appears in &#8220;top&#8221;.</p>
<p><strong>The memory currently used</strong>: memory.usage_in_bytes.</p>
<p>For more detailed info about memory use: memory.stat. For example:</p>
<pre>$ <strong>cat memory.stat </strong>
<span class="punch">cache 1138688</span>
rss 4268224512
rss_huge 0
shmem 0
mapped_file 516096
dirty 0
writeback 0
pgpgin 36038063
pgpgout 34995738
pgfault 21217095
pgmajfault 176307
inactive_anon 0
active_anon 4268224512
inactive_file 581632
active_file 401408
unevictable 0
hierarchical_memory_limit 4294967296
total_cache 1138688
total_rss 4268224512
total_rss_huge 0
total_shmem 0
total_mapped_file 516096
total_dirty 0
total_writeback 0
total_pgpgin 36038063
total_pgpgout 34995738
total_pgfault 21217095
total_pgmajfault 176307
total_inactive_anon 0
total_active_anon 4268224512
total_inactive_file 581632
total_active_file 401408
total_unevictable 0</pre>
<p>Note the &#8220;cache&#8221; part at the beginning. It&#8217;s no coincidence that it&#8217;s first. That&#8217;s the most important part: How much can be reclaimed just by flushing the cache.</p>
<p>On a 6.1.0 kernel, I&#8217;ve seen memory.peak and memory.current instead of memory.max_usage_in_bytes and memory.usage_in_bytes. memory.peak wasn&#8217;t writable however (neither in its permissions nor was it possible to write to it), so it wasn&#8217;t possible to reset the max level.</p>
<h3>Setting memory limits</h3>
<p>It&#8217;s possible to set memory limits in systemd&#8217;s unit file, but it can be more convenient to do this on the fly. In order to set the hard limit of memory use to 40 MiB, go (as root)</p>
<pre># echo 40M &gt; memory.limit_in_bytes</pre>
<p>To disable the limit, pick an unreasonably high number, e.g.</p>
<pre># echo 100G &gt; memory.limit_in_bytes</pre>
<p>Note that restarting the systemd service has no effect on these parameters (unless a memory limit is required in the unit file). The cgroup directory remains intact.</p>
<h3>Resetting between tests</h3>
<p>To reset the maximal value that has been recorded for RAM use (as root)</p>
<pre># echo 0 &gt; memory.max_usage_in_bytes</pre>
<p>But to really want to start from fresh, all disk cache needs to be cleared as well. The sledge-hammer way is going</p>
<pre># echo 1 &gt; /proc/sys/vm/drop_caches</pre>
<p>This frees the page caches system-wide, so everything running on the computer will need to re-read things again from the disk. There&#8217;s a slight and temporary global impact on the performance. On a GUI desktop, it gets a bit slow for a while.</p>
<p>A message like this will appear in the kernel log in response:</p>
<pre>bash (43262): drop_caches: 1</pre>
<p>This is perfectly fine, and indicates no error.</p>
<p>Alternatively, set a low limit for the RAM usage with memory.limit_in_bytes, as shown above. This impacts the cgroup only, forcing a reclaim of disk cache.</p>
<p>Two things that have <strong>no effect</strong>:</p>
<ul>
<li>Reducing the soft limit (memory.soft_limit_in_bytes). This limit is relevant only when the system is in a shortage of RAM overall. Otherwise, it does nothing.</li>
<li>Restarting the service with systemd. It wouldn&#8217;t make any sense to flush a disk cache when restarting a service.</li>
</ul>
<p>It&#8217;s of course a good idea to get rid of the disk cache before clearing memory.max_usage_in_bytes, so the max value starts without taking the disk cache into account.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/12/cgroup-peak-ram-consumption/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>A function similar to Perl&#8217;s die() in bash</title>
		<link>https://billauer.se/blog/2024/08/bash-exit-with-error/</link>
		<comments>https://billauer.se/blog/2024/08/bash-exit-with-error/#comments</comments>
		<pubDate>Mon, 12 Aug 2024 06:51:13 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7130</guid>
		<description><![CDATA[This is maybe a bit silly, but Perl has a die() function that is really handy for quitting a script with an error message. And I kind of miss it in Bash. So it can be defined with this simple one-liner: function die { echo $1 ; exit 1 ; } And then it can [...]]]></description>
			<content:encoded><![CDATA[<p>This is maybe a bit silly, but Perl has a die() function that is really handy for quitting a script with an error message. And I kind of miss it in Bash. So it can be defined with this simple one-liner:</p>
<pre>function die { echo $1 ; exit 1 ; }</pre>
<p>And then it can be used with something like:</p>
<pre>unzip thefile.zip || die "Unzip returned with error status"</pre>
<p>The Perl feeling, in Bash.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/08/bash-exit-with-error/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Linux kernel workqueues: Is it OK for the worker function to kfree its own work item?</title>
		<link>https://billauer.se/blog/2024/07/free-work-struct/</link>
		<comments>https://billauer.se/blog/2024/07/free-work-struct/#comments</comments>
		<pubDate>Tue, 30 Jul 2024 06:37:14 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux kernel]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7122</guid>
		<description><![CDATA[Freeing yourself Working with Linux kernel&#8217;s workqueues, I incremented a kref reference count before queuing a work item, in order to make sure that the data structure that it operated on will still be in memory while it runs. Just before returning, the work item&#8217;s function decremented this reference count, and as a result, the [...]]]></description>
			<content:encoded><![CDATA[<h3>Freeing yourself</h3>
<p>Working with Linux kernel&#8217;s workqueues, I incremented a kref reference count before queuing a work item, in order to make sure that the data structure that it operated on will still be in memory while it runs. Just before returning, the work item&#8217;s function decremented this reference count, and as a result, the data structure&#8217;s memory could be freed at that very moment.</p>
<p>The thing was, that this data structure also included the work item&#8217;s own struct work_struct. In other words, the work item&#8217;s function could potentially free the entry that was pushed into the workqueue on its behalf. Could this possibly be allowed?</p>
<p>The short answer is yes. It&#8217;s OK to call kfree() on the memory of the struct work_struct of the currently running work item. No risk for use-after-free (UAF).</p>
<p>It&#8217;s also OK to requeue the work item on the same workqueue (or on a different one). All in all, the work item&#8217;s struct is just a piece of unused memory as soon as the work item&#8217;s function is called.</p>
<p>On the other hand, don&#8217;t think about calling destroy_workqueue() on the workqueue on which the running work item is queued: destroy_workqueue() waits for all work items to finish before destroying the queue, which will never happen if the request to destroy the queue came from one of its own work items.</p>
<h3>From the horse&#8217;s mouth</h3>
<p>I didn&#8217;t find any documentation on this topic, but there are a couple of comments in the source code, namely in the process_one_work() function in kernel/workqueue.c: First, this one by Tejun Heo from June 2010:</p>
<pre>/*
 * It is permissible to free the struct work_struct from
 * inside the function that is called from it, this we need to
 * take into account for lockdep too.  To avoid bogus "held
 * lock freed" warnings as well as problems when looking into
 * work-&gt;lockdep_map, make a copy and use that here.
 */</pre>
<p>And this comes after calling the work item&#8217;s function, worker-&gt;current_func(work). Written by Arjan van de Ven in August 2010.</p>
<pre>/*
 * While we must be careful to not use "work" after this, the trace
 * point will only record its address.
 */
trace_workqueue_execute_end(<span class="punch">work</span>, worker-&gt;current_func);</pre>
<p>The point of this comment is that the <em>value</em> of @work will be used by the call to trace_workqueue_execute_end(), but it won&#8217;t be used as a pointer. This emphasizes the commitment of not touching what @work points at, i.e. the memory segment may have been freed.</p>
<h3>How it&#8217;s done</h3>
<p>process_one_work(), which is the only function that calls the work item&#8217;s function, is clearly written in a way that ignores the work item&#8217;s struct after calling the work item&#8217;s function.</p>
<p>The first thing is that it copies the address of the work function into the worker struct:</p>
<pre>worker-&gt;current_func = work-&gt;func;</pre>
<p>It then removes the work item from the workqueue:</p>
<pre>list_del_init(&amp;work-&gt;entry);</pre>
<p>And later on, it calls the function, using the copy of the pointer (even though it could also have used the original at this point).</p>
<pre>worker-&gt;current_func(work);</pre>
<p>After this, the @work variable isn&#8217;t used anymore as a pointer.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/07/free-work-struct/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Installing GRUB 2 manually with rescue-like techniques</title>
		<link>https://billauer.se/blog/2024/07/installing-grub-rescue/</link>
		<comments>https://billauer.se/blog/2024/07/installing-grub-rescue/#comments</comments>
		<pubDate>Fri, 12 Jul 2024 15:16:20 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7099</guid>
		<description><![CDATA[Introduction It&#8217;s rarely necessary to make an issue of installing and maintaining the GRUB bootloader. However, for reasons explained in a separate post, I wanted to install GRUB 2.12 on an old distribution (Debian 8). So it required some acrobatics. That said, it doesn&#8217;t limit the possibility to install new kernels in the future etc. [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>It&#8217;s rarely necessary to make an issue of installing and maintaining the GRUB bootloader. However, for reasons explained in <a rel="noopener" href="https://billauer.se/blog/2024/07/container-to-kvm-virtualization/" target="_blank">a separate post</a>, I wanted to install GRUB 2.12 on an old distribution (Debian 8). So it required some acrobatics. That said, it doesn&#8217;t limit the possibility to install new kernels in the future etc. If you&#8217;re ready to edit a simple text file, rather than running automatic tools, that is. Which may actually be a good idea anyhow.</p>
<h3>The basics</h3>
<p>Grub has two parts: First, there&#8217;s the initial code that is loaded by the BIOS, either from the MBR or from the EFI partition. That&#8217;s the plain GRUB executable. This executable goes directly to the ext2/3/4 root partition, and reads from /boot/grub/. That directory contains, among others, the precious grub.cfg file, which GRUB reads in order to decide which modules to load, which menu entries to display and how to act if each is selected.</p>
<p>grub.cfg is created by update-grub, which effectively runs &#8220;grub-mkconfig -o /boot/grub/grub.cfg&#8221;.</p>
<p>This file is created from /etc/grub.d/ and settings from /etc/default/grub, and based upon the kernel image and initrd files that are found in /boot.</p>
<p>Hence an installation of GRUB consists of two tasks, which are fairly independent:</p>
<ul>
<li>Running grub-install so that the MBR or EFI partition are set to run GRUB, and that /boot/grub/ is populated with modules and other stuff. The only important thing is that this utility knows the correct disk to target and where the partition containing /boot/grub is.</li>
<li>Running update-grub in order to create (or update) the /boot/grub/grub.cfg file. This is normally done every time the content of /boot is updated (e.g. a new kernel image).</li>
</ul>
<p>Note that grub-install populates /boot/grub with a lot of files that are used by the bootloader, so it&#8217;s necessary to run this command if /boot is wiped and started from fresh.</p>
<p>What made this extra tricky for me, was that Debian 8 comes with an old GRUB 1 version. Therefore, the option of chroot&#8217;ing into the filesystem for the purpose of installing GRUB was eliminated.</p>
<p>So there were two tasks to accomplish: Obtaining a suitable grub.cfg and running grub-install in a way that will do the job.</p>
<p>This is a good time to understand what this grub.cfg file is.</p>
<h3>The grub.cfg file</h3>
<p>grub.cfg is a script, written with a <a rel="noopener" href="https://www.gnu.org/software/grub/manual/grub/html_node/Shell_002dlike-scripting.html" target="_blank">bash-like syntax</a>. and is based upon an <a rel="noopener" href="https://www.gnu.org/software/grub/manual/grub/grub.html" target="_blank">internal command set</a>. This is a plain file in /boot/grub/, owned by root:root and writable by root only, for obvious reasons. But for the purpose of booting, permissions don&#8217;t make any difference.</p>
<p>Despite the &#8220;DO NOT EDIT THIS FILE&#8221; comment at the top of this file, and the suggestion to use grub-mkconfig, it&#8217;s perfectly OK to edit it for the purposes of updating the behavior of the boot menu. This is unnecessarily complicated in most cases, even when rescuing a system from a Live ISO system: There&#8217;s always the possibility to chroot into the target&#8217;s root filesystem and call grub-mkconfig from there. That&#8217;s usually all that is necessary to update which kernel image / initrd should be kicked off.</p>
<p>That said, it might also be easier to edit this file manually in order to add menu entries for new kernels, for example. In addition, automatic utilities tend to add a lot of specific details that are unnecessary, and that can fail the boot process, for example if the file system&#8217;s UUID changes. So maintaining a clean grub.cfg manually can pay off in the long run.</p>
<p>The most interesting part in this file is the menuentry section. Let&#8217;s look at a sample command:</p>
<pre>menuentry <span class="hljs-string">'Ubuntu'</span> --class ubuntu --class gnu-linux --class gnu --class os <span class="hljs-variable">$menuentry_id_option</span> <span class="hljs-string">'gnulinux-simple-a0c2e12e-5d16-4aac-b11d-15cbec5ae98e'</span> {
	recordfail
	load_video
	gfxmode <span class="hljs-variable">$linux_gfx_mode</span>
	insmod gzio
	<span class="hljs-keyword">if</span> [ x<span class="hljs-variable">$grub_platform</span> = xxen ]; <span class="hljs-keyword">then</span> insmod xzio; insmod lzopio; <span class="hljs-keyword">fi</span>
	insmod part_gpt
	insmod ext2
	search --no-floppy --fs-uuid --set=root a0c2e12e-5d16-4aac-b11d-15cbec5ae98e
	linux	/boot/vmlinuz-6.8.0-36-generic root=UUID=a0c2e12e-5d16-4aac-b11d-15cbec5ae98e ro
	initrd	/boot/initrd.img-6.8.0-36-generic
}</pre>
<p>So these are a bunch of commands that run if the related menu entry is chosen. I&#8217;ll discuss &#8220;menuentry&#8221; and &#8220;search&#8221; below. Note the &#8220;insmod&#8221; commands, which load ELF executable modules from /boot/grub/i386-pc/. GRUB also supports lsmod, if you want to try it with GRUB&#8217;s interactive command interface.</p>
<h3>The menuentry command</h3>
<p>The menuentry command is documented <a rel="noopener" href="https://www.gnu.org/software/grub/manual/grub/grub.html#menuentry" target="_blank">here</a>. Let&#8217;s break down the command in this example:</p>
<ul>
<li>menuentry: Obviously, the command itself.</li>
<li>&#8216;Ubuntu&#8217;: The title, which is the part presented to the user.</li>
<li>&#8211;class ubuntu &#8211;class gnu-linux &#8211;class gnu &#8211;class os: The purpose of these class flags is to help GRUB group the menu options nicer. Usually redundant.</li>
<li>$menuentry_id_option &#8216;gnulinux-simple-a0c2e12e-5d16-4aac-b11d-15cbec5ae98e&#8217;: &#8220;$menuentry_id_option&#8221; expands into &#8220;&#8211;id&#8221;, so this gives the menu option a unique identifier. It&#8217;s useful for submenus, otherwise not required.</li>
</ul>
<p>Bottom line: If there are no submenus (in the original file there actually are), this header would have done the job as well:</p>
<pre>menuentry <span class="hljs-string">'Ubuntu for the lazy'</span> {</pre>
<h3>The search command</h3>
<p>The other interesting part is this row within the menucommand clause:</p>
<pre>search --no-floppy --fs-uuid --set=root a0c2e12e-5d16-4aac-b11d-15cbec5ae98e</pre>
<p>The search command is documented <a rel="noopener" href="https://www.gnu.org/software/grub/manual/grub/grub.html#search" target="_blank">here</a>. The purpose of this command is to set the <a rel="noopener" href="https://www.gnu.org/software/grub/manual/grub/html_node/root.html" target="_blank">$root environment variable</a>, which is what the &#8220;&#8211;set=root&#8221; part means (this is an unnecessary flag, as $root is the target variable anyhow). This tells GRUB in which filesystem to look for the files mentioned in the &#8220;linux&#8221; and &#8220;initrd&#8221; commands.</p>
<p>On a system with only one Linux installed, the &#8220;search&#8221; command is unnecessary: Both $root and <a rel="noopener" href="https://www.gnu.org/software/grub/manual/grub/html_node/prefix.html" target="_blank">$prefix</a> are initialized according to the position of the /boot/grub, so there&#8217;s no reason to search for it again.</p>
<p>In this example, the filesystem is defined according to the its UUID , which can be found with this Linux command:</p>
<pre># dumpe2fs /dev/vda2 | grep UUID</pre>
<p>It&#8217;s better to remove this &#8220;search&#8221; command if there&#8217;s only one /boot directory in the whole system (and it contains the linux kernel files, of course). The advantage is the Linux system can be installed just by pouring all files into an ext4 filesystem (including /boot) and then just run grub-install. Something that won&#8217;t work if grub.cfg contains explicit UUIDs. Well, actually, it will work, but with an error message and a prompt to press ENTER: The &#8220;search&#8221; command fails if the UUID is incorrect, but it wasn&#8217;t necessary to begin with, so $root will retain it&#8217;s correct value and the system can boot properly anyhow. Given that ENTER is pressed. That hurdle can be annoying on a remote virtual machine.</p>
<h3>A sample menuentry command</h3>
<p>I added these lines to my grub.cfg file in order to allow future self to try out a new kernel without begin too scared about it:</p>
<pre>menuentry <span class="hljs-string">'Unused boot menu entry for future hacks'</span> {
        recordfail
        load_video
        gfxmode <span class="hljs-variable">$linux_gfx_mode</span>
        insmod gzio
        <span class="hljs-keyword">if</span> [ x<span class="hljs-variable">$grub_platform</span> = xxen ]; <span class="hljs-keyword">then</span> insmod xzio; insmod lzopio; <span class="hljs-keyword">fi</span>
        insmod part_gpt
        insmod ext2
        linux   /boot/vmlinuz-6.8.12 root=/dev/vda3 ro
}</pre>
<p>This is just an implementation of what I said above about the &#8220;menuentry&#8221; and &#8220;search&#8221; commands above. In particular, that the &#8220;search&#8221; command is unnecessary. This worked well on my machine.</p>
<p>As for the other rows, I suggest mixing and matching with whatever appears in your own grub.cfg file in the same places.</p>
<h3>Obtaining a grub.cfg file</h3>
<p>So the question is: How do I get the initial grub.cfg file? Just take one from a random system? Will that be good enough?</p>
<p>Well, no, that may not work: The grub.cfg is formed differently, depending in particular on how the filesystems on the hard disk are laid out. For example, comparing two grub.cfg files, one had this row:</p>
<pre>insmod lvm</pre>
<p>and the other didn&#8217;t. Obviously, one computer utilized LVM and the other didn&#8217;t. Also, in relation to setting the $root variable, there were different variations, going from the &#8220;search&#8221; method shown above to simply this:</p>
<pre>set root='hd0,msdos1'</pre>
<p>My solution was to install a Ubuntu 24.04 system on the same KVM virtual machine that I intended to install Debian 8 on later. After the installation, I just copied the grub.cfg and wiped the filesystem. I then installed the required distribution and deleted everything under /boot. Instead, I added this grub.cfg into /boot/grub/ and edited it manually to load the correct kernel.</p>
<p>As I kept the structure of the harddisk and the hardware environment remained unchanged, this worked perfectly fine.</p>
<h3>Running grub-install</h3>
<p>Truth to be told, I probably didn&#8217;t need to use grub-install, since the MBR was already set up with GRUB thanks to the installation I had already carried out for Ubuntu 24.04. Also, I could have copied all other files in /boot/grub from this installation before wiping it. But I didn&#8217;t, and it&#8217;s a good thing I didn&#8217;t, because this way I found out how to do it from a Live ISO. And this might be important for rescue purposes, in the unlikely and very unfortunate event that it&#8217;s necessary.</p>
<p>Luckily, grub-install has an undocumented option, &#8211;root-directory, which gets the job done.</p>
<pre># grub-install --root-directory=/mnt/new/ /dev/vda
Installing for i386-pc platform.
Installation finished. No error reported.</pre>
<p>Note that using &#8211;boot-directory isn&#8217;t good enough, even if it&#8217;s mounted. Only &#8211;root-directory makes GRUB detect the correct root directory as the place to fetch the information from. With &#8211;boot-directory, the system boots with no menus.</p>
<h3>Running update-grub</h3>
<p>If you insist on running update-grub, be sure to edit /etc/default/grub and set it this way:</p>
<pre>GRUB_TIMEOUT=3
GRUB_RECORDFAIL_TIMEOUT=3</pre>
<p>The previous value for GRUB_TIMEOUT is 0, which is supposed to mean to skip the menu. If GRUB deems the boot media not to be writable, it considers every previous boot as a failure (because it can&#8217;t know if it was successful or not), and sets the timeout to 30 seconds. 3 seconds are enough, thanks.</p>
<p>And then run update-grub.</p>
<pre># <strong>update-grub</strong>
Sourcing file `/etc/default/grub'
Generating grub configuration file ...
Found linux image: /boot/vmlinuz-6.8.0-36-generic
Warning: os-prober will not be executed to detect other bootable partitions.
Systems on them will not be added to the GRUB boot configuration.
Check GRUB_DISABLE_OS_PROBER documentation entry.
Adding boot menu entry for UEFI Firmware Settings ...
done</pre>
<p>Alternatively, edit grub.cfg and fix it directly.</p>
<h3>A note about old GRUB 1</h3>
<p>This is really not related to anything else above, but since I made an attempt to install Debian 8&#8242;s GRUB on the hard disk at some point, this is what happened:</p>
<pre># <strong>apt install grub</strong>
# <strong>grub --version</strong>
grub (GNU GRUB 0.97)

# <strong>update-grub </strong>
Searching for GRUB installation directory ... found: /boot/grub
Probing devices to guess BIOS drives. This may take a long time.
Searching for default file ... Generating /boot/grub/default file and setting the default boot entry to 0
Searching for GRUB installation directory ... found: /boot/grub
Testing for an existing GRUB menu.lst file ... 

Generating /boot/grub/menu.lst
Searching for splash image ... none found, skipping ...
Found kernel: /boot/vmlinuz
Found kernel: /boot/vmlinuz-6.8.0-31-generic
Updating /boot/grub/menu.lst ... done

# <strong>grub-install /dev/vda</strong>
Searching for GRUB installation directory ... found: /boot/grub
<span class="punch">The file /boot/grub/stage1 not read correctly.</span></pre>
<p>The error message about /boot/grub/stage1 appears to be horribly misleading. According to <a rel="noopener" href="https://velenux.wordpress.com/2014/10/01/grub-install-the-file-bootgrubstage1-not-read-correctly/" target="_blank">this</a> and <a rel="noopener" href="https://blog.widmo.biz/resolved-file-bootgrubstage1-read-correctly/" target="_blank">this</a>, among others, the problem was that the ext4 file system was created with 256 as the inode size, and GRUB 1 doesn&#8217;t support that. Which makes sense, as the installation was done on behalf of Ubuntu 24.04 and not a museum distribution.</p>
<p>The solution is apparently to wipe the filesystem correctly:</p>
<pre># mkfs.ext4 -I 128 /dev/vda3</pre>
<p>Actually, I don&#8217;t know if this was really the problem, because I gave up this old GRUB version quite soon.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/07/installing-grub-rescue/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Migrating an OpenVZ container to KVM</title>
		<link>https://billauer.se/blog/2024/07/container-to-kvm-virtualization/</link>
		<comments>https://billauer.se/blog/2024/07/container-to-kvm-virtualization/#comments</comments>
		<pubDate>Fri, 12 Jul 2024 15:11:13 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Linux kernel]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[systemd]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7097</guid>
		<description><![CDATA[Introduction My Debian 8-based web server had been running for several years as an OpenVZ container, when the web host told me that containers are phased out, and it&#8217;s time to move on to a KVM. This is an opportunity to upgrade to a newer distribution, most of you would say, but if a machine [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>My Debian 8-based web server had been running for several years as an OpenVZ container, when the web host told me that containers are phased out, and it&#8217;s time to move on to a KVM.</p>
<p>This is an opportunity to upgrade to a newer distribution, most of you would say, but if a machine works flawlessly for a long period of time, I&#8217;m very reluctant to change anything. Don&#8217;t touch a stable system. It just happened to have an uptime of 426 days, and the last time this server caused me trouble was way before that.</p>
<p>So the question is if it&#8217;s possible to convert a container into a KVM machine, just by copying the filesystem. After all, what&#8217;s the difference if /sbin/init (systemd) is kicked off as a plain process inside a container or if the kernel does the same thing?</p>
<p>The answer is yes-ish, this manipulation is possible, but it requires some adjustments.</p>
<p>These are my notes and action items while I found my way to get it done. Everything below is very specific to my own slightly bizarre case, and at times I ended up carrying out tasks in a different order than as listed here. But this can be useful for understanding what&#8217;s ahead.</p>
<p>By the way, the wisest thing I did throughout this process, was to go through the whole process on a KVM machine that I built on my own local computer. This virtual machine functioned as a mockup of the server to be installed. Not only did it make the trial and error much easier, but it also allowed me to test all kind of things after the real server was up and running without messing the real machine.</p>
<h3>Faking Ubuntu 24.04 LTS</h3>
<p>To make things even more interesting, I also wanted to push the next time I&#8217;ll be required to mess with the virtual machine as long as possible into the future. Put differently, I wanted to hide the fact that the machine runs on ancient software. There should not be a request to upgrade in the foreseeable future because the old system isn&#8217;t compatible with some future version of KVM.</p>
<p>So to the KVM hypervisor, my machine should feel like an Ubuntu 24.04, which was the latest server distribution offered at the time I did this trick. Which brings the question: What does the hypervisor see?</p>
<p>The KVM guest interfaces with its hypervisor in three ways:</p>
<ul>
<li>With GRUB, which accesses the virtual disk.</li>
<li>Through the kernel, which interacts with the virtual hardware.</li>
<li>Through the guest&#8217;s DHCP client, which fetches the IP address, default gateway and DNS from the hypervisor&#8217;s dnsmasq.</li>
</ul>
<p>Or so I hope. Maybe there&#8217;s some aspect I&#8217;m not aware of. It&#8217;s not like I&#8217;m such an expert in virtualization.</p>
<p>So the idea was that both GRUB and the kernel should be the same as in Ubuntu 24.04. This way, any KVM setting that works with this distribution will work with my machine. The Naphthalene smell from the user-space software underneath will not reach the hypervisor.</p>
<p>This presumption can turn out to be wrong, and the third item in the list above demonstrates that: The guest machine gets its IP address from the hypervisor through a DHCP request issued by systemd-networkd, which is part of systemd version 215. So the bluff is exposed. Will there be some kind of incompatibility between the old systemd&#8217;s DHCP client and some future hypervisor&#8217;s response?</p>
<p>Regarding this specific issue, I doubt there will be a problem, as DHCP is such a simple and well-established protocol. And even if that functionality broke, the IP address is fixed anyhow, so the virtual NIC can be configured statically.</p>
<p>But who knows, maybe there is some kind of interaction with systemd that I&#8217;m not aware of? Future will tell.</p>
<p>So it boils down to faking GRUB and using a recent kernel.</p>
<h3>Solving the GRUB problem</h3>
<p>Debian 8 comes with GRUB version 0.97. Could we call that GRUB 1? I can already imagine the answer to my support ticket saying &#8220;please upgrade your system, as our KVM hypervisor doesn&#8217;t support old versions of GRUB&#8221;.</p>
<p>So I need a new one.</p>
<p>Unfortunately, the common way to install GRUB is with a couple of hocus-pocus tools that do the work well in the usual scenario.</p>
<p>As it turns out, there are two parts that need to be installed: The first part consists of the GRUB binary on the boot partition (GRUB partition or EFI, pick your choice), plus several files (modules and other) in /boot/grub/. The second part is a script file, grub.cfg, which is a textual file that can be edited manually.</p>
<p>To make a long story short, I installed the distribution on a virtual machine with the same layout, and made a copy of the grub.cfg file that was created. I then edited this file directly to fit into the new machine. As for installing GRUB binary, I did this from a Live ISO Ubuntu 24.04, so it&#8217;s genuine and legit.</p>
<p>For the full and explained story, I&#8217;ve written <a rel="noopener" href="https://billauer.se/blog/2024/07/installing-grub-rescue/" target="_blank">a separate post</a>.</p>
<h3>Fitting a decent kernel</h3>
<p>This way or another, a kernel and its modules must be added to the filesystem in order to convert it from a container to a KVM machine. This is the essential difference: With a container, one kernel runs all containers and gives them the illusion that they&#8217;re the only one. With KVM, the boot starts from the very beginning.</p>
<p>If there was something I <strong>didn&#8217;t</strong> worry about, it was the concept of running an ancient distribution with a very recent kernel. I have a lot of experience with compiling the hot-hot-latest-out kernel and run it with steam engine distributions, and very rarely have I seen any issue with that. The Linux kernel is backward compatible in a remarkable way.</p>
<p>My original idea was to grab the kernel image and the modules from a running installation of Ubuntu 24.04. However, the module format of this distro is incompatible with old Debian 8 (ZST compression seems to have been the crux), and as a result, no modules were loaded.</p>
<p>So I took config-6.8.0-36-generic from Ubuntu 24.04 and used it as the starting point for the .config file used for compiling the vanilla stable kernel with version v6.8.12.</p>
<p>And then there were a few modifications to .config:</p>
<ul>
<li>&#8220;make oldconfig&#8221; asked a few questions and made some minor modifications, nothing apparently related.</li>
<li>Dropped kernel module compression (CONFIG_MODULE_COMPRESS_ZSTD off) and set kernel&#8217;s own compression to gzip. This was probably the reason the distribution&#8217;s modules didn&#8217;t load.</li>
<li>Some crypto stuff was disabled: CONFIG_INTEGRITY_PLATFORM_KEYRING, CONFIG_SYSTEM_BLACKLIST_KEYRING and CONFIG_INTEGRITY_MACHINE_KEYRING were dropped, same with CONFIG_LOAD_UEFI_KEYS and most important, CONFIG_SYSTEM_REVOCATION_KEYS was set to &#8220;&#8221;. Its previous value, &#8220;debian/canonical-revoked-certs.pem&#8221; made the compilation fail.</li>
<li>Dropped CONFIG_DRM_I915, which caused some weird compilation error.</li>
<li>After making a test run with the kernel, I also dropped CONFIG_UBSAN with everything that comes with it. UBSAN spat a lot of warning messages on mainstream drivers, and it&#8217;s really annoying. It&#8217;s still unclear to me why these warnings don&#8217;t appear on the distribution kernel. Maybe because a difference between compiler versions (the warnings stem from checks inserted by gcc).</li>
</ul>
<p>The compilation took 32 minutes on a machine with 12 cores (6 hyperthreaded). By far, the longest and most difficult kernel compilation I can remember for a long time.</p>
<p>Based upon <a rel="noopener" href="https://billauer.se/blog/2015/10/linux-kernel-compilation-jots/" target="_blank">my own post</a>, I created the Debian packages for the whole thing, using the bindeb-pkg make target.</p>
<p>That took additional 20 minutes, running on all cores. I used two of these packages in the installation of the KVM machine, as shown in the cookbook below.</p>
<h3>Methodology</h3>
<p>So the deal with my web host was like this: They started a KVM machine (with a different IP address, of course). I prepared this KVM machine, and when that was ready, I sent a support ticket asking for swapping the IP addresses. This way, the KVM machine became the new server, and the old container machine went to the junkyard.</p>
<p>As this machine involved a mail server and web sites with user content (comments to my blog, for example), I decided to stop the active server, copy &#8220;all data&#8221;, and restart the server only after the IP swap. In other words, the net result should be as if the same server had been shut down for an hour, and then restarted. No discontinuities.</p>
<p>As it turned out, everything that is related to the web server and email, including the logs of everything, are in /var/ and /home/. So I could therefore copy all files from the old server to the new one for the sake of setting it up, and verify that everything is smooth as a first stage.</p>
<p>Then I shut down the services and copied /var/ and /home/. And then came the IP swap.</p>
<p>This simple command is handy for checking which files have changed during the past week. The first finds the directories, and the second the plain files.</p>
<pre># find / -xdev -ctime -7 -type d | sort
# find / -xdev -ctime -7 -type f | sort</pre>
<p>The purpose of the -xdev flag is to remain on one filesystem. Otherwise, a lot of files from /proc and such are printed out. If your system has several relevant filesystems, be sure to add them to &#8220;/&#8221; in this example.</p>
<p>The next few sections below are the cookbook I wrote for myself in order to get it done without messing around (and hence mess up).</p>
<p>In hindsight, I can say that except for dealing with GRUB and the kernel, most of the hassle had to with the NIC: Its name changed from venet0 to eth0, and it got its address through DHCP relatively late in the boot process. And that required some adaptations.</p>
<h3>Preparing the virtual machine</h3>
<ul>
<li>Start the installation Ubuntu 24.04 LTS server edition (or whatever is available, it doesn&#8217;t matter much). Possible stop the installation as soon as files are being copied: The only purpose of this step is to partition the disk neatly, so that /dev/vda1 is a small partition for GRUB, and /dev/vda3 is the root filesystem (/dev/vda2 is a swap partition).</li>
<li>Start the KVM machine with a rescue image (preferable graphical or with sshd running). I went for Ubuntu 24.04 LTS server Live ISO (the best choice provided by my web host). See notes below on using Ubuntu&#8217;s server ISO as a rescue image.</li>
<li>Wipe the existing root filesystem, if such has been installed. I considered this necessary at the time, because the default inode size may be 256, and GRUB version 1 won&#8217;t play ball with that. But later on I decided on GRUB 2. Anyhow, I forced it to be 128 bytes, despite the warning that 128-byte inodes cannot handle dates beyond 2038 and are deprecated:
<pre># mkfs.ext4 -I 128 /dev/vda3</pre>
</li>
<li>And since I was at it, no automatic fsck check. Ever. It&#8217;s really annoying when you want to kick off the server quickly.
<pre># tune2fs -c 0 -i 0 /dev/vda3</pre>
</li>
<li>Mount new system as /mnt/new:
<pre># mkdir /mnt/new
# mount /dev/vda3 /mnt/new</pre>
</li>
<li>Copy the filesystem. On the OpenVZ machine:
<pre># tar --one-file-system -cz / | nc -q 0 185.250.251.160 1234 &gt; /dev/null</pre>
<p>and the other side goes (run this before the command above):</p>
<pre># nc -l 1234 &lt; /dev/null | time tar -C /mnt/new/ -xzv</pre>
<p>This took about 30 minutes. The purpose of the &#8220;-q 0&#8243; flag and those /dev/null redirections is merely to make nc quit when the tar finishes.<br />
Or, doing the same from a backup tarball:</p>
<pre>$ cat myserver-all-24.07.08-08.22.tar.gz | nc -q 0 -l 1234 &gt; /dev/null</pre>
<p>and the other side goes</p>
<pre># nc 10.1.1.3 1234 &lt; /dev/null | time tar -C /mnt/new/ -xzv</pre>
</li>
<li>Remove old /lib/modules and boot directory:
<pre># rm -rf /mnt/new/lib/modules/ /mnt/new/boot/</pre>
</li>
<li>Create /boot/grub and copy the grub.cfg file that I&#8217;ve prepared in advance to there. <a rel="noopener" href="https://billauer.se/blog/2024/07/installing-grub-rescue/" target="_blank">This separate post</a> explains the logic behind doing it this way.</li>
<li>Install GRUB on the boot parition (this also adds a lot of files to /boot/grub/):
<pre># grub-install --root-directory=/mnt/new /dev/vda</pre>
</li>
<li>In order to work inside the chroot, some bind and tmpfs mounts are necessary:
<pre># mount -o bind /dev /mnt/new/dev
# mount -o bind /sys /mnt/new/sys
# mount -t proc /proc /mnt/new/proc
# mount -t tmpfs tmpfs /mnt/new/tmp
# mount -t tmpfs tmpfs /mnt/new/run</pre>
</li>
<li>Copy the two .deb files that contain the Linux kernel files to somewhere in /mnt/new/</li>
<li>Chroot into the new fs:
<pre># chroot /mnt/new/</pre>
</li>
<li>Check that /dev, /sys, /proc, /run and /tmp are as expected (mounted correctly).</li>
<li>Disable and stop these services: bind9, sendmail, cron.</li>
<li>This wins the prize for the oddest fix: Probably in relation to the OpenVZ container, the LSB modules_dep service is active, and it deletes all module files in /lib/modules on reboot. So make sure to never see it again. Just disabling it wasn&#8217;t good enough.
<pre># systemctl mask modules_dep.service</pre>
</li>
<li>Install the Linux kernel and its modules into /boot and /lib/modules:
<pre># dpkg -i linux-image-6.8.12-myserver_6.8.12-myserver-2_amd64.deb</pre>
</li>
<li>Also install the headers for compilation (why not?)
<pre># dpkg -i linux-headers-6.8.12-myserver_6.8.12-myserver-2_amd64.deb</pre>
</li>
<li>Add /etc/systemd/network/20-eth0.network
<pre>[Match]
Name=eth0

[Network]
DHCP=yes</pre>
<p>The NIC was a given in a container, but now it has to be raised explicitly and the IP address possibly obtained from the hypervisor via DHCP, as I&#8217;ve done here.</li>
<li>Add the two following lines to /etc/sysctl.conf, in order to turn off IPv6:
<pre>net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1</pre>
</li>
<li>Adjust the firewall rules, so that they don&#8217;t depend on the server having a specific IP address (because a temporary IP address will be used).</li>
<li>Add support for lspci (better do it now if something goes wrong after booting):
<pre># apt install pciutils</pre>
</li>
<li>Ban the evbug module, which is intended to generate debug message on input devices. Unfortunately, it floods the kernel log sometimes when the mouse goes over the virtual machine&#8217;s console window. So ditch it by adding /etc/modprobe.d/evbug-blacklist.conf having this single line:
<pre>blacklist evbug</pre>
</li>
<li>Edit /etc/fstab. Remove everything, and leave only this row:
<pre>/dev/vda3 / ext4 defaults 0 1</pre>
</li>
<li>Remove persistence udev rules, if such exist, at /etc/udev/rules.d. Oddly enough, there was nothing in this directory, not in the existing OpenVZ server and not in a regular Ubuntu 24.04 server installation.</li>
<li>Boot up the system from disk, and perform post-boot fixes as mentioned below.</li>
</ul>
<h3>Post-boot fixes</h3>
<ul>
<li>Verify that /tmp is indeed mounted as a tmpfs.</li>
<li>Disable (actually, mask) the automount service, which is useless and fails. This makes systemd&#8217;s status degraded, which is practically harmless, but confusing.
<pre># systemctl mask proc-sys-fs-binfmt_misc.automount</pre>
</li>
<li>Install the dbus service:
<pre># apt install dbus</pre>
<p>Not only is it the right thing to do on a Linux system, but it also silences this warning:</p>
<pre>Cannot add dependency job for unit dbus.socket, ignoring: Unit dbus.socket failed to load: No such file or directory.</pre>
</li>
<li>Enable login prompt on the default visible console (tty1) so that a prompt appears after all the boot messages:
<pre># systemctl enable getty@tty1.service</pre>
<p>The other tty&#8217;s got a login prompt when using Ctrl-Alt-Fn, but not the visible console. So this fixed it. Otherwise, one can be mislead into thinking that the boot process is stuck.</li>
<li>Optionally: Disable <a rel="noopener" href="https://wiki.openvz.org/Debian_template_creation" target="_blank">vzfifo service</a> and remove /.vzfifo.</li>
</ul>
<h3>Just before the IP address swap</h3>
<ul>
<li>Reboot the openVZ server to make sure that it wakes up OK.</li>
<li>Change the openVZ server&#8217;s firewall, so works with a different IP address. Otherwise, it becomes unreachable after the IP swap.</li>
<li>Boot the target KVM machine <span class="punch">in rescue mode</span>. No need to set up the ssh server as all will be done through VNC.</li>
<li>On the KVM machine, mount new system as /mnt/new:
<pre># mkdir /mnt/new
# mount /dev/vda3 /mnt/new</pre>
</li>
<li>On the OpenVZ server, check for recently changed directories and files:
<pre># find / -xdev -ctime -7 -type d | sort &gt; recently-changed-dirs.txt
# find / -xdev -ctime -7 -type f | sort &gt; recently-changed-files.txt</pre>
</li>
<li>Verify that the changes are only in the places that are going to be updated. If not, consider if and how to update these other files.</li>
<li>Verify that the mail queue is empty, or let sendmail empty it if possible. Not a good idea to have something firing off as soon as sendmail resumes:
<pre># mailq</pre>
</li>
<li>Disable all services except sshd on the OpenVZ server:
<pre># systemctl disable cron dovecot apache2 bind9 sendmail mysql xinetd</pre>
</li>
<li>Run &#8220;mailq&#8221; again to verify that the mail queue is empty (unless there was a reason to leave a message there in the previous check).</li>
<li>Reboot OpenVZ server and verify that none of these is running. This is the point at which this machine is dismissed as a server, and the downtime clock begins ticking.</li>
<li>Verify that this server doesn&#8217;t listen to any ports except ssh, as an indication that all services are down:
<pre># netstat -n -a | less</pre>
</li>
<li>Repeat the check of recently changed files.</li>
<li>On <strong>KVM machine</strong>, remove /var and /home.</li>
<li>
<pre># rm -rf /mnt/new/var /mnt/new/home</pre>
</li>
<li>Copy these parts:<br />
On the KVM machine, <span class="punch">using the VNC console</span>, go&nbsp;</p>
<pre># nc -l 1234 &lt; /dev/null | time tar -C /mnt/new/ -xzv</pre>
<p>and on myserver:</p>
<pre># tar --one-file-system -cz /var /home | nc -q 0 185.250.251.160 1234 &gt; /dev/null</pre>
<p>Took 28 minutes.</li>
<li>Check that /mnt/new/tmp and /mnt/tmp/run are empty and remove whatever is found, if there&#8217;s something there. There&#8217;s no reason for anything to be there, and it would be weird if there was, given the way the filesystem was copied from the original machine. But if there are any files, it&#8217;s just confusing, as /tmp and /run are tmpfs on the running machine, so any files there will be invisible anyhow.</li>
<li>Reboot the KVM machine <strong>with a reboot command</strong>. It will stop anyhow for removing the CDROM.</li>
<li>Remove the KVM&#8217;s CDROM and continue the reboot normally.</li>
<li>Login to the KVM machine with <span class="punch">ssh</span>.</li>
<li>Check that all is OK: systemctl status as well as journalctl. Note that the apache, mysql and dovecot should be running now.</li>
<li>Power down both virtual machines.</li>
<li>Request an IP address swap. Let them do whatever they want with the <span class="punch">IPv6</span> addresses, as they are ignored anyhow.</li>
</ul>
<h3>After IP address swap</h3>
<ul>
<li>Start the KVM server normally, and login normally <span class="punch">through ssh</span>.</li>
<li>Try to browse into the web sites: The web server should already be working properly (even though the DNS is off, but there&#8217;s a backup DNS).</li>
<li>Check journalctl and systemctl status.</li>
<li>Resume the original firewall rules and <span class="punch">verify that the firewall works properly</span>:
<pre># systemctl restart netfilter-persistent
# iptables -vn -L</pre>
</li>
<li>Start all services, and check status and journalctl again:
<pre># systemctl start cron dovecot apache2 bind9 sendmail mysql xinetd</pre>
</li>
<li>If all is fine, enable these services:
<pre># systemctl enable cron dovecot apache2 bind9 sendmail mysql xinetd</pre>
</li>
<li>Reboot (with reboot command), and check that all is fine.</li>
<li>In particular, send DNS queries directly to the server with dig, and also send an email to a foreign address (e.g. gmail). My web host blocked outgoing connections to port 25 on the new server, for example.</li>
<li>Delete ifcfg-venet0 and ifcfg-venet0:0 in /etc/sysconfig/network-scripts/, as they relate to the venet0 interface that exists only in the container machine. It&#8217;s just misleading to have it there.</li>
<li>Compare /etc/rc* and /etc/systemd with the situation before the transition in the git repo, to verify that everything is like it should be.</li>
</ul>
<ul>
<li>Check the server with nmap (run this from another machine):
<pre>$ nmap -v -A <span style="color: #888888;"><em>server</em></span>
$ sudo nmap -v -sU <span style="color: #888888;"><em>server</em></span></pre>
</li>
</ul>
<h3>And then the DNS didn&#8217;t work</h3>
<p>I knew very well why I left plenty of time free for after the IP swap. Something will always go wrong after a maneuver like this, and this time was no different. And for some odd reason, it was the bind9 DNS that played two different kinds of pranks.</p>
<p>I noted immediately that the server didn&#8217;t answer to DNS queries. As it turned out, there were two apparently independent reasons for it.</p>
<p>The first was that when I re-enabled the bind9 service (after disabling it for the sake of moving), systemctl went for the SYSV scripts instead of its own. So I got:</p>
<pre># <strong>systemctl enable bind9</strong>
Synchronizing state for bind9.service with sysvinit using update-rc.d...
Executing /usr/sbin/update-rc.d bind9 defaults
insserv: warning: current start runlevel(s) (empty) of script `bind9' overrides LSB defaults (2 3 4 5).
insserv: warning: current stop runlevel(s) (0 1 2 3 4 5 6) of script `bind9' overrides LSB defaults (0 1 6).
Executing /usr/sbin/update-rc.d bind9 enable</pre>
<p>This could have been harmless and gone unnoticed, had it not been that I&#8217;ve added a &#8220;-4&#8243; flag to bind9&#8242;s command, or else it wouldn&#8217;t work. So by running the SYSV scripts, my change in /etc/systemd/system/bind9.service wasn&#8217;t in effect.</p>
<p>Solution: Delete all files related to bind9 in /etc/init.d/ and /etc/rc*.d/. Quite aggressive, but did the job.</p>
<p>Having that fixed, it still didn&#8217;t work. The problem now was that eth0 was configured through DHCP after the bind9 had begun running. As a result, the DNS didn&#8217;t listen to eth0.</p>
<p>I slapped myself for thinking about adding a &#8220;sleep&#8221; command before launching bind9, and went for the right way to do this. Namely:</p>
<pre>$ <strong>cat /etc/systemd/system/bind9.service</strong>
[Unit]
Description=BIND Domain Name Server
Documentation=man:named(8)
After=network-online.target <span class="punch">systemd-networkd-wait-online.service</span>
Wants=network-online.target <span class="punch">systemd-networkd-wait-online.service</span>

[Service]
ExecStart=/usr/sbin/named -4 -f -u bind
ExecReload=/usr/sbin/rndc reload
ExecStop=/usr/sbin/rndc stop

[Install]
WantedBy=multi-user.target</pre>
<p>The systemd-networkd-wait-online.service is not there by coincidence. Without it, bind9 was launched before eth0 had received an address. With this, systemd consistently waited for the DHCP to finish, and then launched bind9. As it turned out, this also delayed the start of apache2 and sendmail.</p>
<p>If anything, network-online.target is most likely redundant.</p>
<p>And with this fix, the crucial row appeared in the log:</p>
<pre>named[379]: listening on IPv4 interface eth0, 193.29.56.92#53</pre>
<p>Another solution could have been to assign an address to eth0 statically. For some odd reason, I prefer to let DHCP do this, even though the firewall will block all traffic anyhow if the IP address changes.</p>
<h3>Using Live Ubuntu as rescue mode</h3>
<p>Set Ubuntu 24.04 server amd64 as the CDROM image.</p>
<p>After the machine has booted, send a Ctrl-Alt-F2 to switch to the second console. Don&#8217;t go on with the installation wizard, as it will of course wipe the server.</p>
<p>In order to establish an ssh connection:</p>
<ul>
<li>Choose a password for the default user (ubuntu-server).
<pre>$ passwd</pre>
<p>If you insist on a weak password, remember that you can do that only as root.</li>
<li>Use ssh to log in:
<pre>$ ssh ubuntu-server@185.250.251.160</pre>
</li>
</ul>
<p>Root login is forbidden (by default), so don&#8217;t even try.</p>
<p>Note that even though sshd apparently listens only to IPv6 ports, it&#8217;s actually accepting IPv4 connection by virtue of IPv4-mapped IPv6 addresses:</p>
<pre># <strong>lsof -n -P -i tcp 2&gt;/dev/null</strong>
COMMAND    PID            USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
systemd      1            root  143u  <span class="punch">IPv6</span>   5323      0t0  <span class="punch">TCP *:22</span> (LISTEN)
systemd-r  911 systemd-resolve   15u  IPv4   1766      0t0  TCP 127.0.0.53:53 (LISTEN)
systemd-r  911 systemd-resolve   17u  IPv4   1768      0t0  TCP 127.0.0.54:53 (LISTEN)
<span class="punch">sshd</span>      1687            root    3u  <span class="punch">IPv6</span>   5323      0t0  <span class="punch">TCP *:22</span> (LISTEN)
sshd      1847            root    4u  <span class="punch">IPv6</span>  11147      0t0  <span class="punch">TCP 185.250.251.160:22-&gt;85.64.140.6:57208</span> (ESTABLISHED)
sshd      1902   ubuntu-server    4u  IPv6  11147      0t0  TCP 185.250.251.160:22-&gt;85.64.140.6:57208 (ESTABLISHED)<span style="font-family: verdana, Arial, Helvetica, sans-serif; font-size: 16px;">One can get the impression that sshd listens only to IPv6. But somehow, it also accepts</span></pre>
<p>So don&#8217;t get confused by e.g. netstat and other similar utilities.</p>
<h3>To NTP or not?</h3>
<p>I wasn&#8217;t sure if I should run an NTP client inside a KVM virtual machine. So these are the notes I took.</p>
<ul>
<li><a rel="noopener" href="https://opensource.com/article/17/6/timekeeping-linux-vms" target="_blank">This</a> is a nice tutorial to start with.</li>
<li>It&#8217;s probably a good idea to run an NTP client on the client. It <a rel="noopener" href="https://sanjuroe.dev/sync-kvm-guest-using-ptp" target="_blank">would have been better to utilize the PTP protocol</a>, and get the host&#8217;s clock directly. But this is really an overkill. The drawback with these daemons is that if the client goes down and back up again, it will start with the old time, and then jump.</li>
<li>It&#8217;s also <a rel="noopener" href="https://doc.opensuse.org/documentation/leap/archive/42.1/virtualization/html/book.virt/sec.kvm.managing.clock.html" target="_blank">a good idea</a> to use kvm_clock in addition to NTP. This kernel feature uses the pvclock protocol to <a rel="noopener" href="https://docs.redhat.com/en/documentation/red_hat_enterprise_linux/6/html/virtualization_administration_guide/sect-virtualization-tips_and_tricks-libvirt_managed_timers#sect-timer-element" target="_blank">lets guest virtual machines read the host physical machine’s wall clock time</a> as well as its TSC. See <a rel="noopener" href="https://rwmj.wordpress.com/2010/10/15/kvm-pvclock/" target="_blank">this post for a nice tutoria</a>l about kvm_clock.</li>
<li>In order to know which clock source the kernel uses, <a rel="noopener" href="https://access.redhat.com/solutions/18627" target="_blank">look in /sys/devices/system/clocksource/clocksource0/current_clocksource</a>. Quite expectedly, it was kvm-clock (available sources were kvm-clock, tsc and acpi_pm).</li>
<li>It so turned out that systemd-timesyncd started running without my intervention when moving from a container to KVM.</li>
</ul>
<p>On a working KVM machine, timesyncd tells about its presence in the log:</p>
<pre>Jul 11 20:52:52 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/+0.001s/0.007s/0.003s/+0ppm
Jul 11 21:27:00 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.000s/0.007s/0.001s/+0ppm
Jul 11 22:01:08 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.002s/0.007s/0.001s/+0ppm
Jul 11 22:35:17 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.001s/0.007s/0.001s/+0ppm
Jul 11 23:09:25 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/+0.007s/0.007s/0.003s/+0ppm
Jul 11 23:43:33 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.003s/0.007s/0.005s/+0ppm (ignored)
Jul 12 00:17:41 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.006s/0.007s/0.005s/-1ppm
Jul 12 00:51:50 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/+0.001s/0.007s/0.005s/+0ppm
Jul 12 01:25:58 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/+0.002s/0.007s/0.005s/+0ppm
Jul 12 02:00:06 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/+0.002s/0.007s/0.005s/+0ppm
Jul 12 02:34:14 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.001s/0.007s/0.005s/+0ppm
Jul 12 03:08:23 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.000s/0.007s/0.005s/+0ppm
Jul 12 03:42:31 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.001s/0.007s/0.004s/+0ppm
Jul 12 04:17:11 myserver systemd-timesyncd[197]: interval/delta/delay/jitter/drift 2048s/-0.000s/0.007s/0.003s/+0ppm</pre>
<p>So a resync takes place every 2048 seconds (34 minutes and 8 seconds), like a clockwork. As apparent from the values, there&#8217;s no dispute about the time between Debian&#8217;s NTP server and the web host&#8217;s hypervisor.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/07/container-to-kvm-virtualization/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Running KVM on Linux Mint 19 random jots</title>
		<link>https://billauer.se/blog/2024/07/virtualization-notes-to-self-2/</link>
		<comments>https://billauer.se/blog/2024/07/virtualization-notes-to-self-2/#comments</comments>
		<pubDate>Fri, 12 Jul 2024 14:54:45 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Software]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7094</guid>
		<description><![CDATA[General Exactly like my previous post from 14 years ago, these are random jots that I took as I set up a QEMU/KVM-based virtual machine on my Linux Mint 19 computer. This time, the purpose was to prepare myself for moving a server from an OpenVZ container to KVM. Other version details, for the record: [...]]]></description>
			<content:encoded><![CDATA[<h3>General</h3>
<p>Exactly like <a rel="noopener" href="https://billauer.se/blog/2010/01/virtualization-notes-to-self/" target="_blank">my previous post</a> from 14 years ago, these are random jots that I took as I set up a QEMU/KVM-based virtual machine on my Linux Mint 19 computer. This time, the purpose was to prepare myself for moving a server from an OpenVZ container to KVM.</p>
<p>Other version details, for the record: libvirt version 4.0.0, QEMU version 2.11.1, Virtual Machine manager 1.5.1.</p>
<h3>Installation</h3>
<p>Install some relevant packages:</p>
<pre># apt install qemu-kvm qemu-utils libvirt-daemon-system libvirt-clients virt-manager virt-viewer ebtables ovmf</pre>
<p>This clearly installed a few services: libvirt-bin, libvirtd, libvirt-guest, virtlogd, qemu-kvm, ebtables, and a couple of sockets: virtlockd.socket and virtlogd.socket with their attached services.</p>
<p>My regular username on the computer was added automatically to the &#8220;libvirt&#8221; group, however that doesn&#8217;t take effect until one logs out and and in again. Without belonging to this group, one gets the error message &#8220;Unable to connect to libvirt qemu:///system&#8221; when attempting to run the Virtual Machine Manager. Or in more detail: &#8220;libvirtError: Failed to connect socket to &#8216;/var/run/libvirt/libvirt-sock&#8217;: Permission denied&#8221;.</p>
<p>The lazy and temporary solution is to run the Virtual Machine Manager with &#8220;sg&#8221;. So instead of the usual command for starting the GUI tool (NOT as root):</p>
<pre>$ virt-manager &amp;</pre>
<p>Use &#8220;sg&#8221; (or start a session with the &#8220;newgroup&#8221; command):</p>
<pre>$ sg libvirt virt-manager &amp;</pre>
<p>This is necessary only until next time you log in to the console. I think. I didn&#8217;t get that far. Who logs out?</p>
<p>There&#8217;s also a command-line utility, virsh. For example, to list all running machines:</p>
<pre>$ sudo virsh list</pre>
<p>Or just &#8220;sudo virsh&#8221; for an interactive shell.</p>
<p>Note that without root permissions, the list is simply empty. This is really misleading.</p>
<h3>General notes</h3>
<ul>
<li>Virtual machines are called &#8220;domains&#8221; in several contexts (within virsh in particular).</li>
<li>To get the mouse out of the graphical window, use Ctrl-Alt.</li>
<li>For networking to work, some rules related to virbr0 are automatically added to the iptables firewall. If these are absent, go &#8220;systemctl restart libvirtd&#8221; (don&#8217;t do this with virtual machines running, of course).</li>
<li>These iptables rules are important in particular for WAN connections. Apparently, these allow virbr0 to make DNS queries to the local machine (adding rules to INPUT and OUTPUT chains). In addition, the FORWARD rule allows forwarding anything to and from virbr0 (as long as the correct address mask is matched). Plus a whole lot off stuff around POSTROUTING. Quite disgusting, actually.</li>
<li>There are two Ethernet interfaces related to KVM virtualization: vnet0 and virbr0 (typically). For sniffing, virbr0 is a better choice, as it&#8217;s the virtual machine&#8217;s own bridge to the system, so there is less noise. This is also the interface that has an IP address of its own.</li>
<li>A vnetN pops up for each virtual machine that is running, virbr0 is there regardless.</li>
<li>The configuration files are kept as fairly readable XML files in /etc/libvirt/qemu</li>
<li>The images are typically held at /var/lib/libvirt/images, owned by root with 0600 permissions.</li>
<li>The libvirtd service runs /usr/sbin/libvirtd as well as two processes of /usr/sbin/dnsmasq. When a virtual machine runs, it also runs an instance of qemu-system-x86_64 on its behalf.</li>
</ul>
<h3>Creating a new virtual machine</h3>
<p>Start the Virtual Manager. The GUI is good enough for my purposes.</p>
<pre>$ sg libvirt virt-manager &amp;</pre>
<ul>
<li>Click on the &#8220;Create new virtual machine&#8221; and choose &#8220;Local install media&#8221;. Set the other parameters as necessary.</li>
<li>As for storage, choose &#8220;Select or create custom storage&#8221; and create a qcow2 volume in a convenient position on the disk (/var/lib/libvirt/images is hardly a good place for that, as it&#8217;s on the root partition).</li>
<li>In the last step, choose &#8220;customize configuration before install&#8221;.</li>
<li>Network selection: Virtual nework &#8216;default&#8217;: NAT.</li>
<li>Change the NIC, Disk and Video to VirtIO as mentioned below.</li>
<li>Click &#8220;Begin Installation&#8221;.</li>
</ul>
<h3>Do it with VirtIO</h3>
<p>That is, use Linux&#8217; paravirtualization drivers, rather than emulation of hardware.</p>
<p>To set up a machine&#8217;s settings, go View &gt; Details.</p>
<p>This is lspci&#8217;s response with a default virtual machine:</p>
<pre>00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Red Hat, Inc. QXL paravirtual graphic card (rev 04)
00:03.0 Ethernet controller: Realtek Semiconductor Co., Ltd. RTL-8100/8101L/8139 PCI Fast Ethernet Adapter (rev 20)
00:04.0 Audio device: Intel Corporation 82801FB/FBM/FR/FW/FRW (ICH6 Family) High Definition Audio Controller (rev 01)
00:05.0 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #1 (rev 03)
00:05.1 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #2 (rev 03)
00:05.2 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #3 (rev 03)
00:05.7 USB controller: Intel Corporation 82801I (ICH9 Family) USB2 EHCI Controller #1 (rev 03)
00:06.0 Communication controller: Red Hat, Inc Virtio console
00:07.0 Unclassified device [00ff]: Red Hat, Inc Virtio memory balloon</pre>
<p>Cute, but all interfaces are emulations of real hardware. In other words, this will run really slowly.</p>
<p>Testing link speed: On the host machine:</p>
<pre>$ nc -l 1234 &lt; /dev/null &gt; /dev/null</pre>
<p>And on the guest:</p>
<pre>$ dd if=/dev/zero bs=128k count=4k | nc -q 0 10.1.1.3 1234
4096+0 records in
4096+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 3.74558 s, 143 MB/s</pre>
<p>Quite impressive for hardware emulation, I must admit. But it can get better.</p>
<p>Things to change from the default settings:</p>
<ul>
<li>NIC: Choose &#8220;virtio&#8221; as device model, keep &#8220;Virtual network &#8216;default&#8217;&#8221; as NAT.</li>
<li>Disk: On &#8220;Disk bus&#8221;, don&#8217;t use IDE, but rather &#8220;VirtIO&#8221; (it will appear as /dev/vda etc.).</li>
<li>Video: Don&#8217;t use QXL, but Virtio (without 3D acceleration, it wasn&#8217;t supported on my machine). Actually, I&#8217;m not so sure about this one. For example, Ubuntu&#8217;s installation live boot gave me a black screen occasionally with Virtio.</li>
</ul>
<p>Note that it&#8217;s possible to use a VNC server instead of &#8220;Display spice&#8221;.</p>
<p>After making these changes:</p>
<pre>00:00.0 Host bridge: Intel Corporation 440FX - 82441FX PMC [Natoma] (rev 02)
00:01.0 ISA bridge: Intel Corporation 82371SB PIIX3 ISA [Natoma/Triton II]
00:01.1 IDE interface: Intel Corporation 82371SB PIIX3 IDE [Natoma/Triton II]
00:01.3 Bridge: Intel Corporation 82371AB/EB/MB PIIX4 ACPI (rev 03)
00:02.0 VGA compatible controller: Red Hat, Inc <span class="punch">Virtio</span> GPU (rev 01)
00:03.0 Ethernet controller: Red Hat, Inc <span class="punch">Virtio</span> network device
00:04.0 Audio device: Intel Corporation 82801FB/FBM/FR/FW/FRW (ICH6 Family) High Definition Audio Controller (rev 01)
00:05.0 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #1 (rev 03)
00:05.1 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #2 (rev 03)
00:05.2 USB controller: Intel Corporation 82801I (ICH9 Family) USB UHCI Controller #3 (rev 03)
00:05.7 USB controller: Intel Corporation 82801I (ICH9 Family) USB2 EHCI Controller #1 (rev 03)
00:06.0 Communication controller: Red Hat, Inc Virtio console
00:07.0 Unclassified device [00ff]: Red Hat, Inc Virtio memory balloon
00:08.0 SCSI storage controller: Red Hat, Inc <span class="punch">Virtio</span> block device</pre>
<p>Try the speed test again?</p>
<pre>$ dd if=/dev/zero bs=128k count=4k | nc -q 0 10.1.1.3 1234
4096+0 records in
4096+0 records out
536870912 bytes (537 MB, 512 MiB) copied, 0.426422 s, 1.3 GB/s</pre>
<p>Almost ten times faster.</p>
<h3>Preparing a live Ubuntu ISO for ssh</h3>
<pre>$ sudo su
# apt install openssh-server
# passwd ubuntu</pre>
<p>In the installation of the openssh-server, there&#8217;s a question of which configuration files to use. Choose the package maintainer&#8217;s version.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/07/virtualization-notes-to-self-2/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
