<?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; Server admin</title>
	<atom:link href="http://billauer.se/blog/category/server-admin/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>Long list of IP addresses attacking a phpBB forum in May 2025</title>
		<link>https://billauer.se/blog/2025/05/phpbb-attack-bots-ip-addresses/</link>
		<comments>https://billauer.se/blog/2025/05/phpbb-attack-bots-ip-addresses/#comments</comments>
		<pubDate>Mon, 12 May 2025 17:07:51 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7156</guid>
		<description><![CDATA[A good-old phpBB forum that I run had a massive surge of traffic somewhere in May 2025. This had already started a couple of months earlier, but in May it knocked down the web server due to the number of apache instances. Plus the logs reached ~ 3 GB. This doesn&#8217;t seem to be a [...]]]></description>
			<content:encoded><![CDATA[<p>A good-old phpBB forum that I run had a massive surge of traffic somewhere in May 2025. This had already started a couple of months earlier, but in May it knocked down the web server due to the number of apache instances. Plus the logs reached ~ 3 GB.</p>
<p>This doesn&#8217;t seem to be a DDoS attack, mainly because the access from each IP address was far more scarce than a legit bot: There would be several minutes, if not hours between each request. With the huge amount of bots involved, it would be easy to completely knock out the website with a moderate access pattern from each IP address. Besides, there&#8217;s no reason to attack the specific forum. It more looks like a very brute-force attempt to hack the forum itself for the purpose of spamming or something.</p>
<p>One could turn to Cloudflare in situations like this, but I tried the DIY approach. Namely, to block the IP addresses of the bots by virtue of the firewall, as I discuss in a <a rel="noopener" href="https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/" target="_blank">separate post of mine</a>. The strategy with these bots was somewhat different: As the phpBB related bots occasioanally did something that no regular user would do (what exactly I&#8217;ll keep to myself) it was enough to detect this event and blacklist the IP address right away.</p>
<p>Which I did. After a couple of day, the list landed on ~78,000 IP addresses. Note that this is larger than ipset&#8217;s default number of allowed elements, which is 65536. So when creating the ipset, be sure to use maxelem with a larger number (I used 524288 when I didn&#8217;t know how bad the situation was).</p>
<p>I checked a few of these addresses, and they appear to origin from Asia (Vietnam, Indonesia etc.).</p>
<p>The nice thing is that 24-48 hours after I enabled the related blacklisting mechanism, the attack virtually stopped. The firewall recorded no more than ~30 dropped packets per minute on the ipset-matching rule. True victory.</p>
<p>The list of IPs can be <a href="/download/ips-phpbb-attack-may-2025.txt.zip" target="_blank">downloaded here</a>, in case it helps someone understand what&#8217;s going on. If you do have an idea, please comment below.</p>
<p>It&#8217;s a plain zip of a list of IP addresses, one address per row. It&#8217;s possible that a few IP unrelated addresses appear on this list, which could be other kinds of bots that were blacklisted on the same server. Maybe a dozen of these.</p>
<h3>Ehm, no</h3>
<p>The silence was short-lived. A couple of days later, the bots came again, this time in a much larger scale. Within a few days, I collected ten times as many IP addresses, that is ~840,000 of them. That&#8217;s a really large number. This larger list of IPs can be <a href="https://billauer.se/download/ips-phpbb-attack-may-2025-bigger.txt.zip" target="_blank">downloaded here</a>. It was obtained exactly the same as the previous one.</p>
<p>But after this swarm, it got silent again. For a while. In the days that followed, the same traffic pattern came back, IPs were blacklisted (~250k of them at some point) and so it went on. But with the blacklisting mechanism, the load on the server was quite small, so for now I consider the problem solved. Let&#8217;s hope it stays this way.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2025/05/phpbb-attack-bots-ip-addresses/feed/</wfw:commentRss>
		<slash:comments>3</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>Altering the Message-ID header in Thunderbird for non-spam detection</title>
		<link>https://billauer.se/blog/2024/08/mail-message-id-spam-filtering/</link>
		<comments>https://billauer.se/blog/2024/08/mail-message-id-spam-filtering/#comments</comments>
		<pubDate>Sat, 10 Aug 2024 10:38:18 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[email]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=7125</guid>
		<description><![CDATA[TL;DR In this post, I suggest manipulating the Message IDs of outgoing mails, so that legit inbound replies to my mails are easily detected as non-spam. I also show how to do this with Thunderbird (Linux version 91.10.0, but it works with practically all versions, I believe). Briefly about Message-ID Each email should have a [...]]]></description>
			<content:encoded><![CDATA[<h3>TL;DR</h3>
<p>In this post, I suggest manipulating the Message IDs of outgoing mails, so that legit inbound replies to my mails are easily detected as non-spam. I also show how to do this with Thunderbird (Linux version 91.10.0, but it works with practically all versions, I believe).</p>
<h3>Briefly about Message-ID</h3>
<p>Each email should have a Message-ID header, which uniquely identifies this message. The value of this header <a rel="noopener" href="https://www.rfc-editor.org/rfc/rfc2822#section-3.6.4" target="_blank">should consist</a> of a random string, followed by an &#8216;@&#8217; and a string that represents the domain name (referred to as FQDN, Fully Qualified Domain Name). This is often the full domain name of the &#8220;From&#8221; header (e.g. gmail.com).</p>
<p>For example, an email generated by Gmail&#8217;s web client had Message-ID: &lt;CAD8P7-R2OuJvGiuQ-0RQqgSSmDguwv1VdjHgQND4jMJxPc628w@mail.gmail.com&gt;. A similar result (same FQDN) was obtained when sending from the phone. However, when using Thunderbird to send an email, only &#8220;gmail.com&#8221; was set as the FQDN.</p>
<h3>Does the Message-ID matter?</h3>
<p>Like anything related to email, there are a lot of actors, and each has its own quirks. For example, rspamd adds the spam score by 0.5, with the MID_RHS_NOT_FQDN rule, if the Message ID isn&#8217;t an FQDN. I&#8217;m not sure to which extent it checks that the FQDN matches the email&#8217;s From, but even if it does, it can&#8217;t be that picky, given the example I showed above in relation to gmail.com.</p>
<p>It&#8217;s quite rare that people care about this header. I&#8217;ve <a rel="noopener" href="https://forum.emclient.com/t/message-id-contains-local-computer-name-can-it-be-changed/68828" target="_blank">seen somewhere</a> that someone sending mails from a work computer didn&#8217;t like that the name of the internal domain leaking.</p>
<p>All in all, it&#8217;s probably a good idea to make sure that the Message-ID header looks legit. Putting the domain from the From header seems to be a good idea to keep spam filters happy.</p>
<h3>Why manipulate the Message-ID?</h3>
<p>In an reply, the In-Reply-To header gets the value of the Message ID of the message replied to. So if a spam filter can identify that that the email is genuinely a reply to something I sent, it&#8217;s definitely not spam. It&#8217;s also a good idea to scan the References header too, in order to cover more elaborate scenarios when there are several people corresponding.</p>
<p>The rigorous way to implement this spam filtering feature is storing the Message IDs of all sent mails in some small database, and check for a match with the content of In-Reply-To of arriving mails. Possible, however daunting.</p>
<p>A much easier way is to change the FQDN part, so that it&#8217;s easily identifiable. This is unnecessary if you happen send emails with your own domain, as spam senders are very unlikely to add an In-Reply-To with a matching domain (actually, very few spam messages have an In-Reply-To header at all).</p>
<p>But for email sent through gmail, changing the FQDN to something unique is required to make a distinction.</p>
<p>Will this mess up things? I&#8217;m not sure any software tries to fully match the FQDN with the sender, but I suppose it&#8217;s safe to add a subdomain to the correct domain. I mean, if both &#8220;mail.gmail.com&#8221; and &#8220;gmail.com&#8221; are commonly out there, why shouldn&#8217;t &#8220;secretsauce.gmail.com&#8221; seem likewise legit to any spam filter that checks the message?</p>
<p>And by the way, as of August 2024, a DNS query for mail.gmail.com yields no address, neither for A nor MX. In other words, Gmail itself uses an invalid domain in its Message ID, so any other invented subdomain should do as well.</p>
<h3>Changing the FQDN on Thunderbird</h3>
<p>Click the hamburger icon, choose Preferences, and scroll down all the way (on the General tab) and click on Config Editor.</p>
<p>First, we need to find Thunderbird&#8217;s internal ID number for the mail account to manipulate.</p>
<p>To get a list of IDs, write &#8220;useremail&#8221; in the search text box. This lists entries like mail.identity.id1.useremail and their values. This listing allows making the connection between e.g. &#8220;id1&#8243; and the email address related to it.</p>
<p>For example, to change the FQDN of the mail account corresponding to &#8220;id3&#8243;, add a string property (using the Config Editor). The key of this property is &#8220;mail.identity.id3.FQDN&#8221; and the value is something like &#8220;secretsauce.gmail.com&#8221;.</p>
<p>There is no need to restart Thunderbird. The change is in effect on the next mail sent, and it remains in the settings across restarts.</p>
<p>The need for this feature has been questioned, as was discussed <a rel="noopener" href="https://thunderbird.topicbox.com/groups/planning/T53e9959c6d6c12a9-M030a61c790caab2d7763a25b" target="_blank">here</a>. So if any Thunderbird maintainer reads this, please keep this feature up and running.</p>
<h3>A possible alternative approach</h3>
<p>Instead of playing around with the Message-ID, it would be possible to add an entry to the References header (or add this header if there is none). The advantage of this way is that this can also be done by the MTA further down the delivery path, and it doesn&#8217;t alter anything that is already in place.</p>
<p>And since it&#8217;s an added entry, it can also be crafted arbitrarily. For example, it may contain a timestamp (epoch time in hex) and the SHA1 sum of a string that is composed by this timestamp and a secret string. This way, this proof of genuine correspondence is impossible to forge and may expire with time.</p>
<p>I haven&#8217;t looked into how to implement this in Thunderbird. Right now I&#8217;m good with the Message-ID solution.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2024/08/mail-message-id-spam-filtering/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>Blocking bots by their IP addresses, the DIY version</title>
		<link>https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/</link>
		<comments>https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/#comments</comments>
		<pubDate>Tue, 16 Aug 2022 10:26:37 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[perl]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6670</guid>
		<description><![CDATA[Introduction I had some really annoying bots on one of my websites. Of the sort that make a million requests (like really, a million) per month, identifying themselves as a browser. So IP blocking it is. I went for a minimalistic DIY approach. There are plenty of tools out there, but my experience with things [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>I had some really annoying bots on one of my websites. Of the sort that make a million requests (like really, a million) per month, identifying themselves as a browser.</p>
<p>So IP blocking it is. I went for a minimalistic DIY approach. There are plenty of tools out there, but my experience with things like this is that in the end, it&#8217;s me and the scripts. So I might as well write them myself.</p>
<h3>The IP set feature</h3>
<p>Iptables has an IP set module, which allows feeding it with a set of random IP addresses. Internally, it creates a hash with these addresses, so it&#8217;s an efficient way to keep track of multiple addresses.</p>
<p>IP sets has been in the kernel since ages, but it has to be opted in the kernel with CONFIG_IP_SET. Which it most likely is.</p>
<p>The ipset utility may need to be installed, with something like</p>
<pre># apt install ipset</pre>
<p>There seems to be a protocol mismatch issue with the kernel, which apparently is a non-issue. But every time something goes wrong with ipset, there&#8217;s a warning message about this mismatch, which is misleading. So it looks something like this.</p>
<pre># ipset <span class="yadayada">[ ... something stupid or malformed ... ]</span>
ipset v6.23: Kernel support protocol versions 6-7 while userspace supports protocol versions 6-6
<span class="yadayada">[ ... some error message related to the stupidity ... ]</span></pre>
<p>So the important thing is to be aware of is that odds are that the problem isn&#8217;t the version mismatch, but between chair and keyboard.</p>
<h3>Hello, world</h3>
<p>A quick session</p>
<pre># ipset create testset hash:ip
# ipset add testset 1.2.3.4
# iptables -I INPUT -m set --match-set testset src -j DROP
# ipset del testset 1.2.3.4</pre>
<p>Attempting to add an IP address that is already in the list causes a warning, and the address isn&#8217;t added. So no need to check if the address is already there. Besides, there the -exist option, which is really great.</p>
<p>List the members of the IP set:</p>
<pre># ipset -L</pre>
<h3>Timeout</h3>
<p>An entry can have a timeout feature, which works exactly as one would expect: The rule vanishes after the timeout expires. The timeout entry in ipset -L counts down.</p>
<p>For this to work, the set must be created with a default timeout attribute. Zero means that timeout is disabled (which I chose as a default in this example).</p>
<pre># ipset create testset hash:ip timeout 0
# ipset add testset 1.2.3.4 timeout 10</pre>
<p>The &#8216;-exist&#8217; flag causes ipset to re-add an existing entry, which also resets its timeout. So this is the way to keep the list fresh.</p>
<h3>Don&#8217;t put the DROP rule first</h3>
<p>It&#8217;s tempting to put the DROP rule with &#8211;match-set first, because hey, let&#8217;s give those intruders the boot right away. But doing that, there might be TCP connections lingering, because the last FIN packet is caught by the firewall as the new rule is added. Given that adding an IP address is the result of a flood of requests, this is a realistic scenario.</p>
<p>The solution is simple: There&#8217;s most likely a &#8220;state RELATED,ESTABLISHED&#8221; rule somewhere in the list. So push it to the top. The rationale is simple: If a connection has begun, don&#8217;t chop it in the middle in any case. It&#8217;s the first packet that we want killed.</p>
<h3>Persistence</h3>
<p>The rule in iptables must refer to an existing set. So if the rule that relies on the set is part of the persistent firewall rules, it must be created before the script that brings up iptables runs.</p>
<p>This is easily done by adding a rule file like this as /usr/share/netfilter-persistent/plugins.d/10-ipset</p>
<pre><span class="hljs-meta">#!/bin/sh</span>

IPSET=/sbin/ipset
SET=mysiteset

<span class="hljs-keyword">case</span> <span class="hljs-string">"<span class="hljs-variable">$1</span>"</span> <span class="hljs-keyword">in</span>
start|restart|reload|force-reload)
	<span class="hljs-variable">$IPSET</span> destroy
	<span class="hljs-variable">$IPSET</span> create <span class="hljs-variable">$SET</span> <span class="hljs-built_in">hash</span>:ip <span class="hljs-built_in">timeout</span> 0
	;;

save)
	<span class="hljs-built_in">echo</span> <span class="hljs-string">"ipset-persistent: The save option does nothing"</span>
	;;

stop|flush)
	<span class="hljs-variable">$IPSET</span> flush <span class="hljs-variable">$SET</span>
	;;
*)
    <span class="hljs-built_in">echo</span> <span class="hljs-string">"Usage: <span class="hljs-variable">$0</span> {start|restart|reload|force-reload|save|flush}"</span> &gt;&amp;2
    <span class="hljs-built_in">exit</span> 1
    ;;
<span class="hljs-keyword">esac</span>

<span class="hljs-built_in">exit</span> 0</pre>
<p>The idea is that the index 10 in the file&#8217;s name is smaller than the rule that sets up iptables, so it runs first.</p>
<p>This script is a dirty hack, but hey, it works. There&#8217;s a <a rel="noopener" href="https://sourceforge.net/p/ipset-persistent/wiki/Home/" target="_blank">small project</a> on this, for those who like to do it properly.</p>
<p>The operating system in question is systemd-based, but this old school style is still in effect.</p>
<h3>Maybe block by country?</h3>
<p>Since all offending requests came from the same country (cough, cough, China, from more than 4000 different IP addresses) I&#8217;m considering to block them in one go. A list of 4000+ IP addresses that I busted in August 2022 with aggressive bots (all from China) can be downloaded as a simple <a href="/download/china-bots-ip-addresses.txt.gz" target="_blank">compressed text file</a>.</p>
<p>So the idea is going something like</p>
<pre>ipset create foo hash:net
ipset add foo 192.168.0.0/24
ipset add foo 10.1.0.0/16
ipset add foo 192.168.0/24</pre>
<p>and download the per-country IP ranges from <a href="https://www.ipdeny.com/ipblocks/" target="_blank">IP deny</a>. That&#8217;s a simple and crude tool for denial by geolocation. The only thing that puts me down a bit is that it&#8217;s &gt; 7000 rules, so I wonder if that doesn&#8217;t put a load on the server. But what really counts is the number of sizes of submasks, because each submask size has its own hash. So if the list covers all possible  sizes, from a full /32 down to say, 16/, there are 17 hashes to look up for each packet arriving.</p>
<p>On the other hand, since the rule should be after the &#8220;state RELATED,ESTABLISHED&#8221; rule, it only covers SYN packets. And if this whole thing is put as late as possible in the list of rules, it boils down to handling only packets that are intended for the web server&#8217;s ports, or those that are going to be dropped anyhow. So compared with the CPU cycles of handling the http request, even 17 hashes isn&#8217;t all that much.</p>
<p>The biggest caveat is however if other websites are colocated on the server. It&#8217;s one thing to block offending IPs, but blocking a whole country from all sites, that&#8217;s a bit too much.</p>
<p><em>Note to self: In the end, I wrote a little Perl-XS module that says if the IP belongs to a group. Look for byip.pm.</em></p>
<h3>The blacklisting script</h3>
<p>The Perl script that performs the blacklisting is crude and inaccurate, but simple. This is the part to tweak and play with, and in particular adapt to each specific website. It&#8217;s all about detecting abnormal access.</p>
<p>Truth to be told, I replaced this script with a more sophisticated mechanism pretty much right away on my own system. But what&#8217;s really interesting is the calls to ipset.</p>
<p>This script reads through Apache&#8217;s access log file, and analyzes each minute in time (as in 60 seconds). In other words, all accesses that have the same timestamp, with the seconds part ignored. Note that the regex part that captures $time in the script ignores the last part of :\d\d.</p>
<p>If the same IP address appears more than 50 times, that address is blacklisted, with a timeout of 86400 seconds (24 hours). Log file that correspond to page requisites and such (images, style files etc.) are skipped for this purpose. Otherwise, it&#8217;s easy to reach 50 accesses within a minute with legit web browsing.</p>
<p>There are several imperfections about this script, among others:</p>
<ul>
<li>Since it reads through the entire log file each time, it keeps relisting each IP address until the access file is rotated away, and a new one is started. This causes an update of the timeout, so effectively the blacklisting takes place for up to 48 hours.</li>
<li>Looking in segments of accesses that happen to have the same minute in the timestamp is quite inaccurate regarding which IPs are caught and which aren&#8217;t.</li>
</ul>
<p>The script goes as follows:</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">my</span> $logfile = <span class="hljs-string">'/var/log/mysite.com/access.log'</span>;
<span class="hljs-keyword">my</span> $limit = <span class="hljs-number">50</span>; <span class="hljs-comment"># 50 accesses per minute</span>
<span class="hljs-keyword">my</span> $timeout = <span class="hljs-number">86400</span>;

<span class="hljs-keyword">open</span>(<span class="hljs-keyword">my</span> $in, <span class="hljs-string">"&lt;"</span>, $logfile)
  <span class="hljs-keyword">or</span> <span class="hljs-keyword">die</span> <span class="hljs-string">"Can't open $logfile for read: $!\n"</span>;

<span class="hljs-keyword">my</span> $current = <span class="hljs-string">''</span>;
<span class="hljs-keyword">my</span> $l;
<span class="hljs-keyword">my</span> %h;
<span class="hljs-keyword">my</span> %blacklist;

<span class="hljs-keyword">while</span> (<span class="hljs-keyword">defined</span> ($l = &lt;$in&gt;)) {
  <span class="hljs-keyword">my</span> ($ip, $time, $req) = ($l =~ <span class="hljs-regexp">/^([^ ]+).*?\[(.+?):\d\d[ ].*?\"\w+[ ]+([^\"]+)/</span>);
  <span class="hljs-keyword">unless</span> (<span class="hljs-keyword">defined</span> $ip) {
    <span class="hljs-comment">#    warn("Failed to parse line $l\n");</span>
    <span class="hljs-keyword">next</span>;
  }

  <span class="hljs-keyword">next</span>
    <span class="hljs-keyword">if</span> ($req =~ <span class="hljs-regexp">/^\/(?:media\/|robots\.txt)/</span>);

  <span class="hljs-keyword">unless</span> ($time eq $current) {
    <span class="hljs-keyword">foreach</span> <span class="hljs-keyword">my</span> $k (<span class="hljs-keyword">sort</span> <span class="hljs-keyword">keys</span> %h) {
      $blacklist{$k} = <span class="hljs-number">1</span>
	<span class="hljs-keyword">if</span> ($h{$k} &gt;= $limit);
    }

    %h = ();
    $current = $time;
  }
  $h{$ip}++;
}

<span class="hljs-keyword">close</span> $in;

<span class="hljs-keyword">foreach</span> <span class="hljs-keyword">my</span> $k (<span class="hljs-keyword">sort</span> <span class="hljs-keyword">keys</span> %blacklist) {
  <span class="hljs-keyword">system</span>(<span class="hljs-string">'/sbin/ipset'</span>, <span class="hljs-string">'add'</span>, <span class="hljs-string">'-exist'</span>, <span class="hljs-string">'mysiteset'</span>, $k, <span class="hljs-string">'timeout'</span>, $timeout);
}</pre>
<p>It has to be run as root, of course. Most likely as a cronjob.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/08/spiders-bots-denial-iptables-ipset/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>Fetchmail and Google&#8217;s OAuth 2.0 enforcement</title>
		<link>https://billauer.se/blog/2022/06/fetchmail-gmail-lsa-oauth2/</link>
		<comments>https://billauer.se/blog/2022/06/fetchmail-gmail-lsa-oauth2/#comments</comments>
		<pubDate>Sat, 11 Jun 2022 13:54:53 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[email]]></category>
		<category><![CDATA[Internet]]></category>
		<category><![CDATA[Server admin]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6627</guid>
		<description><![CDATA[This post is about fetching mail. For sending emails through OAuth2-enabled SMTP servers, see this post. Introduction After a long time that Google&#8217;s smtp server occasionally refused to play ball with fetchmail, tons of Critical Alerts on &#8220;someone knowing my password&#8221; and requests to move away from &#8220;Less Secure Apps&#8221; (LSA) and other passive-aggressive behaviors, [...]]]></description>
			<content:encoded><![CDATA[<p><em>This post is about fetching mail. For sending emails through OAuth2-enabled SMTP servers, see <a title="Using git send-email with Gmail + OAUTH2, but without subscribing to cloud services" href="https://billauer.se/blog/2022/10/git-send-email-with-oauth2-gmail/" target="_blank">this post</a>.</em></p>
<h3>Introduction</h3>
<p>After a long time that Google&#8217;s smtp server occasionally refused to play ball with fetchmail, tons of Critical Alerts on &#8220;someone knowing my password&#8221; and requests to move away from &#8220;Less Secure Apps&#8221; (LSA) and other passive-aggressive behaviors, I eventually got the famous &#8220;On May 30, you may lose access&#8221; mail. That was in the beginning of March.</p>
<p>My reaction to that was exactly the same as to previous warnings: I ignored it. Given the change that was needed to conform to Google&#8217;s requirements, I went on with the strategy that &#8220;may&#8221; doesn&#8217;t necessarily mean &#8220;will&#8221;, and that I don&#8217;t move until they pull the plug. Which they didn&#8217;t on May 30th, as suggested. Instead, it happened on June 8th.</p>
<p>Since I don&#8217;t intend to ditch my Gmail address, it follows that OAuth2 is going to be part of my life from now on, and that I&#8217;ll probably have to figure out why this or that doesn&#8217;t work out every now and then. So I might as well learn the darn thing once and for all. Which I just did.</p>
<p>This post consists of my own understanding on this matter. More than anything, it&#8217;s intended as a reminder to myself each time I&#8217;ll need to shove my hands deep into some related problem.</p>
<p>None of the covered subjects have anything to do with my professional activity as an engineer.</p>
<h3>Spoiler: My decisions on my own mail processing</h3>
<p>This is my practical conclusion of everything written below, so the TL;DR is that I decided to stop using Fetchmail with Google&#8217;s mail servers. Instead, I&#8217;ve set Gmail to forward all emails to one of my other email accounts, to which I have pop3 access with Fetchmail (it&#8217;s actually on a server I control). I just added another transmission leg.</p>
<p>This is mainly because of the possibility that continuing to use Fetchmail with Google&#8217;s server will require my personal attention every now and then, for reasons I elaborate on below. It&#8217;s not impossible, but I&#8217;m not sure Fetchmail with Gmail is going to be a background thing anymore.</p>
<p>Mail forwarding is a solid solution to this. It doesn&#8217;t create a single point of failure, because I can always access the mails with Gmail&#8217;s web interface, should there be a problem with the forward-to-Fetchmail route. The only nasty thing that can happen is that the forwarding&#8217;s destination email address may be disclosed to the sender, if the delivery fails for some reason: It appears in the bounce message.</p>
<p>So if you want to use the forwarding method for an email address that you keep for the sake of anonymity, you&#8217;ll have to use a destination email address that says nothing about you either. There are plenty of email services with POP3 support at a fairly low cost.</p>
<p>The only reason I still need to live with OAuth2 support is that emails that I send with my Gmail address must go through Google&#8217;s servers, or else they are rejected by a whole lot of mail servers out there by virtue of <a rel="noopener" href="https://billauer.se/blog/2019/03/spf-dkim-dmarc-email-domain-delivered/" target="_blank">DMARC</a>.</p>
<p>So I <a rel="noopener" href="https://billauer.se/blog/2022/06/thunderbird-installation-oauth2/" target="_blank">upgraded Thunderbird</a> to a version that supports OAuth2, and it works nicely with Google. I could have fetched the emails with Thunderbird too, but I still want to run my own spam filter, which I why I want fetchmail to remain in the loop for arriving mails.</p>
<p>And now, to the long story.</p>
<h3>What is OAuth2?</h3>
<p>To make a long story short, it&#8217;s the mechanism behind &#8220;Login with Google / Facebook / whatever&#8221;. Rather than having the user maintain a username and password for every service it accesses, there&#8217;s one Authorization Server, say Google, that maintains the capability to verify the actual user.</p>
<p>The idea is that when the user wants to use some website with &#8220;Login with Google&#8221;, the website doesn&#8217;t need to check the user&#8217;s identity itself, but instead it relies on the authentication made by Google. As a bonus, the fact that the user has logged into the site with Google, allows the site&#8217;s back-end (that is, the web server) to access some Google services on behalf of the user. For example, to add an entry in the user&#8217;s Google calendar.</p>
<p>To make this work, the site&#8217;s back-end needs to be able to prove that it&#8217;s eligible to act on behalf of the said user. For this purpose, it obtains an <em>access token </em>from the Authorization Server. In essence, this access token is a short-lived password for performing certain tasks on behalf of a certain user.</p>
<p>So an access token is limited in three ways:</p>
<ul>
<li>It&#8217;s related to a specific Google user</li>
<li>It&#8217;s limited in time</li>
<li>It gives its owner only specific permissions to carry out operations, or as they&#8217;re called, <em>scopes</em>.</li>
</ul>
<p>For the sake of fetching emails, the recent change was that Gmail moved from accepting username + password authentication to only accepting an access token that allows the relevant user to perform pop / imap operations.</p>
<h3>Interactions with the Authorization Server</h3>
<p>The Authorization Server is responsible mainly for two tasks:</p>
<ul>
<li>The initial authentication, which results in obtaining an access token, a refresh token and various information (in JSON format).</li>
<li>The refreshing of the access token, which is performed to replace an expired or soon-to-expire access token with a valid one.</li>
</ul>
<p>So the overall picture is that it starts with some initial authentication, and then the owner of the access token keeps extending its validity by recurring refresh requests.</p>
<p>The initial authentication is done by a human using a web browser. That&#8217;s the whole point. This allows the Authorization Server to control the level of torture necessary to obtain the access token. It may not require any action if the user is already safely logged in, and it may suddenly decide to ask silly questions and/or perform two-factor authentication and whatnot.</p>
<p>Refreshing the access token is a computer-to-computer protocol that requires no human interaction. In principle, access can be granted forever based upon that initial authentication by refreshing the token indefinitely. But the Authorization Server is nevertheless allowed to refuse a refresh request for any or no reason. In fact, this is the way Google can force us humans to pay attention. The documentation tends to imply that tokens are always refreshed, but at the same time clearly state that the requester of a refresh should handle a refusal gracefully by reverting to the browser thing.</p>
<p>Remember those &#8220;suspicious activity&#8221; notifications from Google, begging us to confirm that it was us doing something on an uknown device? No need to beg anymore. If Google wants us to confirm something, it just denies the token refresh request. The only way to resume access is going back to initial authentication. This brings the human user to a browser soon enough to re-authenticate, which is a good opportunity to sort out whatever needs sorting out.</p>
<p>For example, if Thunderbird is used to access mail from Gmail with OAuth2, it must have the capability to open a browser window in order to perform the initial authentication (which it does nowadays). Hence if a refresh requests fails, this browser window will be opened again for further action. So there&#8217;s a means to talk with the human user. This possibility didn&#8217;t exist with the old password authentication, because if that failed, the user was prompted for a new password. So there was no reasonable way to initiate communication with the human user by refusing access.</p>
<p>How obnoxious service providers intend to be with this new whip is yet to be seen, but it&#8217;s clear that OAuth2 opens that possibility. The fact that access tokens are currently refreshed forever without the need to re-authenticate, doesn&#8217;t say how it&#8217;s going to be in the future.</p>
<p>As a bit of a side note, it&#8217;s common practice that access to cloud services can be made with an initial authentication that doesn&#8217;t involve a web browser. This makes sense, as software that consumes these services typically runs on servers with no human around. Today, this can be used to obtain tokens for Gmail access, but I doubt that will go on for long.</p>
<h3>The authentication handshake in a nutshell</h3>
<p>There are plenty of resources on OAuth2: To be begin with, there&#8217;s <a rel="noopener" href="https://datatracker.ietf.org/doc/html/rfc6749" target="_blank">RFC 6749</a>, which defines OAuth2, and several tutorials on the matter, for example <a rel="noopener" href="https://www.digitalocean.com/community/tutorials/an-introduction-to-oauth-2" target="_blank">this one</a>. And there&#8217;s <a rel="noopener" href="https://developers.google.com/identity/protocols/oauth2?hl=en" target="_blank">Google&#8217;s page</a> on using OAuth2 for accessing Google APIs, which is maybe the most interesting one, as it walks through the different usage scenarios, including devices that can&#8217;t run a web browser.</p>
<p>This way or another, it boils down to the following stages for a website with &#8220;Login with X&#8221;:</p>
<ul>
<li>A web browser goes to the Authorization Server with a URL that includes information about the request, by virtue of a link saying &#8220;Login with Google&#8221; or something like that. It&#8217;s typically a very long and tangled URL with several CGI-style parameters (it&#8217;s a GET request).  Among the parameters in the link, there&#8217;s the client ID (who is requesting access), what kind of access is required from Google&#8217;s servers (the scopes) and to what URL the Authorization Server should redirect the browser when it&#8217;s done torturing the human in front of the browser. For example, the link used by TikTok&#8217;s &#8220;Continue with Google&#8221; goes
<pre>https://accounts.google.com/o/oauth2/v2/auth/identifier?client_id=1096011445005-sdea0nf5jvj14eia93icpttv27cidkvk.apps.googleusercontent.com&amp;response_type=token&amp;redirect_uri=https%3A%2F%2Fwww.tiktok.com%2Flogin%2F&amp;state=%7B%22client_id%22%3A%221096011445005-sdea0nf5jvj14eia93icpttv27cidkvk.apps.googleusercontent.com%22%2C%22network%22%3A%22google%22%2C%22display%22%3A%22popup%22%2C%22callback%22%3A%22_hellojs_5kkckpps%22%2C%22state%22%3A%22%22%2C%22redirect_uri%22%3A%22https%3A%2F%2Fwww.tiktok.com%2Flogin%2F%22%2C%22scope%22%3A%22basic%22%7D&amp;scope=openid%20profile&amp;prompt=consent&amp;flowName=GeneralOAuthFlow</pre>
</li>
<li>The Authorization Server does whatever it does in that browser window, and when that ends, it redirects the browser with a 302 HTTP redirect to the URL that appeared in the request. It appends a CGI-style &#8220;code=&#8221; parameter to the URL, and by doing that it gives the back-end server an <em>authorization code</em>. If there was a &#8220;state&#8221; parameter in the link to the Authorization Server, it&#8217;s copied as a second parameter in this redirection. This is how the back-end server knows which request it got a response for.</li>
<li>Now that the back-end server has the authorization code, it contacts the Authorization Server directly over HTTP, and requests access tokens, using this code in the request. The Authorization Server responds with a JSON string, that contains the access token, the refresh token and other information.</li>
<li>Using the access token, the back-end server can access various Google API servers.</li>
<li>Using the refresh token, the back-end server can obtain a new access token (and possibly a new refresh token) when the existing access token is about to expire. Refresh tokens have no given expiration time, but if a new one is obtained during refresh, it should be used in following refresh requests.</li>
</ul>
<p>It may be required to add additional credentials in requests for an access token (i.e. along with an authorization code or a refresh token), namely the client_id and client_secret parameters. These credentials are relevant in particular with cloud applications, and they are obtained when registering for such.</p>
<p>So this was the scenario for a website. What about fetching mails with Thunderbird and alike? It&#8217;s basically the same principle, only that the redirection with the authorization code is handled differently. There are several other variations, depending on the capabilities of the device that needs access. Among others, there&#8217;s a browser-less option for cloud applications, which is once again a variant of the above.</p>
<p>As for Thunderbird and other MUAs, they take the role of the back-end server: If they don&#8217;t have a valid access token, they open a browser window with the Authorization Server&#8217;s URL, with all necessary parameters. The redirection to the website is done differently, but it boils down to Thunderbird obtaining the authorization code and subsequently using it to obtain the access token. And then refreshing it as necessary.</p>
<p>So to summarize: There&#8217;s a browser session that ends with an authorization code, and the application uses this authorization code to get an access token. This access token is effectively a short-lived password that is used with Google&#8217;s API servers, Google&#8217;s smtp server included.</p>
<p>And by the way, there&#8217;s a maintained <a rel="noopener" href="https://metacpan.org/pod/LWP::Authen::OAuth2" target="_blank">Perl module for OAuth2</a>. I don&#8217;t know if I should be surprised about that.</p>
<h3>fetchmail and OAuth2</h3>
<p>Fetchmail 7 is apparently going to to support OAuth2, but there&#8217;s little enthusiasm for supporting it on the long run. It also <a rel="noopener" href="https://bugzilla.redhat.com/show_bug.cgi?id=1890076" target="_blank">appears like</a> OAuth2 will not be backported to fetchmail-6.x.x.</p>
<p>To Fetchmail, the authentication tokens are just a replacement for the password. It&#8217;s another secret to send away to the server. So the entry in .fetchmailrc goes something like this:</p>
<pre>poll &lt;imap_server&gt; protocol imap
  auth <span class="punch">oauthbearer</span> username &lt;your_email&gt;
  <span class="punch">passwordfile "/home/yourname/.fetchmail-token"</span>
  is yourname here
<span class="yadayada">[ ... ]</span></pre>
<p>For this to work, there must be a mechanism for keeping the token valid. The mechanism suggested in fetchmail&#8217;s own git repository is that a cronjob first invokes a Python script that refreshes the token if necessary (and updates .fetchmail-token). Fetchmail is then called (in non-daemon mode) as part of this cronjob, and does its thing.</p>
<p>The approach for making this work automatically is to rely on the API for Google and Microsoft&#8217;s cloud services, which is intended for allowing scripts to access these services in a safely authenticated way. It seems to be an attempt to avoid the browser session at all costs. Which is understandable, given that fetchmail is traditionally a daemon that works silently in the background.</p>
<p>However using fetchmail like this requires registering the user as a Google cloud API user, which is quite difficult and otherwise annoying. So I can definitely understand the lack to of enthusiasm expressed by Fetchmail&#8217;s authors (more on that below).</p>
<p>But I beg to differ on this approach. The browser session is what Google really wants, so there&#8217;s no choice but to embrace it. Since my own motivation to use fetchmail is zero at this point, I didn&#8217;t implement anything, but this is what I would have done. And maybe will do, if it becomes relevant in the future:</p>
<p>A simple systemd-based daemon keeps track on when tokens expire, and issues refresh requests as necessary. If a valid token for a Gmail account is missing (because the refresh requests failed, or because an account was just added), this daemon draws the user&#8217;s attention to the need for an authentication session. Maybe a popup, maybe an icon on the system tray. When the user responds to that alert, a browser window opens with the relevant URL, and the authentication process takes place, ending with an authorization code, which is then turned into a valid token.</p>
<p>As for Fetchmail itself, it keeps running as usual as a daemon, only using access tokens instead of passwords. If a token is invalid, Google&#8217;s server will reject it, and if that goes on for too long, Fetchmail issues the warning mail message we&#8217;re probably all familiar with. Nothing new.</p>
<p>This doesn&#8217;t require any registration to any service. Just to enter the username and password the first time the daemon is launched, and then possibly go through whatever torture Google requires when it gets paranoid. But this is the way Google probably wants it to work, so no point trying to fight it. Frankly, I don&#8217;t quite understand why the Fetchmail guys didn&#8217;t go this way to begin with.</p>
<h3>Future of OAuth2 support</h3>
<p>Personally, I think Fetchmail should support OAuth2 authentication to the extent that it&#8217;s capable of using an access token for authentication. As for obtaining and maintaining the tokens, I can&#8217;t see why that has anything to do with Fetchmail.</p>
<p>The authors&#8217; view is currently somewhat pessimistic. To cite the <a rel="noopener" href="https://gitlab.com/fetchmail/fetchmail/-/commit/fd79252ca4929c71913b9002420fc6f474d7021f" target="_blank">relevant entry</a> in the NEWS file:</p>
<blockquote><p>OAuth2 access so far seems only to be supported by providers who want to exert control over what clients users can use to access their very own personal data, or make money out of having clients verified. There does not appear to be a standard way how service end-points are configured, so fetchmail would have to carry lots of provider-specific information, which the author cannot provide for lack of resources.</p>
<p>OAuth2 is therefore generally considered as experimental, and unsupported, OAuth2 may be removed at any time without prior warning.</p></blockquote>
<p>As for their affection for OAuth2, see the preface in <a rel="noopener" href="https://gitlab.com/fetchmail/fetchmail/-/blob/e92e57cb1ce93b5a09509e65f26bbb5aee5de533/README.OAUTH2" target="_blank">README.OAUTH2 file</a>. This file nevertheless explains how to obtain an OAuth2 client id and client secret from Google and Microsoft. Something I suggested to skip, but anyhow.</p>
<h3>App passwords</h3>
<p>This isn&#8217;t really related, but it&#8217;s often mentioned as a substitute for OAuth2, so here are a few words on that.</p>
<p>It seems like there&#8217;s <a rel="noopener" href="https://support.google.com/accounts/answer/185833?hl=en" target="_blank">a possibility</a> to generate a 16-digit password, which is specific to an app. So at least in theory, this app password could be given to Fetchmail in order to perform a regular login.</p>
<p>I didn&#8217;t pursue this direction, mainly because the generation of an app password requires two-step verification. Forwarding sounds so much nicer all of the sudden.</p>
<p>Besides, I will not be surprised if Google drops App passwords sooner or later, in particular for Gmail access.</p>
<h3>Summary</h3>
<p>I can&#8217;t say that I&#8217;m happy with OAuth2 becoming mandatory, but I guess it&#8217;s here to stay. My personal speculation is that it has become mandatory to allow Google to re-authenticate humans gracefully, possibly with increasingly annoying means. This is a fight against spammers, scammers and account hijackers, so paranoia is the name of the game.</p>
<p>Apparently, forcing the owner of the Google account into an authentication session, either with a browser on the desktop or on the mobile phone, possibly both combined, is the future weapon in this fight. It&#8217;s quite annoying indeed, but I guess there are worse problems on this planet.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2022/06/fetchmail-gmail-lsa-oauth2/feed/</wfw:commentRss>
		<slash:comments>9</slash:comments>
		</item>
		<item>
		<title>Run Firefox over X11 over SSH / VNC on a cheap virtual machine</title>
		<link>https://billauer.se/blog/2021/11/x-app-over-ssh/</link>
		<comments>https://billauer.se/blog/2021/11/x-app-over-ssh/#comments</comments>
		<pubDate>Tue, 16 Nov 2021 09:49:16 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Internet]]></category>
		<category><![CDATA[Linux]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6444</guid>
		<description><![CDATA[To run over SSH: Not This is how to run a Firefox browser on a cheap VPS machine (e.g. a Google Cloud VM Instance) with an X-server connection. It&#8217;s actually not a good idea, because it&#8217;s extremely slow. The correct way is to set up a VNC server, because the X server connection exchanges information [...]]]></description>
			<content:encoded><![CDATA[<h3>To run over SSH: Not</h3>
<p>This is how to run a Firefox browser on a cheap VPS machine (e.g. a Google Cloud VM Instance) with an X-server connection. It&#8217;s actually not a good idea, because it&#8217;s extremely slow. The correct way is to set up a VNC server, because the X server connection exchanges information on every little mouse movement or screen update. It&#8217;s a disaster on a slow connection.</p>
<p>My motivation was to download a 10 GB file from Microsoft&#8217;s cloud storage. With my own Internet connection it failed consistently after a Gigabyte or so (I guess the connection timed out). So the idea is to have Firefox running on a remote server with a much better connection. And then transfer the file.</p>
<p>Since it&#8217;s a one-off task, and I kind-of like these bizarre experiments, here we go.</p>
<p>These steps:</p>
<p>Edit /etc/ssh/sshd_config, making sure it reads</p>
<pre>X11Forwarding yes</pre>
<p>Install xauth, also necessary to open a remote X:</p>
<pre># apt install xauth</pre>
<p>Then restart the ssh server:</p>
<pre># systemctl restart ssh</pre>
<p>and then install Firefox</p>
<pre># apt install firefox-esr</pre>
<p>There will be a lot of dependencies to install.</p>
<p>At this point, it&#8217;s possible to connect to the server with ssh -X and run firefox on the remote machine.</p>
<p>Expect a horribly slow browser, though. Every small animation or mouse movement is transferred on the link, so it definitely gets stuck easily. So think before every single move, and think about every single little thing in the graphics that gets updated.</p>
<p>Firefox &#8220;cleverly&#8221; announces that &#8220;a web page is slowing down your browser&#8221; all the time, but the animation of these announcements become part of the problem.</p>
<p>It&#8217;s also a good idea to keep the window small, so there isn&#8217;t much to area to keep updated. And most important: <strong>Keep the mouse pointer off the remote window</strong> unless it&#8217;s needed there for a click. Otherwise things get stuck. Just gen into the window, click, and leave. Or stay if the click was for the sake of typing (or better, pasting something).</p>
<h3>Run over VNC instead</h3>
<p>This requires installing an X-Windows server. Not a big deal.</p>
<pre># apt update
# apt-get install xfce4
# apt install x-window-system</pre>
<p>once installed, open a VNC window. It&#8217;s really easiest by clicking a button on the user&#8217;s VPS Client Area (also available on the control panel, but why go that far) and go</p>
<pre># startx</pre>
<p>at command prompt to start the server. And then start the browser as usual.</p>
<p>It doesn&#8217;t make sense to have a login server as it slows down the boot process and eats memory. Unless a VNC connection is the intended way to always use the virtual machine.</p>
<p>Firefox is still quite slow, but not as bad as with ssh.</p>
<p>&nbsp;</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2021/11/x-app-over-ssh/feed/</wfw:commentRss>
		<slash:comments>2</slash:comments>
		</item>
		<item>
		<title>Using firejail to throttle network bandwidth for wget and such</title>
		<link>https://billauer.se/blog/2021/08/firejail-network-bandwidth-limit/</link>
		<comments>https://billauer.se/blog/2021/08/firejail-network-bandwidth-limit/#comments</comments>
		<pubDate>Sun, 15 Aug 2021 13:39:17 +0000</pubDate>
		<dc:creator>eli</dc:creator>
				<category><![CDATA[Linux]]></category>
		<category><![CDATA[Server admin]]></category>
		<category><![CDATA[Virtualization]]></category>

		<guid isPermaLink="false">https://billauer.se/blog/?p=6382</guid>
		<description><![CDATA[Introduction Occasionally, I download / upload huge files, and it kills my internet connection for plain browsing. I don&#8217;t want to halt the download or suspend it, but merely calm it down a bit, temporarily, for doing other stuff. And then let it hog as much as it want again. There are many ways to [...]]]></description>
			<content:encoded><![CDATA[<h3>Introduction</h3>
<p>Occasionally, I download / upload huge files, and it kills my internet connection for plain browsing. I don&#8217;t want to halt the download or suspend it, but merely calm it down a bit, temporarily, for doing other stuff. And then let it hog as much as it want again.</p>
<p>There are many ways to do this, and I went for firejail. I suggest reading <a title="Firejail: Putting a program in its own little container" href="https://billauer.se/blog/2020/06/firejail-cgroups/" target="_blank">this post of mine</a> as well on this tool.</p>
<p>Firejail gives you a shell prompt, which runs inside a mini-container, like those cheap virtual hosting services. Then run wget or youtube-dl as you wish from that shell.</p>
<p>It has practically access to everything on the computer, but the network interface is controlled. Since firejail is based on cgroups, all processes and subprocesses are collectively subject to the network bandwidth limit.</p>
<p>Using firejail requires setting up a bridge network interface. This is a bit of container hocus-pocus, and is necessary to get control  over the network data flow. But it&#8217;s simple, and it can be done once  (until the next reboot, unless the bridge is configured permanently,  something I don&#8217;t bother).</p>
<h3>Setting up a bridge interface</h3>
<p>Remember: Do this once, and just don&#8217;t remove the interface when done with it.</p>
<p>You might need to</p>
<pre># <strong>apt install bridge-utils</strong></pre>
<p>So first, set up a new bridge device (as root):</p>
<pre># <strong>brctl addbr hog0</strong></pre>
<p>and give it an IP address that doesn&#8217;t collide with anything else on the system. Otherwise, it really doesn&#8217;t matter which:</p>
<pre># <strong>ifconfig hog0 10.22.1.1/24</strong></pre>
<p>What&#8217;s going to happen is that there will be a network interface named eth0 inside the container, which will behave as if it was connected to a real Ethernet card named hog0 on the computer. Hence the container has access to everything that is covered by the routing table (by means of IP forwarding), and is also subject to the firewall rules. With my specific firewall setting, it prevents some access, but ppp0 isn&#8217;t blocked, so who cares.</p>
<p>To remove the bridge (no real reason to do it):</p>
<pre># <strong>brctl delbr hog0</strong></pre>
<h3>Running the container</h3>
<p>Launch a shell with firejail (I called it &#8220;nethog&#8221; in this example):</p>
<pre>$ <strong>firejail --net=hog0 --noprofile --name=nethog</strong></pre>
<p>This starts a new shell, for which the bandwidth limit is applied. Run wget or whatever from here.</p>
<p>Note that despite the &#8211;noprofile flag, there are still some directories that are read-only and some are temporary as well. It&#8217;s done in a sensible way, though so odds are that it won&#8217;t cause any issues. Running &#8220;df&#8221; inside the container gives an idea on what is mounted how, and it&#8217;s scarier than the actual situation.</p>
<p>But <strong>be sure to check that the files that are downloaded are visible outside the container</strong>.</p>
<p>From another shell prompt, <strong>outside the container</strong> go something like (<strong>doesn&#8217;t </strong>require root):</p>
<pre>$ <strong>firejail --bandwidth=nethog set hog0 800 75</strong>
Removing bandwith limit
Configuring interface eth0
Download speed  6400kbps
Upload speed  600kbps
cleaning limits
configuring tc ingress
configuring tc egress</pre>
<p>To drop the bandwidth limit:</p>
<pre>$ <strong>firejail --bandwidth=nethog clear hog0</strong></pre>
<p>And get the status (saying, among others, how many packets have been dropped):</p>
<pre>$ <strong>firejail --bandwidth=nethog status</strong></pre>
<p>Notes:</p>
<ul>
<li>The &#8220;eth0&#8243; mentioned in firejail&#8217;s output blob relates to the interface name <strong>inside</strong> the container. So the &#8220;real&#8221; eth0 remains untouched.</li>
<li>Actual download speed is slightly slower.</li>
<li>The existing group can be joined by new processes with firejail &#8211;join, as well as from firetools.</li>
<li>Several containers may use the same bridge (hog0 in the example  above), in which case each has its own independent bandwidth setting.  Note that the commands configuring the bandwidth limits mention both the  container&#8217;s name and the bridge.</li>
</ul>
<h3>Working with browsers</h3>
<p>When starting a browser from within a container, pay attention to  whether it really started a new process. Using firetools can help.</p>
<p>If  Google Chrome says &#8220;Created new window in existing browser session&#8221;, it <strong>didn&#8217;t</strong> start a new process inside the container, in which case the window isn&#8217;t subject to bandwidth limitation.</p>
<p>So close all windows of Chrome before kicking off a new one. Alternatively, this can we worked around by starting the container with.</p>
<pre>$ firejail --net=hog0 --noprofile <strong>--private</strong> --name=nethog</pre>
<p>The &#8211;private flags creates, among others, a new <strong>volatile</strong> home directory, so Chrome doesn&#8217;t detect that it&#8217;s already running. Because I use some other disk mounts for the large partitions on my computer, it&#8217;s still possible to download stuff to them from within the container.</p>
<p>But extra care is required with this, and regardless, the new browser doesn&#8217;t remember passwords and such from the private container.</p>
<h3>Using a different version of Google Chrome</h3>
<p>This isn&#8217;t really related, and yet: What if I want to use a different version of Chrome momentarily, without upgrading? This can be done by downloading the .deb package, and extracting its files as shown on <a href="https://billauer.se/blog/2014/11/apt-dpkg-ubuntu-pin/" target="_blank">this post</a>. Then copy the directory opt/google/chrome in the package&#8217;s &#8220;data&#8221; files to somewhere reachable by the jail (e.g. /bulk/transient/google-chrome-105.0/).</p>
<p>All that is left is to start a jail with the &#8211;private option as shown above (possibly without the &#8211;net flag, if throttling isn&#8217;t required) and go e.g.</p>
<pre>$ /bulk/transient/google-chrome-105.0/chrome &amp;</pre>
<p>So the new browser can run while there are still windows of the old one open. The advantage and disadvantage of jailing is that there&#8217;s no access to the persistent data. So the new browser doesn&#8217;t remember passwords. This is also an advantage, because there&#8217;s a chance that the new version will mess up things for the old version.</p>
]]></content:encoded>
			<wfw:commentRss>https://billauer.se/blog/2021/08/firejail-network-bandwidth-limit/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
